618 lines
22 KiB
Markdown
618 lines
22 KiB
Markdown
# 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, '')) <= 1
|
||
AND COUNT(DISTINCT ISNULL(COALESCE(current_in_type, history_in_type, archived_in_type), '')) <= 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: 提交策略检查点**
|
||
|
||
由于目标文件包含实施前未提交改动,本计划默认不暂存、不提交实现文件。最终交付列出本次修改文件和验证结果,由用户决定是否整体提交。
|