feat: 完善WCS调度、库存追溯和出库分配

This commit is contained in:
zgw
2026-07-15 16:24:07 +08:00
parent 9d3aa7a3bd
commit 66ac268524
101 changed files with 12596 additions and 669 deletions

View File

@@ -0,0 +1,186 @@
# Cargo Area Direction Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Convert warehouse 03 cargo-area codes from `1`–`8` to `A`–`H`, add configurable access directions, and apply those directions to dense-storage inbound and outbound candidate ordering.
**Architecture:** `bas_cargo_area_config` is the source of truth per warehouse and area. Direction values are `1` (small-to-large), `2` (large-to-small), and `3` (both ends); missing configuration falls back to `1` so other warehouses and existing behavior remain stable. Warehouse 03 cargo spaces are migrated transactionally, and both current allocation SQL paths join the configuration table.
**Tech Stack:** SQL Server, Spring Boot/Java 17, MyBatis XML, Vue 2, JUnit 5/AssertJ, Playwright
---
### Task 1: Define executable migration contracts
**Files:**
- Create: `ruoyi-admin/src/test/java/com/ruoyi/bas/cargospace/mapper/CargoAreaDirectionMigrationSqlTest.java`
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/pda/mapper/PdaMapperSqlTest.java`
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/bas/cargospace/mapper/BasCargoSpaceMapperSqlTest.java`
- [ ] **Step 1: Add migration assertions for table structure, A-H conversion, and seed directions**
```java
assertThat(sql)
.contains("create table dbo.bas_cargo_area_config")
.contains("direction_type")
.contains("nchar(unicode(n'a')")
.contains("n'a'", "n'h'");
```
- [ ] **Step 2: Add mapper assertions for all three direction branches**
```java
assertThat(lockSql)
.contains("bas_cargo_area_config")
.contains("when '1' then")
.contains("when direction_type = '2'")
.contains("when direction_type = '3'");
```
- [ ] **Step 3: Run focused tests before implementation**
Run: `mvn -pl ruoyi-admin -am -Dtest=CargoAreaDirectionMigrationSqlTest,PdaMapperSqlTest,BasCargoSpaceMapperSqlTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: FAIL until the migration and mapper ordering exist.
### Task 2: Create and seed cargo-area direction configuration
**Files:**
- Create: `sql/20260714_cargo_area_direction.sql`
- Modify: `sql/20260714_warehouse_03_cargo_spaces.sql`
- [ ] **Step 1: Create the configuration table with a unique warehouse/area key**
```sql
CREATE TABLE dbo.bas_cargo_area_config (
id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY,
warehouse_id nvarchar(80) NOT NULL,
cargo_area_code nvarchar(10) NOT NULL,
cargo_area_name nvarchar(80) NOT NULL,
direction_type nvarchar(2) NOT NULL,
is_active nchar(1) NOT NULL CONSTRAINT DF_bas_cargo_area_config_active DEFAULT N'1',
create_by nvarchar(64) NULL,
create_time datetime2(3) NULL,
update_by nvarchar(64) NULL,
update_time datetime2(3) NULL,
remark nvarchar(500) NULL,
CONSTRAINT UQ_bas_cargo_area_config UNIQUE (warehouse_id, cargo_area_code),
CONSTRAINT CK_bas_cargo_area_config_direction CHECK (direction_type IN (N'1', N'2', N'3'))
);
```
- [ ] **Step 2: Convert warehouse 03 numeric codes to letters transactionally**
```sql
UPDATE dbo.bas_cargo_space
SET this_cargo_area = NCHAR(UNICODE(N'A') + TRY_CONVERT(int, this_cargo_area) - 1)
WHERE warehouse_id = N'03'
AND TRY_CONVERT(int, this_cargo_area) BETWEEN 1 AND 8;
```
- [ ] **Step 3: Seed A-H with direction `1` to preserve current ordering**
```sql
INSERT INTO dbo.bas_cargo_area_config (warehouse_id, cargo_area_code, cargo_area_name, direction_type, is_active)
SELECT N'03', area_code, area_code + N'货区', N'1', N'1'
FROM (VALUES (N'A'),(N'B'),(N'C'),(N'D'),(N'E'),(N'F'),(N'G'),(N'H')) x(area_code);
```
- [ ] **Step 4: Make the warehouse-03 generator emit A-H directly**
Use `NCHAR(UNICODE(N'A') + area_number - 1)` instead of converting the area number to text.
### Task 3: Apply direction to inbound allocation
**Files:**
- Modify: `ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml`
- Modify: `ruoyi-admin/src/main/resources/mapper/bas/BasCargoSpaceMapper.xml`
- [ ] **Step 1: Join active area configuration in the atomic WCS allocator**
```sql
LEFT JOIN bas_cargo_area_config cac
ON cac.warehouse_id = bcs.warehouse_id
AND cac.cargo_area_code = bcs.this_cargo_area
AND cac.is_active = N'1'
```
- [ ] **Step 2: Calculate each area's column bounds and order by direction**
```sql
CASE WHEN direction_type = N'1' THEN the_column END ASC,
CASE WHEN direction_type = N'2' THEN the_column END DESC,
CASE WHEN direction_type = N'3'
THEN CASE WHEN the_column - area_min_column <= area_max_column - the_column
THEN the_column - area_min_column ELSE area_max_column - the_column END
END ASC
```
- [ ] **Step 3: Apply identical fallback ordering to occupied-location reallocation**
Keep height priority and platoon balancing ahead of the direction-specific column order.
### Task 4: Apply direction to outbound inventory candidates
**Files:**
- Modify: `sql/out_stockout_flow.sql`
- [ ] **Step 1: Join area configuration and derive area bounds in manual allocation**
```sql
LEFT JOIN dbo.bas_cargo_area_config cac
ON cac.warehouse_id = bs.warehouse_id
AND cac.cargo_area_code = bcs.this_cargo_area
AND cac.is_active = N'1'
```
- [ ] **Step 2: Put direction ordering before FIFO tie breakers**
Order by configured column direction, then `bs.in_date`, `bs.cargo_space_id`, and `bsd.detail_id`.
- [ ] **Step 3: Apply the same ordering to automatic allocation cursor candidates**
Use the same `1`/`2`/`3` CASE expressions in both outbound procedures.
### Task 5: Update dense-map area labels
**Files:**
- Modify: `ruoyi-ui/src/views/kpi/plan/components/DenseCargoSpaceMap.vue`
- Modify: `scripts/verify_dense_cargo_space_plan.py`
- [ ] **Step 1: Display letter area codes without numeric padding or the old `Z` prefix**
```vue
<span v-if="activeLocation.thisCargoArea">货区 {{ activeLocation.thisCargoArea }}</span>
```
- [ ] **Step 2: Add browser assertions for A-H values**
Click an ordinary node and assert the footer contains `货区 A`–`货区 H`, never `区域 Z`.
### Task 6: Verify and deploy
**Files:**
- Verify: all files above
- [ ] **Step 1: Run focused backend tests**
Run: `mvn -pl ruoyi-admin -am -Dtest=CargoAreaDirectionMigrationSqlTest,PdaMapperSqlTest,BasCargoSpaceMapperSqlTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: PASS.
- [ ] **Step 2: Execute the SQL migration in one database transaction**
Expected: warehouse 03 has exactly 1028 ordinary nodes with A-H and eight active configuration rows.
- [ ] **Step 3: Build the frontend**
Run: `cd ruoyi-ui && npm run build:prod`
Expected: build completes successfully.
- [ ] **Step 4: Run the dense-map browser verification**
Run: `python scripts/verify_dense_cargo_space_plan.py`
Expected: both floors render, no overflow occurs, A-H area labels display, and legacy warehouse rendering remains visible.

View File

