Files
FTC25009/docs/superpowers/plans/2026-07-15-wcs-inventory-outbound-fixes.md

618 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: 提交策略检查点**
由于目标文件包含实施前未提交改动,本计划默认不暂存、不提交实现文件。最终交付列出本次修改文件和验证结果,由用户决定是否整体提交。