@@ -0,0 +1,181 @@
# Dense Cargo Space Plan Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Display warehouse 03 as a floor-selectable 13排 × 25列 dense-storage map matching the supplied layout while preserving the existing heatmap for every other warehouse.
**Architecture:** Add a dedicated read-only KPI endpoint for dense-layout coordinates so the legacy `/kpi/cargoSpacePlan` response remains unchanged. The Vue page branches only when `warehouseId === '03'`, rendering a focused dense-map component with exact coordinate placement, floor controls, special-node colors, and per-floor counts; all other warehouses continue through the current roadway/platoon/ECharts path.
**Tech Stack:** Java 17, Spring Boot, Mockito/JUnit 5, Vue 2, Element UI, ECharts 5, SCSS, Playwright.
---
### Task 1: Add a dense-plan KPI response
**Files:**
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/kpi/controller/KpiController.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/kpi/service/KpiService.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/kpi/service/impl/KpiServiceImpl.java`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/kpi/service/impl/KpiServiceImplTest.java`
- [x] **Step 1: Write the failing service test**
Add a test that returns two warehouse-03 `BasCargoSpace` records on different floors and asserts that `getDenseCargoSpacePlanData("03")` returns sorted `floors` plus location maps containing `cargoSpaceId`, `thePlatoon`, `theColumn`, `theFloor`, `status`, `groupCoding`, and `thisCargoArea`.
```java
Map<String, Object> result = service.getDenseCargoSpacePlanData("03");
assertThat(result.get("floors")).isEqualTo(List.of(1L, 2L));
assertThat(result.get("locations")).asList().hasSize(2);
```
- [x] **Step 2: Run the test and verify the missing-method failure**
Run: `D:/BDevTools/maven/apache-maven-3.9.11/bin/mvn.cmd -pl ruoyi-admin -am -Dtest=KpiServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: compilation fails because `getDenseCargoSpacePlanData` does not exist.
- [x] **Step 3: Implement the service and endpoint**
Add `GET /kpi/denseCargoSpacePlan?warehouseId=03` protected by `kpi:cargo:space:plan`. Query `BasCargoSpaceMapper` by warehouse, derive sorted floors, and return plain location maps without JSON-string encoding.
```java
@GetMapping("/denseCargoSpacePlan")
@PreAuthorize("@ss.hasPermi('kpi:cargo:space:plan')")
public AjaxResult getDenseCargoSpacePlan(String warehouseId) {
return AjaxResult.success(kpiService.getDenseCargoSpacePlanData(warehouseId));
}
```
- [x] **Step 4: Run the focused backend test**
Run: `D:/BDevTools/maven/apache-maven-3.9.11/bin/mvn.cmd -pl ruoyi-admin -am -Dtest=KpiServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: all `KpiServiceImplTest` tests pass.
### Task 2: Build the floor-selectable dense map
**Files:**
- Create: `ruoyi-ui/src/views/kpi/plan/components/DenseCargoSpaceMap.vue`
- Modify: `ruoyi-ui/src/api/kpi/kpi.js`
- Modify: `ruoyi-ui/src/views/kpi/plan/index.vue`
- [x] **Step 1: Add the dense-plan API client**
```javascript
export function getDenseChartData(query) {
return request({
url: '/kpi/denseCargoSpacePlan',
method: 'get',
params: query
})
}
```
- [x] **Step 2: Create the coordinate-grid component**
Render a top axis row, 25 descending 列 rows, 13 ascending 排 columns, and a bottom axis row. Place each location with `gridColumn = thePlatoon + 1` and `gridRow = 27 - theColumn`; use status 7 brown, status 8 yellow, status 9 green, and show the eight-digit cargo-space ID inside every real node.
```javascript
cellStyle(location) {
return {
gridColumn: String(Number(location.thePlatoon) + 1),
gridRow: String(27 - Number(location.theColumn))
}
}
```
- [x] **Step 3: Branch warehouse 03 without changing legacy behavior**
Keep the warehouse selector for all warehouses. For warehouse 03, hide roadway/platoon filters, call the dense endpoint, and show floor buttons 1-5. For any other warehouse, retain the existing roadway/platoon calls and ECharts heatmap code.
```javascript
isDenseWarehouse() {
return String(this.queryParams.warehouseId) === '03'
}
```
- [x] **Step 4: Build the production frontend**
Run: `npm run build:prod` from `ruoyi-ui`.
Expected: Vue production build succeeds with no compile errors.
### Task 3: Verify behavior and visual fidelity
**Files:**
- Create: `scripts/verify_dense_cargo_space_plan.py`
- Verify: `ruoyi-ui/src/views/kpi/plan/index.vue`
- Verify: `ruoyi-ui/src/views/kpi/plan/components/DenseCargoSpaceMap.vue`
- [x] **Step 1: Start the backend and frontend with the webapp-testing helper**
Run `python C:/Users/13623/.agents/skills/webapp-testing/scripts/with_server.py --help` first, then start the Spring Boot backend and Vue dev server through the helper.
- [x] **Step 2: Verify warehouse 03 in Chromium**
Open the KPI cargo-space plan, select warehouse 03, wait for network idle, and verify floor controls, 259 current-floor nodes, the green 王-shaped mother channel, two brown lifts, two yellow conveyors, axis labels, and no console errors.
- [x] **Step 3: Verify a legacy warehouse**
Switch to warehouse 01 and verify roadway/platoon controls and the existing ECharts heatmap remain visible.
- [ ] **Step 4: Commit**
```bash
git add docs/superpowers/plans/2026-07-14-dense-cargo-space-plan.md scripts/verify_dense_cargo_space_plan.py ruoyi-admin/src/main/java/com/ruoyi/kpi/controller/KpiController.java ruoyi-admin/src/main/java/com/ruoyi/kpi/service/KpiService.java ruoyi-admin/src/main/java/com/ruoyi/kpi/service/impl/KpiServiceImpl.java ruoyi-admin/src/test/java/com/ruoyi/kpi/service/impl/KpiServiceImplTest.java ruoyi-ui/src/api/kpi/kpi.js ruoyi-ui/src/views/kpi/plan/index.vue ruoyi-ui/src/views/kpi/plan/components/DenseCargoSpaceMap.vue
git commit -m "feat: add dense warehouse floor plan"
```
Only perform this step when the user explicitly requests a Git commit.
### Task 4: Make the dense map viewport adaptive
**Files:**
- Modify: `ruoyi-ui/src/views/kpi/plan/index.vue`
- Modify: `ruoyi-ui/src/views/kpi/plan/components/DenseCargoSpaceMap.vue`
- Modify: `scripts/verify_dense_cargo_space_plan.py`
- [x] **Step 1: Move floor actions into the existing search form**
For warehouse 03, render `el-radio-group` floor buttons and a refresh button after the warehouse selector. Remove the dense-mode explanatory sentence. Pass only the selected floor and locations into the map component.
```vue
<template v-if="isDenseWarehouse">
<el-form-item label="层">
<el-radio-group v-model="selectedDenseFloor" size="small">
<el-radio-button v-for="floor in denseFloors" :key="floor" :label="floor">{{ floor }}层</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button icon="el-icon-refresh" @click="loadDenseChartData">刷新</el-button>
</el-form-item>
</template>
```
- [x] **Step 2: Remove the map header and column 25**
Delete the warehouse eyebrow, title, subtitle, internal floor switch, and internal refresh action. Generate column labels from 24 down to 1, place data rows with `gridRow = 26 - theColumn`, and move the bottom 排 axis to row 26.
```javascript
columnLabels() {
return Array.from({ length: 24 }, (_, index) => 24 - index)
}
```
- [x] **Step 3: Fit the full grid without internal scrolling**
Use a fixed 13-column fraction grid and 24 adaptive rows. Set the viewport to `overflow: hidden`; compute the map row height from available viewport height with CSS `clamp()`, reduce summary/legend/footer vertical space, and allow cargo IDs to scale with `clamp()`.
```scss
.dense-map__viewport { overflow: hidden; }
.dense-grid {
grid-template-columns: 36px repeat(13, minmax(0, 1fr));
grid-template-rows: 24px repeat(24, minmax(18px, 1fr)) 28px;
width: 100%;
min-width: 0;
}
```
- [x] **Step 4: Rebuild and verify responsive behavior**
Run `npm run build:prod`, then run `python scripts/verify_dense_cargo_space_plan.py`. Assert column 25 is absent, floor controls are in `.search-box`, the dense map has 259 nodes, and both `.dense-map__viewport` and `.dense-grid` have `scrollWidth <= clientWidth` and `scrollHeight <= clientHeight` at 1500×1050 and 1280×800 viewports.

View File

@@ -0,0 +1,191 @@
# 03 Warehouse Dense Inbound Lane Allocation Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Configure deterministic A-H cargo-area directions and make warehouse 03 allocate dense-storage lanes by empty tray, raw-material identity, or project number without mixing incompatible cargo in one `group_coding` lane.
**Architecture:** Resolve the exact inbound job and its details into a typed storage profile before selecting a location. For warehouse 03, serialize allocation by warehouse and roadway, evaluate completed stock plus active/reserved inbound jobs for every lane, choose a compatible existing lane before a new empty lane (empty trays use lane-level pseudo-random selection), and then choose the next slot using the configured cargo-area direction. Other warehouses retain the current location selection behavior.
**Tech Stack:** Java 17, Spring Boot, MyBatis XML, SQL Server application locks and CTEs, JUnit 5, Mockito, AssertJ, Maven.
---
## File map
- Create `ruoyi-admin/src/main/java/com/ruoyi/in/job/domain/InboundStorageProfile.java`: immutable four-mode storage profile and mapper-parameter export.
- Modify `ruoyi-admin/src/main/java/com/ruoyi/in/job/domain/MydoJob.java`: transient profile fields used by occupied-location reallocation.
- Modify `ruoyi-admin/src/main/java/com/ruoyi/in/job/service/IMydoJobService.java`: expose profile resolution for the WCS location request path.
- Modify `ruoyi-admin/src/main/java/com/ruoyi/in/job/service/impl/MydoJobServiceImpl.java`: validate exact job details and resolve the four storage modes; attach the same profile during occupied-location reallocation.
- Modify `ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java`: apply dense profile rules only to warehouse 03 and acquire the roadway allocation lock before choosing a new slot.
- Modify `ruoyi-admin/src/main/java/com/ruoyi/pda/mapper/PdaMapper.java`: add the roadway-lock mapper contract.
- Modify `ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml`: add roadway serialization and lane-aware atomic selection.
- Modify `ruoyi-admin/src/main/resources/mapper/bas/BasCargoSpaceMapper.xml`: apply the same compatibility and direction rules to occupied-location reallocation.
- Modify `sql/20260714_cargo_area_direction.sql`: make the default warehouse 03 directions A/B/C/E ascending and D/F/G/H descending.
- Create `sql/20260714_warehouse_03_inbound_lane_allocation.sql`: idempotently apply the final direction configuration and supporting indexes to an already-migrated database.
- Modify focused JUnit tests under `ruoyi-admin/src/test/java/com/ruoyi/pda` and `ruoyi-admin/src/test/java/com/ruoyi/in/job`: prove profile validation, lock order, lane compatibility, randomization scope, and the reallocation guard.
### Task 1: Define and validate the four inbound storage profiles
**Files:**
- Create: `ruoyi-admin/src/main/java/com/ruoyi/in/job/domain/InboundStorageProfile.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/in/job/service/IMydoJobService.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/in/job/service/impl/MydoJobServiceImpl.java`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/in/job/service/impl/MydoJobServiceImplTest.java`
- [ ] **Step 1: Write failing service tests**
Add tests that resolve these exact inputs: `KTP0001` to `EMPTY_TRAY`; purchase type `1` with blank project to `RAW_PRODUCT`; purchase type `1` with one nonblank project to `PROJECT`; production type `2` with one nonblank project to `PROJECT`. Add rejection tests for mixed product code/name in a projectless purchase tray, mixed project numbers, production without project number, missing job/details/stock-in bill, and unsupported inbound type.
- [ ] **Step 2: Run the focused tests and verify failure**
Run: `mvn -pl ruoyi-admin -Dtest=MydoJobServiceImplTest test`
Expected: FAIL because `InboundStorageProfile` and `resolveInboundStorageProfile` do not exist.
- [ ] **Step 3: Implement the profile value object and resolver**
Define modes `EMPTY_TRAY`, `RAW_PRODUCT`, and `PROJECT`; preserve `inType`, `jobId`, trimmed `projectNo`, `productId`, and `productName`. Resolve the exact latest inbound job for the tray and warehouse, load all details by `jobId`, and use the referenced `in_stockin.in_type`. Treat empty trays by the `KTP0001` detail identity; require one exact `productId + productName` pair for projectless purchase; require one nonblank project for project purchase and production.
- [ ] **Step 4: Run focused tests and verify pass**
Run: `mvn -pl ruoyi-admin -Dtest=MydoJobServiceImplTest test`
Expected: PASS.
### Task 2: Serialize warehouse 03 lane allocation
**Files:**
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/mapper/PdaMapper.java`
- Modify: `ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/pda/mapper/PdaMapperSqlTest.java`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/pda/service/impl/PdaServiceImplTest.java`
- [ ] **Step 1: Write failing lock and parameter tests**
Assert that warehouse 03 resolves and forwards `v_job_id`, `v_storage_mode`, `v_project_no`, `v_product_id`, `v_product_name`, and `v_in_type`; that other warehouses do not require a profile; and that a second transaction-owned application lock uses warehouse plus roadway and is acquired only after the existing tray lock and locked-location lookup.
- [ ] **Step 2: Run the focused tests and verify failure**
Run: `mvn -pl ruoyi-admin -Dtest=PdaServiceImplTest,PdaMapperSqlTest test`
Expected: FAIL because the profile parameters and roadway lock are absent.
- [ ] **Step 3: Implement the roadway lock and service orchestration**
Keep the existing warehouse+tray lock for idempotency. If no location is already locked and `warehouseId` is `03`, resolve the storage profile, add it to the selection map, then acquire `sp_getapplock` with resource `wms:inbound-roadway:<hash(warehouse,roadway)>`, `Exclusive`, `Transaction`, and a 10-second timeout. Do not change other warehouses' current selection inputs or behavior.
- [ ] **Step 4: Run focused tests and verify pass**
Run: `mvn -pl ruoyi-admin -Dtest=PdaServiceImplTest,PdaMapperSqlTest test`
Expected: PASS.
### Task 3: Select a compatible `group_coding` lane atomically
**Files:**
- Modify: `ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/pda/mapper/PdaMapperSqlTest.java`
- [ ] **Step 1: Write failing SQL contract tests**
Assert the warehouse 03 SQL requires nonblank `group_coding`, reads positive-quantity `bas_stock + bas_stock_detail`, reads active `mydo_job + mydo_job_detail`, recognizes status-3 reserved slots by tray even before the job location is written back, excludes the current job, and rejects unknown occupied slots. Assert exact compatibility by mode: only `KTP0001` for empty trays, only projectless purchase rows with equal trimmed product code and name for `RAW_PRODUCT`, and only equal nonblank project numbers for `PROJECT`.
- [ ] **Step 2: Assert ranking and randomization rules**
Require project and raw-material allocation to prefer a compatible occupied lane over an empty lane and consolidate into the most-filled compatible lane. Require empty trays to randomize once per lane using `CHECKSUM(group_coding, tray_code)`, never once per slot. Require the slot inside the selected lane to retain height priority and configured direction order.
- [ ] **Step 3: Run the mapper test and verify failure**
Run: `mvn -pl ruoyi-admin -Dtest=PdaMapperSqlTest test`
Expected: FAIL because the current SQL ranks individual empty slots and never inspects lane contents.
- [ ] **Step 4: Implement the SQL Server CTE-based candidate update**
Build profiled occupancy from completed stock, active inbound jobs with assigned locations, and status-3 reservations joined by tray. Add an unknown profile for an occupied slot without traceable data so the lane fails closed. Aggregate by `group_coding`, retain only fully compatible lanes, rank lanes before slots, and perform one guarded `UPDATE ... WITH (UPDLOCK, READPAST, ROWLOCK)` from the chosen candidate. Keep the old candidate query under the non-03 branch.
- [ ] **Step 5: Run the mapper test and verify pass**
Run: `mvn -pl ruoyi-admin -Dtest=PdaMapperSqlTest test`
Expected: PASS.
### Task 4: Preserve lane rules during occupied-location reallocation
**Files:**
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/in/job/domain/MydoJob.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/in/job/service/impl/MydoJobServiceImpl.java`
- Modify: `ruoyi-admin/src/main/resources/mapper/bas/BasCargoSpaceMapper.xml`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/in/job/service/impl/MydoJobServiceImplTest.java`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/bas/cargospace/mapper/BasCargoSpaceMapperSqlTest.java`
- [ ] **Step 1: Write failing reallocation tests**
Assert that `handleWcsPutOccupiedException` resolves the exact job's storage profile before marking the old location, attaches the profile fields to the mapper query, and aborts without changing location when the profile is invalid. Assert the reallocation SQL uses stock, active/reserved jobs, current-job exclusion, compatibility, and `group_coding` ranking.
- [ ] **Step 2: Run focused tests and verify failure**
Run: `mvn -pl ruoyi-admin -Dtest=MydoJobServiceImplTest,BasCargoSpaceMapperSqlTest test`
Expected: FAIL because reallocation currently uses only tray type, height, and direction.
- [ ] **Step 3: Reuse the profile and lane predicate**
Resolve the exact job before the old location becomes status `5`, copy the profile to transient `MydoJob` fields, and make `selectAvailableInboundCargoSpaceInRoadway` use the same warehouse-03 lane-compatibility and ranking rules. Keep its existing behavior for other warehouses.
- [ ] **Step 4: Run focused tests and verify pass**
Run: `mvn -pl ruoyi-admin -Dtest=MydoJobServiceImplTest,BasCargoSpaceMapperSqlTest test`
Expected: PASS.
### Task 5: Apply deterministic A-H direction configuration
**Files:**
- Modify: `sql/20260714_cargo_area_direction.sql`
- Create: `sql/20260714_warehouse_03_inbound_lane_allocation.sql`
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/bas/cargospace/mapper/CargoAreaDirectionMigrationSqlTest.java`
- [ ] **Step 1: Write the failing direction migration test**
Assert final values `A=1`, `B=1`, `C=1`, `D=2`, `E=1`, `F=2`, `G=2`, and `H=2`, with active state `1`, and assert the migration is idempotent.
- [ ] **Step 2: Run the focused test and verify failure**
Run: `mvn -pl ruoyi-admin -Dtest=CargoAreaDirectionMigrationSqlTest test`
Expected: FAIL because all eight current defaults are direction `1`.
- [ ] **Step 3: Implement and apply the migration**
Use a `MERGE`/guarded update inside `TRY/CATCH` transaction handling. Preserve the existing table and unrelated warehouses. Add only supporting nonclustered indexes that are absent. Execute against the configured disposable/current schema and query the eight rows afterward.
- [ ] **Step 4: Run the direction test and verify pass**
Run: `mvn -pl ruoyi-admin -Dtest=CargoAreaDirectionMigrationSqlTest test`
Expected: PASS.
### Task 6: End-to-end verification and completion audit
**Files:**
- Verify all files above; no additional production changes unless a failing check exposes a gap.
- [ ] **Step 1: Run all backend tests**
Run: `mvn test`
Expected: BUILD SUCCESS with every test passing.
- [ ] **Step 2: Run synthetic SQL business scenarios in a rollback transaction**
Create isolated warehouse-03 fixture lanes and prove: empty tray chooses only empty/empty-tray lanes; projectless raw material rejects a different code or name; purchase project and production project accept the same project lane and reject another project; active jobs and status-3 reservations block incompatible allocations; each A-H area returns its configured first column. Roll back all fixture data.
- [ ] **Step 3: Verify the deployed direction rows**
Query `bas_cargo_area_config` for warehouse `03` ordered A-H and confirm `1,1,1,2,1,2,2,2`.
- [ ] **Step 4: Audit every explicit requirement**
Map each of the four user modes, the two inbound types, all eight directions, other-warehouse preservation, completed/in-flight occupancy, and concurrent allocation to a passing test or database result. Do not mark completion if any item lacks direct evidence.

View File

@@ -0,0 +1,158 @@
# Dense WCS Pull and Warehouse Routing Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Keep warehouse 03 inbound jobs pending for WCS pull, prevent duplicate active tray jobs, route push-mode jobs to each warehouse's configured WCS, and remove inbound-point selection from PDA tray binding.
**Architecture:** Warehouse 03 is the dense warehouse. PDA binding creates a status-2/phase-0 job without an entrance or outbound HTTP call; `POST /api/external/wcs/inbound/job` atomically claims that job by tray, writes the WCS-provided entrance and height, changes it to status-3/phase-1, and returns the stored job. Other warehouses keep push mode, but `WcsClient` resolves a warehouse-specific base URL from configuration. A SQL Server unique filtered index is the final concurrency-safe guarantee that `mydo_job.tray_code` is unique.
**Tech Stack:** Java 17, Spring Boot, MyBatis XML, SQL Server, JUnit 5/Mockito/AssertJ, Vue 2 uni-app.
---
## File map
- Modify `ruoyi-admin/src/main/java/com/ruoyi/api/config/ExternalProp.java`: support WCS base URLs keyed by warehouse ID.
- Modify `ruoyi-admin/src/main/java/com/ruoyi/api/client/BaseExternalClient.java`: resolve an API URL against a supplied base URL.
- Modify `ruoyi-admin/src/main/java/com/ruoyi/api/client/WcsClient.java`: route job push calls by `job.warehouseId`.
- Create `ruoyi-admin/src/main/java/com/ruoyi/api/domain/WcsDenseInboundJobRequest.java`: tray barcode, entrance number, and cargo height request.
- Modify `ruoyi-admin/src/main/java/com/ruoyi/api/controller/ExternalApiController.java`: expose the dense WCS job-pull endpoint.
- Modify `ruoyi-admin/src/main/java/com/ruoyi/in/job/mapper/MydoJobMapper.java` and `ruoyi-admin/src/main/resources/mapper/in/MydoJobMapper.xml`: atomic dense-job claim and tray-existence contracts.
- Modify `ruoyi-admin/src/main/java/com/ruoyi/in/job/service/IMydoJobService.java` and `ruoyi-admin/src/main/java/com/ruoyi/in/job/service/impl/MydoJobServiceImpl.java`: claim/validate the dense job and give duplicate trays a business error.
- Modify `ruoyi-admin/src/main/java/com/ruoyi/pda/domain/BindTrayRequest.java`, `ruoyi-admin/src/main/java/com/ruoyi/pda/domain/AddInJobRequest.java`, and `ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java`: make entrance optional for binding and branch dense pull mode from ordinary push mode.
- Modify `pda/pages/warehouse/inbound/bindTray.vue` and `pda/api/warehouse/inbound.js`: remove inbound-point loading, selection, validation, and submission.
- Modify `ruoyi-admin/src/main/resources/application.yml`: document independently configurable warehouse WCS base URLs.
- Create `sql/20260714_mydo_job_tray_unique_and_wcs_pull.sql`: fail clearly on existing duplicates and add the unique index.
- Add/modify focused tests under `ruoyi-admin/src/test/java/com/ruoyi/api`, `ruoyi-admin/src/test/java/com/ruoyi/in/job`, and `ruoyi-admin/src/test/java/com/ruoyi/pda`.
### Task 1: Prove dense pending and station-free PDA binding
- [ ] Add service tests with a warehouse-03 request whose `stationPointId` is null. Assert the generated job is initialized with `status="2"`, `executionPhase="0"`, blank `entranceId`, blank roadway/location, and `wcsClient.sendPutawayTask` is never called.
- [ ] Add the ordinary-warehouse counterpart. Stub `selectAvailableInboundRoadway` and assert the job is still pushed, while no station is occupied and no entrance is written.
- [ ] Run `mvn -pl ruoyi-admin -am -Dtest=PdaServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`; expect the new assertions to fail before implementation.
- [ ] Implement a station-free branch equivalent to:
```java
if (DENSE_WAREHOUSE_ID.equals(request.getWarehouseId())) {
job.setStatus("2");
job.setExecutionPhase("0");
return resultMsg + ";密集库作业已生成,等待WCS获取";
}
job.setRoadwayId(String.valueOf(selectAvailableInboundRoadway(
request.getTrayCode(), request.getWarehouseId()).get("roadway_id")));
return dispatchInboundJob(job, resultMsg);
```
- [ ] Remove station-point controls and `stationPointId` from the PDA payload, then re-run the focused test.
### Task 2: Add concurrency-safe tray uniqueness
- [ ] Add mapper/service tests asserting a nonblank tray already in `mydo_job` is rejected with `托盘已存在未完成作业` before insert/procedure execution.
- [ ] Add an idempotent SQL migration test requiring a duplicate preflight query and a unique filtered index on `mydo_job(tray_code)`.
- [ ] Implement `countMydoJobByTrayCode(String trayCode)` and call it at all application job-insert entry points touched by this flow.
- [ ] Create the migration with this invariant:
```sql
IF EXISTS (
SELECT tray_code FROM dbo.mydo_job
WHERE tray_code IS NOT NULL
GROUP BY tray_code HAVING COUNT_BIG(*) > 1
)
THROW 51000, N'mydo_job存在重复托盘条码,请清理后重试', 1;
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.mydo_job') AND name = N'UX_mydo_job_tray_code')
CREATE UNIQUE NONCLUSTERED INDEX UX_mydo_job_tray_code
ON dbo.mydo_job(tray_code) WHERE tray_code IS NOT NULL;
```
- [ ] Run focused mapper/service/migration tests and verify pass.
### Task 3: Route push-mode jobs by warehouse WCS address
- [ ] Add `WcsClientTest` cases with distinct `01` and `02` base URLs and assert each job posts to its own URL.
- [ ] Extend configuration with `warehouseBaseUrls` and deterministic fallback to the existing WCS `baseUrl` when a warehouse has no override.
- [ ] Resolve job URLs as:
```java
String url = config().getApiUrl(apiKey, job.getWarehouseId());
return post(url, body, interfaceName, this::parseWcsAck, MsgTypeEnum.JSON);
```
- [ ] Configure `01`, `02`, and `03` with independent environment-variable overrides in `application.yml`, without committing credentials.
- [ ] Run `mvn -pl ruoyi-admin -am -Dtest=WcsClientTest -Dsurefire.failIfNoSpecifiedTests=false test` and verify pass.
### Task 4: Add the dense WCS job-pull endpoint
- [ ] Add controller/service tests for missing tray, entrance, unsupported height, unknown tray, non-03 warehouse, non-inbound job, successful claim, and idempotent same-value retry.
- [ ] Define the request fields and aliases:
```java
private String trayCode;
private String entranceId;
private BigDecimal height;
```
- [ ] Add mapper CAS SQL that updates only warehouse 03 inbound jobs in pending state:
```sql
UPDATE mydo_job
SET entrance_id = #{entranceId}, height = #{height}, status = '3',
execution_phase = '1', start_time = COALESCE(start_time, CONVERT(varchar(19), GETDATE(), 120))
WHERE job_id = #{jobId} AND warehouse_id = '03' AND in_or_out < 20
AND status = '2' AND execution_phase = '0';
```
- [ ] Expose `POST /api/external/wcs/inbound/job`, invoke the transactional claim service, reload the persisted row, and return it in the established WCS `code/message/data` response.
- [ ] Run controller and service tests and verify pass.
### Task 5: Full verification and completion audit
- [ ] Run `mvn -pl ruoyi-admin -am test`; expect `BUILD SUCCESS`.
- [ ] Run the PDA build command available in `pda/package.json`; if no build script exists, run a static import/reference check proving the removed station API is no longer referenced by the bind page.
- [ ] Run `cd ruoyi-ui && npm run build:prod` if shared warehouse/config UI files were changed.
- [ ] Inspect the final diff and map each of the five numbered requirements to direct code plus test/migration evidence; do not mark complete if any item lacks evidence.
### Task 6: Correct point selection scope to warehouse 03 only
**Files:**
- Modify: `pda/pages/warehouse/inbound/bindTray.vue`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/pda/service/impl/PdaServiceImplTest.java`
- [ ] **Step 1: Add differentiated service tests**
Assert warehouse `01`/`02` binding without `stationPointId` fails with `请选择入库点位`, while warehouse `03` succeeds without a station and never calls station-point services.
- [ ] **Step 2: Restore conditional PDA selection**
Use the exact dense-warehouse predicate and conditional submit rule:
```javascript
isDenseWarehouse() {
return this.warehouseId === '03'
}
if (!this.isDenseWarehouse && !this.stationPointId) {
this.$modal.msg('请选择入库点位')
return
}
```
Show/load/reset the station picker only when `warehouseId !== '03'`; submit `stationPointId` for ordinary warehouses and omit it for `03`.
- [ ] **Step 3: Restore backend enforcement**
In `bindTrayAndAddInJob`, reject a missing point for non-03 warehouses, pass the selected point into `AddInJobRequest`, and explicitly clear it for warehouse 03:
```java
if (!DENSE_WAREHOUSE_ID.equals(request.getWarehouseId())
&& !StringUtils.hasText(request.getStationPointId())) {
throw new RuntimeException("请选择入库点位");
}
addInJobRequest.setStationPointId(DENSE_WAREHOUSE_ID.equals(request.getWarehouseId())
? null : request.getStationPointId());
```
- [ ] **Step 4: Verify corrected behavior**
Run `mvn -pl ruoyi-admin -am -Dtest=PdaServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`, compile the PDA SFC template/script, and run the full Maven test suite. Expected: warehouse 01/02 require points, warehouse 03 does not, and all tests pass.

View File

@@ -0,0 +1,42 @@
# Inbound/Outbound Lift Conflict Exclusion Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Prevent warehouse-03 inbound layer/location allocation from using a floor currently being retrieved by an executing outbound job targeting the same station as the inbound entrance.
**Architecture:** Resolve blocked floors directly in SQL by joining the current inbound job (`v_job_id`/`jobId`) to executing outbound jobs where `outbound.exit_id = inbound.entrance_id`, then join the outbound source location to obtain `the_floor`. Apply the same exclusion predicate to first-stage layer planning, second-stage atomic location allocation, and occupied-location reallocation so no path can bypass the lift-conflict rule.
**Tech Stack:** MyBatis XML, SQL Server CTEs, JUnit 5/AssertJ, Maven, rollback-only database scenarios.
---
### Task 1: Prove the conflict-floor contract
**Files:**
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/pda/mapper/PdaMapperSqlTest.java`
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/bas/cargospace/mapper/BasCargoSpaceMapperSqlTest.java`
- [ ] Write failing SQL tests requiring `status = '3'`, outbound `in_or_out >= 20`, equal outbound `exit_id` and current inbound `entrance_id`, source cargo-space join, and `the_floor` exclusion in planner, final lock, and reallocation SQL.
- [ ] Run the two mapper test classes and confirm failure because no conflict-floor CTE exists.
### Task 2: Exclude lift-conflict floors everywhere
**Files:**
- Modify: `ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml`
- Modify: `ruoyi-admin/src/main/resources/mapper/bas/BasCargoSpaceMapper.xml`
- [ ] Add `lift_conflict_floors` before `available_slots` in both warehouse-03 allocation CTEs.
- [ ] Filter candidate slots with `NOT EXISTS` against the candidate floor.
- [ ] Add the equivalent CTE and predicate to warehouse-03 occupied-location reallocation.
- [ ] Run focused tests and confirm pass.
### Task 3: Database and regression verification
**Files:**
- Verify all files above.
- [ ] In a rollback transaction, create an inbound job and an executing outbound job with the same port; prove the outbound source floor is unavailable while another layer remains selectable.
- [ ] Change the outbound status away from `3` and prove the floor becomes available again.
- [ ] Change the outbound exit to another port and prove the floor becomes available again.
- [ ] Run `mvn test` and require all tests to pass.
- [ ] Audit that first-stage planning, second-stage locking, and exception reallocation all enforce the rule before marking complete.

View File

@@ -0,0 +1,218 @@
# Project Number Traceability Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a standalone `project_no` field and preserve it from inbound document creation through inbound details, job details, and inventory master/detail records.
**Architecture:** The inbound master form is the single input point. The backend copies the master value to every inbound detail, SQL Server procedures carry the detail value into job details and inventory rows, and Java/MyBatis models expose the value consistently as `projectNo`. Archive tables receive the same column because current completion procedures archive with `INSERT ... SELECT *`.
**Tech Stack:** Vue 2 + Element UI, Spring Boot/Java 17, MyBatis XML, SQL Server, JUnit 5/Mockito/AssertJ
---
### Task 1: Lock down the project-number propagation contract
**Files:**
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/in/stockin/service/impl/InStockinServiceImplTest.java`
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/in/job/mapper/MydoJobMapperSqlTest.java`
- Create: `ruoyi-admin/src/test/java/com/ruoyi/in/stockin/mapper/ProjectNoMapperSqlTest.java`
- [ ] **Step 1: Add a service test that verifies the master project number is copied to each inbound detail**
```java
saveDTO.setProjectNo("PRJ-001");
service.saveInStockinWithDetails(saveDTO);
assertThat(detailCaptor.getValue().getProjectNo()).isEqualTo("PRJ-001");
```
- [ ] **Step 2: Add mapper/procedure text tests for all required columns and transfers**
```java
assertThat(inStockinMapperXml).contains("project_no", "#{projectNo}");
assertThat(jobDetailMapperXml).contains("project_no", "#{projectNo}");
assertThat(stockMapperXml).contains("project_no", "#{projectNo}");
assertThat(projectMigrationSql)
.contains("in_stockin", "in_stockin_detail", "mydo_job_detail")
.contains("bas_stock", "bas_stock_detail");
```
- [ ] **Step 3: Run focused tests and verify they fail before implementation**
Run: `mvn -pl ruoyi-admin -Dtest=InStockinServiceImplTest,ProjectNoMapperSqlTest,MydoJobMapperSqlTest test`
Expected: FAIL because `projectNo` properties, SQL mappings, and migration do not exist yet.
### Task 2: Add the database migration and procedure propagation
**Files:**
- Create: `sql/20260714_project_no_traceability.sql`
- Modify: `sql/proc_add_in_job_trace.sql`
- Modify: `sql/bas_stock_master_detail.sql`
- [ ] **Step 1: Add nullable project-number columns idempotently**
```sql
IF COL_LENGTH(N'dbo.in_stockin', N'project_no') IS NULL
ALTER TABLE dbo.in_stockin ADD project_no nvarchar(80) NULL;
```
Repeat for `in_stockin_detail`, `in_stockin_finish`, `in_stockin_detail_finish`, `mydo_job_detail`, `mydo_job_detail_finish`, `bas_stock`, and `bas_stock_detail`.
- [ ] **Step 2: Backfill existing traceable rows**
```sql
UPDATE d
SET d.project_no = h.project_no
FROM dbo.in_stockin_detail d
JOIN dbo.in_stockin h ON h.stockin_id = d.stockin_id
WHERE d.project_no IS NULL AND h.project_no IS NOT NULL;
```
Backfill job details from inbound details, inventory details from job/inbound details, and inventory masters from their inventory details.
- [ ] **Step 3: Update `proc_add_in_job` to copy `in_stockin_detail.project_no` into `mydo_job_detail.project_no`**
```sql
SELECT ..., d.project_no
FROM dbo.in_tray_msg tm
LEFT JOIN dbo.in_stockin_detail d ON tm.stockin_detail_id = d.stockin_detail_id;
INSERT INTO dbo.mydo_job_detail (..., project_no)
VALUES (..., @project_no);
```
- [ ] **Step 4: Update `proc_complete_in_job` to copy the job detail project number into inventory master and detail rows**
```sql
INSERT INTO bas_stock (..., project_no)
VALUES (..., @project_no);
INSERT INTO bas_stock_detail (..., project_no)
VALUES (..., @project_no);
```
- [ ] **Step 5: Keep baseline procedure scripts consistent with the deployable migration**
Update the existing standalone procedure scripts with the same cursor variables, selected columns, and insert columns.
### Task 3: Expose `projectNo` in backend entities and MyBatis mappings
**Files:**
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/in/stockin/domain/InStockin.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/in/sotckindetail/domain/InStockinDetail.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/in/jobdetail/domain/MydoJobDetail.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/bas/stock/domain/BasStock.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/bas/stock/domain/BasStockDetail.java`
- Modify: `ruoyi-admin/src/main/resources/mapper/in/InStockinMapper.xml`
- Modify: `ruoyi-admin/src/main/resources/mapper/in/InStockinDetailMapper.xml`
- Modify: `ruoyi-admin/src/main/resources/mapper/in/MydoJobDetailMapper.xml`
- Modify: `ruoyi-admin/src/main/resources/mapper/bas/BasStockMapper.xml`
- Modify: `ruoyi-admin/src/main/resources/mapper/bas/BasStockDetailMapper.xml`
- [ ] **Step 1: Add the Java property to all five domain models**
```java
/** 项目号 */
@Excel(name = "项目号")
private String projectNo;
public String getProjectNo() { return projectNo; }
public void setProjectNo(String projectNo) { this.projectNo = projectNo; }
```
- [ ] **Step 2: Add `project_no` to every result map, select list, query filter, insert, and update section**
```xml
<result property="projectNo" column="project_no" />
<if test="projectNo != null and projectNo != ''">project_no,</if>
<if test="projectNo != null and projectNo != ''">#{projectNo},</if>
```
- [ ] **Step 3: Run mapper contract tests**
Run: `mvn -pl ruoyi-admin -Dtest=ProjectNoMapperSqlTest test`
Expected: PASS.
### Task 4: Copy the master project number to inbound details and support the create form
**Files:**
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/in/stockin/service/impl/InStockinServiceImpl.java`
- Modify: `ruoyi-ui/src/views/in/add/index.vue`
- [ ] **Step 1: Enforce server-side master-to-detail copying**
```java
detail.setStockinId(saveDTO.getStockinId());
detail.setProjectNo(saveDTO.getProjectNo());
```
- [ ] **Step 2: Add the project-number input to the main inbound form**
```vue
<el-form-item label="项目号" prop="projectNo">
<el-input v-model="mainForm.projectNo" placeholder="请输入项目号" clearable />
</el-form-item>
```
- [ ] **Step 3: Include `projectNo` in the form model and detail payload**
```javascript
mainForm: { projectNo: null }
// detail DTO
projectNo: this.mainForm.projectNo
```
- [ ] **Step 4: Run the inbound service test**
Run: `mvn -pl ruoyi-admin -Dtest=InStockinServiceImplTest test`
Expected: PASS and the captured detail contains the master `projectNo`.
### Task 5: Preserve project number when jobs are created from existing inventory
**Files:**
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/out/location/service/impl/LocationOutboundServiceImpl.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/pd/stockpd/service/impl/PdStockpdServiceImpl.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java`
- [ ] **Step 1: Copy inventory-detail project numbers into outbound and inventory job details**
```java
jobDetail.setProjectNo(stockDetail.getProjectNo());
```
- [ ] **Step 2: Include `projectNo` in inventory material map queries used for return jobs and copy it into return-job details**
```java
returnDetail.setProjectNo(mapString(material, "projectNo"));
```
- [ ] **Step 3: Run focused service tests**
Run: `mvn -pl ruoyi-admin -Dtest=LocationOutboundServiceImplTest,PdStockpdServiceImplTest,PdaServiceImplTest test`
Expected: PASS.
### Task 6: Verify the end-to-end change
**Files:**
- Verify: all files listed above
- [ ] **Step 1: Run all backend tests**
Run: `mvn -pl ruoyi-admin test`
Expected: BUILD SUCCESS.
- [ ] **Step 2: Build the frontend production bundle**
Run: `cd ruoyi-ui && npm run build:prod`
Expected: Build completes successfully.
- [ ] **Step 3: Inspect the final diff for unrelated or overwritten work**
Run: `git diff --check && git status --short`
Expected: no whitespace errors; existing user changes remain intact.

View File

@@ -0,0 +1,107 @@
# Warehouse 03 Cargo Spaces Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Create the five-floor warehouse 03 dense-storage layout shown in the supplied diagram, with stable status values and lane-level `group_coding`.
**Architecture:** Add one safe, one-time SQL migration that seeds warehouse/roadway metadata, derives every valid coordinate from compact layout ranges, classifies equipment and mother-channel nodes, and inserts five floors inside a transaction. The migration refuses to overwrite existing warehouse 03 data and aborts unless all expected totals, status counts, coordinate uniqueness rules, and lane grouping rules pass.
**Tech Stack:** SQL Server T-SQL, `dbo.bas_warehouse`, `dbo.bas_roadway`, `dbo.bas_cargo_space`, RuoYi dictionary tables.
---
### Task 1: Add the warehouse 03 layout migration
**Files:**
- Create: `sql/20260714_warehouse_03_cargo_spaces.sql`
- [x] **Step 1: Encode the valid diagram footprint**
Create an in-memory layout from these inclusive column ranges. `the_platoon` is the diagram's horizontal 排 coordinate and `the_column` is its vertical 列 coordinate.
```sql
insert into @footprint(the_column, platoon_from, platoon_to)
values
(1,1,3),(2,1,3),(3,1,3),
(4,1,5),(4,9,13),
(5,1,7),(5,9,13),
(6,1,13),
(7,1,13),(8,1,13),(9,1,13),(10,1,13),(11,1,13),
(12,1,13),(13,1,13),(14,1,13),(15,1,13),(16,1,13),
(17,1,13),(18,1,13),(19,1,13),(20,1,13),(21,1,13),
(22,1,5),(22,9,13),(23,9,13),(24,9,13);
```
- [x] **Step 2: Classify all five floors**
Use the eight-digit ID `03 + 排(2) + 列(2) + 层(2)`. Apply statuses in this precedence order:
```sql
case
when the_platoon = 7 and the_column in (5,21) then '7'
when the_platoon = 7 and the_column in (6,20) then '8'
when the_column in (7,13,19)
or (the_platoon = 7 and the_column between 7 and 19) then '9'
else '1'
end
```
Set `this_cargo_area` to zones 1-8 for ordinary storage coordinates. Leave equipment and mother-channel nodes unzoned.
- [x] **Step 3: Assign stable lane groups**
For ordinary positions only, define a lane as the same warehouse, 排, floor, and one of the four storage bands `01-06`, `08-12`, `14-18`, or `20-25`. Use that lane's lowest existing cargo-space ID as `group_coding`; special nodes keep `group_coding = NULL`.
```sql
min(cargo_space_id) over (
partition by the_platoon, the_floor, channel_band
)
```
- [x] **Step 4: Seed metadata and insert atomically**
Insert warehouse `03` as `密集库`, roadway `1` as `密集库巷道`, and dictionary values 7/8/9 when missing. Refuse to overwrite existing warehouse 03 cargo-space rows; insert all generated rows in one transaction.
### Task 2: Verify the generated data
**Files:**
- Verify: `sql/20260714_warehouse_03_cargo_spaces.sql`
- [x] **Step 1: Run the migration against FTC25009-WMS**
Run the SQL through the configured project SQL Server connection. Expected result: one committed transaction and `1295` warehouse 03 rows.
- [x] **Step 2: Verify totals and status distribution**
```sql
select status, count(*)
from dbo.bas_cargo_space
where warehouse_id = '03'
group by status;
```
Expected: status 1 = 1030, status 7 = 10, status 8 = 10, status 9 = 245.
- [x] **Step 3: Verify IDs and lane grouping**
```sql
select count(*) as invalid_ids
from dbo.bas_cargo_space
where warehouse_id = '03'
and cargo_space_id not like '03[0-9][0-9][0-9][0-9][0-9][0-9]';
select count(distinct group_coding) as lane_groups
from dbo.bas_cargo_space
where warehouse_id = '03' and status = '1';
```
Expected: `invalid_ids = 0` and `lane_groups = 240` (48 physical lanes on each of five floors).
- [ ] **Step 4: Commit**
```bash
git add docs/superpowers/plans/2026-07-14-warehouse-03-cargo-spaces.md sql/20260714_warehouse_03_cargo_spaces.sql
git commit -m "feat: add warehouse 03 cargo spaces"
```
Only perform this step when the user explicitly requests a Git commit.

View File

@@ -0,0 +1,169 @@
# WCS Two-Stage Layer Allocation Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make warehouse 03 WCS inbound allocation select a viable layer on the first request without claiming a location, then atomically allocate a compatible location on that selected layer on the second request.
**Architecture:** Extend the WCS request with ordered arrays for available layers and shuttle-current layers plus an optional explicit stage. The first stage serializes planning, evaluates the same inventory/job/lane predicates used by final allocation, reserves only capacity by atomically writing the selected layer into `mydo_job.roadway_id`, and returns without changing cargo-space state or job cargo-space fields. The second stage interprets warehouse-03 `roadway_id` as the selected floor, re-evaluates and atomically claims a location, while other warehouses retain their one-stage behavior.
**Tech Stack:** Java 17, Spring Boot, Jackson, MyBatis XML, SQL Server CTEs/application locks, JUnit 5, Mockito, AssertJ, Maven.
---
### Task 1: Define and validate the WCS layer-array contract
**Files:**
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/api/domain/WcsInboundLocationRequest.java`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/api/domain/WcsInboundLocationRequestTest.java`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/api/controller/ExternalApiControllerWcsCodeTest.java`
- [ ] **Step 1: Write failing request-contract tests**
Assert typed properties `List<Integer> availableLayers`, `List<Integer> currentLayers`, and optional `Integer allocationStage`; cover JSON aliases for floor/layer naming. Add controller rejection tests for missing arrays, layers outside 1-5, empty available layers, and stage outside 1-2.
- [ ] **Step 2: Run tests and verify failure**
Run: `mvn -pl ruoyi-admin -am -Dtest=WcsInboundLocationRequestTest,ExternalApiControllerWcsCodeTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: FAIL because the array and stage properties do not exist.
- [ ] **Step 3: Add the request fields and normalization**
Use `availableLayers` as WCS order, place current layers that are also available first, remove duplicates while preserving order, then append remaining available layers. Require both array properties for warehouse 03; allow an empty current-layer array so the allocator can fall back to all available layers. Explicit stage 1 means layer planning and stage 2 means location allocation; when omitted, infer stage 1 from blank job `roadway_id` and stage 2 from a selected layer for backward compatibility.
- [ ] **Step 4: Run focused tests and verify pass**
Run the command from Step 2.
Expected: PASS.
### Task 2: Add first-stage layer planning without cargo-space mutation
**Files:**
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/service/PdaService.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/mapper/PdaMapper.java`
- Modify: `ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/pda/service/impl/PdaServiceImplTest.java`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/pda/mapper/PdaMapperSqlTest.java`
- [ ] **Step 1: Write failing service and SQL tests**
Assert current-layer ordering, fallback to other available layers, a transaction-owned warehouse planning lock, and an atomic update that changes only `mydo_job.roadway_id` when cargo-space fields are blank. Assert first-stage candidate SQL reads positive inventory, assigned active jobs, status-3 reservations, unknown occupied locations, lane compatibility, height, configured direction, and pending layer reservations.
- [ ] **Step 2: Run tests and verify failure**
Run: `mvn -pl ruoyi-admin -am -Dtest=PdaServiceImplTest,PdaMapperSqlTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: FAIL because layer planning contracts are absent.
- [ ] **Step 3: Implement transactional planning**
Add `planInboundLayer(trayCode, warehouseId, jobId, orderedLayers, height)`. Acquire the existing tray lock, then a warehouse-03 layer-planning application lock. Resolve the existing four-mode storage profile, query each preferred layer using the shared dense-lane candidate CTE, subtract other active jobs whose selected layer is persisted but cargo-space ID is still blank, then atomically write only the selected layer to `roadway_id`. Return `roadway_id`/`selected_layer`; never update `bas_cargo_space`, job height, cargo-space ID, or cargo-space name.
- [ ] **Step 4: Run focused tests and verify pass**
Run the command from Step 2.
Expected: PASS.
### Task 3: Route the controller through two explicit stages
**Files:**
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/api/controller/ExternalApiController.java`
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/api/controller/ExternalApiControllerWcsCodeTest.java`
- [ ] **Step 1: Write failing controller flow tests**
For stage 1, assert the controller calls planning, returns the selected layer, never calls `lockInboundCargoSpace`, never writes job height/cargo-space fields, and stage-1 retries return the same selected layer without locking. For stage 2, assert a selected layer is required, the layer must still be in `availableLayers`, only then call the atomic location lock and cargo-space CAS update.
- [ ] **Step 2: Run the controller tests and verify failure**
Run: `mvn -pl ruoyi-admin -am -Dtest=ExternalApiControllerWcsCodeTest -Dsurefire.failIfNoSpecifiedTests=false test`
Expected: FAIL because the controller currently performs one-stage locking.
- [ ] **Step 3: Implement stage routing**
Keep current job lookup and status checks. Apply the two-stage flow only to warehouse 03. Preserve the original one-stage path for other warehouses. For an already assigned location, retain the existing idempotent response and height check.
- [ ] **Step 4: Run the controller tests and verify pass**
Run the command from Step 2.
Expected: PASS.
### Task 4: Interpret warehouse-03 roadway as floor during final allocation
**Files:**
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java`
- Modify: `ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml`
- Modify: `ruoyi-admin/src/main/resources/mapper/bas/BasCargoSpaceMapper.xml`
- Modify: `ruoyi-admin/src/main/resources/mapper/in/MydoJobMapper.xml`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/pda/mapper/PdaMapperSqlTest.java`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/bas/cargospace/mapper/BasCargoSpaceMapperSqlTest.java`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/in/job/mapper/MydoJobMapperSqlTest.java`
- [ ] **Step 1: Write failing floor-semantics tests**
Assert warehouse-03 candidate SQL uses `the_floor = TRY_CONVERT(... roadway_id)` rather than physical `bcs.roadway_id`; locked-location reuse compares the requested selected layer to `the_floor`; final job CAS preserves the selected layer; occupied-location reallocation applies the same floor predicate.
- [ ] **Step 2: Run focused tests and verify failure**
Run the three mapper test classes.
Expected: FAIL because all current SQL treats roadway as the physical aisle.
- [ ] **Step 3: Apply floor semantics and shared capacity checks**
Make the read-only planner and atomic updater share the same dense-lane CTE and pending-reservation guard. Return `the_floor` from locked-location lookup. For warehouse 03 normalize the returned `roadway_id` to the selected layer so the existing job CAS keeps that layer. Update reallocation to filter by selected floor; retain physical-roadway behavior in non-03 branches.
- [ ] **Step 4: Run mapper tests and verify pass**
Run the command from Step 2.
Expected: PASS.
### Task 5: Initialize and migrate active warehouse-03 jobs to two-stage semantics
**Files:**
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java`
- Create: `sql/20260714_warehouse_03_two_stage_layer_allocation.sql`
- Test: `ruoyi-admin/src/test/java/com/ruoyi/pda/mapper/Warehouse03TwoStageMigrationSqlTest.java`
- [ ] **Step 1: Write failing initialization and migration tests**
Assert newly created normal and empty-tray warehouse-03 jobs have blank `roadway_id`, while other warehouses keep the station roadway. Assert the migration clears only active warehouse-03 inbound jobs with blank cargo-space IDs and leaves assigned/history/other-warehouse jobs unchanged.
- [ ] **Step 2: Run focused tests and verify failure**
Run the PDA service and migration tests.
Expected: FAIL because job creation currently persists station roadway and no transition migration exists.
- [ ] **Step 3: Implement initialization and idempotent migration**
Write blank roadway for new warehouse-03 jobs. In an XACT_ABORT/TRY-CATCH transaction, clear stale physical roadway values only for active warehouse-03 inbound jobs without a cargo-space assignment, and add absent candidate-query indexes if needed.
- [ ] **Step 4: Apply migration and verify affected rows**
Execute the migration against the configured database, then query active warehouse-03 jobs to prove unassigned jobs have blank roadway and assigned jobs are unchanged.
### Task 6: Full verification and completion audit
**Files:**
- Verify all files above.
- [ ] **Step 1: Run all backend tests**
Run: `mvn test`
Expected: BUILD SUCCESS with every test passing.
- [ ] **Step 2: Run rollback-only database scenarios**
Prove current-layer-first selection, fallback when the current layer has no compatible candidate, no cargo-space status change after stage 1, only-roadway job mutation after stage 1, stage-2 allocation on the persisted floor, pending-reservation capacity protection, and stock/assigned/reserved job compatibility. Roll back all fixtures.
- [ ] **Step 3: Audit every explicit requirement**
Map array input, current-layer priority, two request stages, first-stage no-location mutation, second-stage atomic allocation, inventory/job occupancy, and no layer overbooking to direct test or database evidence before marking complete.

View File

@@ -0,0 +1,617 @@
# WCS、库存追溯与出库状态约束 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 完成 WCS 请求作业响应阶段、03 仓入口就近货位、盘点库存追溯、库存明细界面和出库双状态约束六项需求。
**Architecture:** 保留领取后的数据库执行阶段与现有 SQL 原子锁,仅在专用控制器响应中生成阶段为 0 的副本。入口号沿控制器、PDA 服务和 Mapper 传递并参与软排序;盘点追溯沿原库存到回库作业明细再到新库存传递;出库候选列表与存储过程同时执行双状态约束。
**Tech Stack:** Java 17、Spring Boot、MyBatis、SQL Server、JUnit 5、Mockito、AssertJ、Vue 2、Element UI、Node.js 静态验证脚本。
## Global Constraints
- 只有 `/api/external/wcs/inbound/job` 返回的 `MydoJob.executionPhase` 为 `"0"`;其他 WCS 接口正常返回实际阶段。
- 数据库领取后的 `mydo_job.execution_phase` 继续为 `"1"`,不得破坏重复领取保护。
- 入口区域是软优先,目标区无可用货位时必须回退其他区域。
- `03-AU-01`、`03-AU-03` 优先 G、H;`03-AU-02`、`03-AU-04` 优先 A、B。
- 不能覆盖或丢失当前工作区已有未提交改动。
- 目标文件多数已有前置改动,实施任务不自动暂存或提交这些文件;每个任务以测试和差异审查作为检查点。
- SQL 迁移必须幂等;无法可靠追溯的历史数据不得猜测回填。
---
### Task 1: 仅请求作业接口返回执行阶段 0
**Files:**
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/api/controller/ExternalApiControllerWcsCodeTest.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/api/controller/ExternalApiController.java`
**Interfaces:**
- Consumes: `IMydoJobService.claimDenseInboundJob(String, String, BigDecimal)` 返回数据库真实 `MydoJob`。
- Produces: `ExternalApiController.claimDenseInboundJob` 返回独立副本,副本阶段为 `"0"`。
- [ ] **Step 1: 写失败的控制器测试**
在 `ExternalApiControllerWcsCodeTest` 增加:
```java
@Test
void claimDenseInboundJobReturnsPhaseZeroWithoutChangingStoredPhase() {
WcsDenseInboundJobRequest request = new WcsDenseInboundJobRequest();
request.setTrayCode("TP-PULL-01");
request.setEntranceId("03-AU-01");
request.setHeight(BigDecimal.ONE);
MydoJob claimed = denseInboundJob(null);
claimed.setExecutionPhase("1");
when(mydoJobService.claimDenseInboundJob("TP-PULL-01", "03-AU-01", BigDecimal.ONE))
.thenReturn(claimed);
AjaxResult result = controller.claimDenseInboundJob(request);
assertWcsSuccess(result);
MydoJob responseJob = (MydoJob) result.get(AjaxResult.DATA_TAG);
assertThat(responseJob).isNotSameAs(claimed);
assertThat(responseJob.getExecutionPhase()).isEqualTo("0");
assertThat(claimed.getExecutionPhase()).isEqualTo("1");
}
```
在既有货位请求成功测试中补充 `assertThat(((MydoJob) result.get(AjaxResult.DATA_TAG)).getExecutionPhase()).isEqualTo("1")`,证明其他接口不被归零。
- [ ] **Step 2: 运行测试并确认按预期失败**
Run:
```powershell
mvn -pl ruoyi-admin -am "-Dtest=ExternalApiControllerWcsCodeTest" -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: 新测试因响应对象仍是原对象且阶段为 `1` 而失败。
- [ ] **Step 3: 编写最小实现**
在控制器导入 `org.springframework.beans.BeanUtils`,并把主动请求作业成功分支改为:
```java
MydoJob claimed = mydoJobService.claimDenseInboundJob(
request.getTrayCode(), request.getEntranceId(), request.getHeight());
return wcsClaimSuccess(claimed);
```
新增专用帮助方法,不改通用 `wcsSuccess`:
```java
private AjaxResult wcsClaimSuccess(MydoJob claimed) {
if (claimed == null) {
return wcsError("未找到可领取入库作业");
}
MydoJob response = new MydoJob();
BeanUtils.copyProperties(claimed, response);
response.setExecutionPhase("0");
return wcsSuccess(response);
}
```
- [ ] **Step 4: 运行测试并确认通过**
运行 Step 2 命令。Expected: `ExternalApiControllerWcsCodeTest` 全部通过。
- [ ] **Step 5: 差异检查点**
Run: `git diff --check -- ruoyi-admin/src/main/java/com/ruoyi/api/controller/ExternalApiController.java ruoyi-admin/src/test/java/com/ruoyi/api/controller/ExternalApiControllerWcsCodeTest.java`
Expected: 无空白错误;不暂存当前文件,避免把既有改动带入提交。
---
### Task 2: 03 仓按入口软优先分配 A/B 或 G/H 区
**Files:**
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/pda/service/impl/PdaServiceImplTest.java`
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/pda/mapper/PdaMapperSqlTest.java`
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/bas/cargospace/mapper/BasCargoSpaceMapperSqlTest.java`
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/api/controller/ExternalApiControllerWcsCodeTest.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/service/PdaService.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/api/controller/ExternalApiController.java`
- Modify: `ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml`
- Modify: `ruoyi-admin/src/main/resources/mapper/bas/BasCargoSpaceMapper.xml`
**Interfaces:**
- Produces: `planInboundLayer(..., String entranceId, List<Integer> orderedLayers, BigDecimal height)`。
- Produces: `lockInboundCargoSpace(..., String roadwayId, String entranceId, BigDecimal height)`。
- Consumes: `MydoJob.entranceId` 由 WCS 请求作业时保存。
- [ ] **Step 1: 写入口参数和 SQL 排序失败测试**
在 `PdaServiceImplTest` 的选层与锁位测试中捕获传给 `PdaMapper` 的 Map,并增加:
```java
assertThat(captor.getValue()).containsEntry("v_entrance_id", "03-AU-01");
```
调用签名使用:
```java
service.planInboundLayer("TP01", "03", "RK01", "03-AU-01", List.of(1, 2), BigDecimal.ONE);
service.lockInboundCargoSpace("TP01", "03", "1", "03-AU-01", BigDecimal.ONE);
```
在 `PdaMapperSqlTest` 对 `selectAvailableInboundLayerCandidate` 和 `lockInboundCargoSpace` 的 03 仓分支分别断言包含:
```java
assertThat(sql)
.contains("#{v_entrance_id}")
.contains("03-AU-01")
.contains("03-AU-03")
.contains("03-AU-02")
.contains("03-AU-04")
.contains("this_cargo_area in ('G', 'H')")
.contains("this_cargo_area in ('A', 'B')")
.contains("entrance_area_priority")
.contains("order by height_priority asc, entrance_area_priority asc");
```
在 `BasCargoSpaceMapperSqlTest` 对异常重分配查询增加相同断言,但参数名为 `#{entranceId}`。
- [ ] **Step 2: 运行测试并确认失败**
Run:
```powershell
mvn -pl ruoyi-admin -am "-Dtest=PdaServiceImplTest,PdaMapperSqlTest,BasCargoSpaceMapperSqlTest,ExternalApiControllerWcsCodeTest" -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: 编译或断言失败,因为接口没有入口参数且 SQL 没有入口区域优先级。
- [ ] **Step 3: 沿调用链传入口号**
将 `PdaService` 和实现签名改为:
```java
Map<String, Object> planInboundLayer(String trayCode, String warehouseId, String jobId,
String entranceId, List<Integer> orderedLayers, BigDecimal height);
Map<String, Object> lockInboundCargoSpace(String trayCode, String warehouseId, String roadwayId,
String entranceId, BigDecimal height);
```
在 `PdaServiceImpl` 两个参数 Map 中写入:
```java
params.put("v_entrance_id", entranceId);
lockMap.put("v_entrance_id", entranceId);
```
控制器调用传 `job.getEntranceId()`;同步更新所有测试和既有调用点。
- [ ] **Step 4: 在三条 SQL 路径加入软优先排序**
在候选 CTE 中加入:
```sql
case
when upper(ltrim(rtrim(isnull(#{v_entrance_id}, '')))) in ('03-AU-01', '03-AU-03')
then case when bcs.this_cargo_area in ('G', 'H') then 0 else 1 end
when upper(ltrim(rtrim(isnull(#{v_entrance_id}, '')))) in ('03-AU-02', '03-AU-04')
then case when bcs.this_cargo_area in ('A', 'B') then 0 else 1 end
else 0
end as entrance_area_priority
```
在 `PdaMapper.xml` 的选层查询和锁位更新选择中,把排序开头统一为:
```sql
order by height_priority asc,
entrance_area_priority asc,
```
`BasCargoSpaceMapper.xml` 使用同一表达式,将参数改为 `#{entranceId}`,并把异常重分配排序开头改成相同顺序。
- [ ] **Step 5: 运行测试并确认通过**
运行 Step 2 命令。Expected: 四个测试类全部通过。
- [ ] **Step 6: 差异检查点**
运行 `git diff --check` 覆盖本任务文件;确认没有把区域条件写入 `WHERE`,因此仍允许跨区回退。
---
### Task 3: 盘点回库记录并回填入库主单与入库类型
**Files:**
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/pda/service/impl/PdaServiceImplTest.java`
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/pda/mapper/PdaMapperSqlTest.java`
- Create: `ruoyi-admin/src/test/java/com/ruoyi/pda/mapper/InventoryInboundTraceMigrationSqlTest.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java`
- Modify: `ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml`
- Modify: `sql/bas_stock_master_detail.sql`
- Modify: `sql/20260714_project_no_traceability.sql`
- Create: `sql/20260715_inventory_inbound_trace.sql`
**Interfaces:**
- Produces: `selectInventoryTrayMaterials` Map 的 `stockinId`、`inType`。
- Produces: 盘点回库 `MydoJobDetail.stockinId`。
- Produces: 新库存 `bas_stock.stockin_id`、`bas_stock.in_type`。
- [ ] **Step 1: 写盘点追溯失败测试**
`PdaMapperSqlTest` 断言盘点物料查询包含:
```java
assertThat(selectInventoryMaterials)
.contains("left join bas_stock bs on bs.stock_id = jd.stock_id")
.contains("bs.stockin_id as stockinId")
.contains("bs.in_type as inType");
```
在 `PdaServiceImplTest` 的盘点确认成功用例中,让物料 Map 包含 `stockinId=RKD001`、`inType=1`,捕获插入的回库 `MydoJobDetail` 并断言:
```java
assertThat(returnDetail.getStockinId()).isEqualTo("RKD001");
```
`InventoryInboundTraceMigrationSqlTest` 读取新迁移,断言包含 `in_stockin_finish`、`bas_stock_detail_finish`、`bas_stock_finish`、`stockin_id`、`in_type`、`in_or_out in (10, 11)`,并检查没有无条件覆盖非空追溯字段。
- [ ] **Step 2: 运行测试并确认失败**
Run:
```powershell
mvn -pl ruoyi-admin -am "-Dtest=PdaServiceImplTest,PdaMapperSqlTest,InventoryInboundTraceMigrationSqlTest" -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: 查询字段、回库明细赋值和迁移文件缺失导致失败。
- [ ] **Step 3: 在盘点确认阶段保存原追溯**
把查询改为:
```sql
select jd.job_detail_id as jobDetailId,
jd.stock_id as stockId,
jd.stockin_detail_id as stockDetailId,
jd.in_or_out_d_id as stockpdDetailId,
jd.product_id as materialCode,
jd.product_name as materialName,
jd.lot_no as lotNo,
jd.project_no as projectNo,
bs.stockin_id as stockinId,
bs.in_type as inType,
jd.stock_quantity as stockQuantity,
jd.out_quantity as quantity
from mydo_job_detail jd
left join bas_stock bs on bs.stock_id = jd.stock_id
where jd.job_id = #{jobId}
order by jd.job_detail_id
```
创建回库明细时增加:
```java
returnDetail.setStockinId(mapString(material, "stockinId"));
```
- [ ] **Step 4: 修正完成入库过程的类型恢复**
在两个基线脚本的 `proc_complete_in_job` 中,把只查当前主单的类型查询替换为:
```sql
SELECT TOP 1 @in_type = source.in_type
FROM (
SELECT 1 AS source_order, current_bill.in_type
FROM dbo.in_stockin current_bill
WHERE current_bill.stockin_id = @stockin_id
UNION ALL
SELECT 2 AS source_order, history_bill.in_type
FROM dbo.in_stockin_finish history_bill
WHERE history_bill.stockin_id = @stockin_id
UNION ALL
SELECT 3 AS source_order, archived_stock.in_type
FROM dbo.bas_stock_detail_finish archived_detail
INNER JOIN dbo.bas_stock_finish archived_stock
ON archived_stock.stock_id = archived_detail.stock_id
WHERE archived_detail.detail_id = @stockin_detail_id
) source
WHERE NULLIF(LTRIM(RTRIM(source.in_type)), '') IS NOT NULL
ORDER BY source.source_order;
```
`sql/20260715_inventory_inbound_trace.sql` 使用 `CREATE OR ALTER PROCEDURE` 部署同一过程,并在过程定义前执行历史回填。
- [ ] **Step 5: 编写只回填唯一来源的历史数据 SQL**
新迁移中的回填采用:
```sql
;WITH trace_candidates AS (
SELECT bs.stock_id AS new_stock_id,
archived_stock.stockin_id AS traced_stockin_id,
archived_stock.in_type AS archived_in_type,
current_bill.in_type AS current_in_type,
history_bill.in_type AS history_in_type
FROM dbo.bas_stock bs
INNER JOIN dbo.mydo_job_finish inbound_job
ON inbound_job.job_id = bs.job_id
AND inbound_job.in_or_out IN (10, 11)
INNER JOIN dbo.mydo_job_detail_finish inbound_detail
ON inbound_detail.job_id = inbound_job.job_id
INNER JOIN dbo.bas_stock_detail_finish archived_detail
ON archived_detail.detail_id = inbound_detail.stockin_detail_id
INNER JOIN dbo.bas_stock_finish archived_stock
ON archived_stock.stock_id = archived_detail.stock_id
LEFT JOIN dbo.in_stockin current_bill
ON current_bill.stockin_id = archived_stock.stockin_id
LEFT JOIN dbo.in_stockin_finish history_bill
ON history_bill.stockin_id = archived_stock.stockin_id
), unique_trace AS (
SELECT new_stock_id,
MAX(traced_stockin_id) AS traced_stockin_id,
MAX(COALESCE(current_in_type, history_in_type, archived_in_type)) AS traced_in_type
FROM trace_candidates
GROUP BY new_stock_id
HAVING COUNT(DISTINCT ISNULL(traced_stockin_id, '')) &lt;= 1
AND COUNT(DISTINCT ISNULL(COALESCE(current_in_type, history_in_type, archived_in_type), '')) &lt;= 1
)
UPDATE target
SET target.stockin_id = COALESCE(NULLIF(target.stockin_id, ''), unique_trace.traced_stockin_id),
target.in_type = COALESCE(NULLIF(target.in_type, ''), unique_trace.traced_in_type)
FROM dbo.bas_stock target
INNER JOIN unique_trace ON unique_trace.new_stock_id = target.stock_id
WHERE (target.stockin_id IS NULL OR target.stockin_id = ''
OR target.in_type IS NULL OR target.in_type = '');
```
- [ ] **Step 6: 运行测试并确认通过**
运行 Step 2 命令。Expected: 三个测试类通过。
- [ ] **Step 7: 差异检查点**
运行 `git diff --check`;人工核对迁移只填空值且歧义来源不回填。
---
### Task 4: 库存明细项目号、追溯字段只读和物料状态开关
**Files:**
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/bas/stock/service/impl/BasStockServiceImplTest.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/bas/stock/service/impl/BasStockServiceImpl.java`
- Modify: `ruoyi-admin/src/main/resources/mapper/bas/BasStockDetailMapper.xml`
- Modify: `ruoyi-ui/src/views/bas/stock/index.vue`
- Create: `scripts/verify-wcs-inventory-outbound-fixes.js`
**Interfaces:**
- Consumes: `BasStockDetail.projectNo` 已由现有 Mapper 查询提供。
- Produces: 追溯字段更新保持原值;开关仍提交字符串 `1`/`2`。
- [ ] **Step 1: 写后端和前端失败测试**
在 `BasStockServiceImplTest` 增加:
```java
@Test
void updateDetailPreservesTraceabilityIds() {
BasStockDetail stored = new BasStockDetail();
stored.setDetailId("KC01-1");
stored.setStockId("KC01");
stored.setJobDetailId("JOB-ORIGINAL");
stored.setStockinDetailId("IN-ORIGINAL");
BasStockDetail update = new BasStockDetail();
update.setDetailId("KC01-1");
update.setStockId("KC01");
update.setJobDetailId("JOB-TAMPERED");
update.setStockinDetailId("IN-TAMPERED");
when(basStockDetailMapper.selectBasStockDetailByDetailId("KC01-1")).thenReturn(stored);
when(basStockDetailMapper.updateBasStockDetail(any())).thenReturn(1);
service.updateBasStockDetail(update);
ArgumentCaptor<BasStockDetail> captor = ArgumentCaptor.forClass(BasStockDetail.class);
verify(basStockDetailMapper).updateBasStockDetail(captor.capture());
assertThat(captor.getValue().getJobDetailId()).isEqualTo("JOB-ORIGINAL");
assertThat(captor.getValue().getStockinDetailId()).isEqualTo("IN-ORIGINAL");
}
```
创建 Node 脚本,读取库存 Vue 文件并断言项目号列、两个 `readonly`、`el-switch`、`active-value="1"`、`inactive-value="2"`;读取 Mapper 更新段并断言不包含 `job_detail_id =` 与 `stockin_detail_id =`。
- [ ] **Step 2: 运行测试并确认失败**
Run:
```powershell
mvn -pl ruoyi-admin -am "-Dtest=BasStockServiceImplTest" -Dsurefire.failIfNoSpecifiedTests=false test
node scripts/verify-wcs-inventory-outbound-fixes.js
```
Expected: 服务仍接受篡改值,前端/Mapper 静态断言失败。
- [ ] **Step 3: 实现后端不可变保护**
在更新服务调用 Mapper 前增加:
```java
if (oldDetail != null) {
basStockDetail.setJobDetailId(oldDetail.getJobDetailId());
basStockDetail.setStockinDetailId(oldDetail.getStockinDetailId());
}
```
从 `updateBasStockDetail` SQL 删除:
```xml
<if test="jobDetailId != null">job_detail_id = #{jobDetailId},</if>
<if test="stockinDetailId != null">stockin_detail_id = #{stockinDetailId},</if>
```
- [ ] **Step 4: 实现库存明细界面**
子表增加:
```vue
<el-table-column label="项目号" align="center" prop="projectNo" width="140" show-overflow-tooltip />
```
状态表单改为:
```vue
<el-switch
v-model="detailForm.productStatus"
active-value="1"
inactive-value="2"
active-text="正常"
inactive-text="异常"
/>
```
两个追溯输入框增加 `readonly`;`resetDetail()` 将 `productStatus` 默认设为 `'1'`。
- [ ] **Step 5: 运行测试并确认通过**
运行 Step 2 两条命令。Expected: JUnit 和 Node 验证均通过。
- [ ] **Step 6: 差异检查点**
运行 `git diff --check`;确认查询筛选区的物料状态仍保持下拉框,只有新增/修改明细弹窗改为开关。
---
### Task 5: 出库候选与分配过程限制库存/物料双正常
**Files:**
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/out/stockout/mapper/OutStockoutMapperSqlTest.java`
- Modify: `scripts/verify-wcs-inventory-outbound-fixes.js`
- Modify: `ruoyi-ui/src/views/out/create/index.vue`
- Modify: `ruoyi-ui/src/views/out/stockout/index.vue`
- Modify: `sql/out_stockout_flow.sql`
- Create: `sql/20260715_outbound_normal_stock_filter.sql`
**Interfaces:**
- Consumes: `BasStockDetailMapper` 现有 `stockStatus`、`productStatus` 可选过滤参数。
- Produces: 自动和手动分配 SQL均只选择 `1/1` 状态库存。
- [ ] **Step 1: 写失败测试**
`OutStockoutMapperSqlTest` 读取 `sql/out_stockout_flow.sql`,分别截取 `proc_out_hand_allocation`、`proc_out_auto_allocation` 并断言:
```java
assertThat(handProcedure)
.contains("bs.stock_status = '1'")
.contains("bsd.product_status = '1'");
assertThat(autoProcedure)
.contains("bs.stock_status = '1'")
.contains("bsd.product_status = '1'");
```
Node 脚本断言两个 Vue 文件的 `listStockDetail` 调用都同时包含:
```javascript
stockStatus: '1',
productStatus: '1'
```
- [ ] **Step 2: 运行测试并确认失败**
Run:
```powershell
mvn -pl ruoyi-admin -am "-Dtest=OutStockoutMapperSqlTest" -Dsurefire.failIfNoSpecifiedTests=false test
node scripts/verify-wcs-inventory-outbound-fixes.js
```
Expected: 自动/手动过程或前端查询缺少物料状态条件导致失败。
- [ ] **Step 3: 实现前端候选过滤**
出库创建页的 `stockQuery` 初始化增加:
```javascript
stockStatus: '1',
productStatus: '1'
```
重置搜索时不清空这两个固定值。手动分配调用增加 `productStatus: '1'`,保留现有 `stockStatus: '1'`。
- [ ] **Step 4: 实现 SQL 双状态过滤**
在手动过程和自动过程的候选 `WHERE` 中、可用数量条件之前增加:
```sql
AND bs.stock_status = '1'
AND bsd.product_status = '1'
```
创建 `sql/20260715_outbound_normal_stock_filter.sql`,使用 `CREATE OR ALTER PROCEDURE` 部署与 `out_stockout_flow.sql` 相同的两个完整过程定义,保证增量环境生效。
- [ ] **Step 5: 运行测试并确认通过**
运行 Step 2 两条命令。Expected: JUnit 与 Node 验证通过。
- [ ] **Step 6: 差异检查点**
运行 `git diff --check`;确认过滤同时存在于 UI 和两个 SQL 过程,而不是只依赖页面。
---
### Task 6: 全量验证和需求审计
**Files:**
- Verify: all files changed in Tasks 1–5
- Update if necessary: `scripts/verify-wcs-inventory-outbound-fixes.js`
**Interfaces:**
- Consumes: 六项实现及其测试。
- Produces: 可逐项追溯的完成证据。
- [ ] **Step 1: 运行专项后端测试**
```powershell
mvn -pl ruoyi-admin -am "-Dtest=ExternalApiControllerWcsCodeTest,PdaServiceImplTest,PdaMapperSqlTest,BasCargoSpaceMapperSqlTest,InventoryInboundTraceMigrationSqlTest,BasStockServiceImplTest,OutStockoutMapperSqlTest" -Dsurefire.failIfNoSpecifiedTests=false test
```
Expected: 0 failures、0 errors。
- [ ] **Step 2: 运行前端/SQL静态需求验证**
```powershell
node scripts/verify-wcs-inventory-outbound-fixes.js
```
Expected: 六项检查全部输出 PASS,进程退出码 0。
- [ ] **Step 3: 运行后端完整测试和打包**
```powershell
mvn test
mvn clean package -DskipTests
```
Expected: 两条命令均 `BUILD SUCCESS`。
- [ ] **Step 4: 运行管理端生产构建**
```powershell
cd ruoyi-ui
npm run build:prod
```
Expected: 退出码 0,无编译错误。
- [ ] **Step 5: 执行工作区质量检查**
```powershell
git diff --check
git status --short
```
Expected: 无新增空白错误;状态列表只包含预期改动和实施前已有改动。
- [ ] **Step 6: 逐项完成审计**
确认以下证据全部存在:请求作业专用响应测试;四入口两区域 SQL;盘点追溯查询、过程和回填迁移;项目号列;只读追溯字段与开关;UI 与两个分配过程双正常过滤。任何一项证据缺失都不得报告完成。
- [ ] **Step 7: 提交策略检查点**
由于目标文件包含实施前未提交改动,本计划默认不暂存、不提交实现文件。最终交付列出本次修改文件和验证结果,由用户决定是否整体提交。