diff --git a/docs/testing/2026-07-16-dense-outbound-relocation-dispatch-report.md b/docs/testing/2026-07-16-dense-outbound-relocation-dispatch-report.md new file mode 100644 index 0000000..99a12c4 --- /dev/null +++ b/docs/testing/2026-07-16-dense-outbound-relocation-dispatch-report.md @@ -0,0 +1,135 @@ +# 密集库出库、移库与 WCS 下发验收报告 + +## 验收结论 + +本次实现已完成代码级回归、生产构建、数据库迁移幂等验证、真实只读接口验证和浏览器页面验证。后端共执行 235 个测试,失败、错误和跳过均为 0;后端打包和前端生产构建均成功。 + +运行期验证使用本地 FTC25009-WMS 数据库和重新打包启动的后端,仅执行查询、预览、登录和页面访问,没有提交出库或移库作业,避免影响现有库存。 + +## 验证环境 + +- 日期:2026-07-16 +- 分支:`codex/dense-outbound-relocation` +- Java:17 +- Maven:`D:\BDevTools\maven\apache-maven-3.9.11\bin\mvn.cmd` +- 后端:`http://127.0.0.1:8080` +- 前端:`http://127.0.0.1` +- 数据库:FTC25009-WMS + +## 自动化测试与构建 + +### 后端全量测试 + +```powershell +& 'D:\BDevTools\maven\apache-maven-3.9.11\bin\mvn.cmd' -pl ruoyi-admin -am test +``` + +结果:`BUILD SUCCESS`,`Tests run: 235, Failures: 0, Errors: 0, Skipped: 0`。 + +覆盖的核心测试包括: + +- `OutboundDispatchServiceImplTest`:初次失败、最多 5 次自动重试、人工下发、并发抢占、密集库阻塞和类型路由。 +- `DenseLaneServiceImplTest`、`DenseLaneMapperSqlTest`:端头顺序、遮挡集合、同层不同通道目标、显式字段映射。 +- `RelocationJobServiceImplTest`、`RelocationJobMapperSqlTest`:类型 23 作业、准确预览汇总、库存搬移、货位切换、归档与真实供应商字段。 +- `OutStockoutServiceImplTest`、`LocationOutboundServiceImplTest`:指定库存和指定货位遇到遮挡时的移库编排。 +- `KpiDenseRelocationServiceTest`:03 仓、同层目标、容量、预览与生成复核。 +- `DenseLaneOutboundServiceImplTest`:端头 N 托、完整库存明细、准确货位汇总、预览签名失效和出库口适配。 +- `WcsClientTest`、`OutMydoJobDispatchControllerTest`:独立 WCS 地址和人工下发接口。 + +### 后端打包 + +```powershell +& 'D:\BDevTools\maven\apache-maven-3.9.11\bin\mvn.cmd' clean package -DskipTests +``` + +结果:`BUILD SUCCESS`。打包前停止了占用 JAR 的本地后端,完成后使用新 JAR 重新启动,并通过 `/captchaImage` 返回 HTTP 200 验证服务恢复。 + +### 前端生产构建 + +```powershell +Set-Location ruoyi-ui +npm run build:prod +``` + +结果:构建成功并生成 `dist`。存在项目原有的包体积提示,没有编译错误。 + +## 数据库迁移 + +连续执行两次: + +```powershell +& '.\scripts\apply-sql-file.ps1' -SqlFile 'sql\20260716_dense_outbound_relocation_dispatch.sql' +& '.\scripts\apply-sql-file.ps1' -SqlFile 'sql\20260716_dense_outbound_relocation_dispatch.sql' +``` + +两次均成功。执行后计数如下: + +| 对象 | 数量 | +| --- | ---: | +| `wcs_job_dispatch` 表 | 1 | +| `IX_wcs_job_dispatch_retry` 索引 | 1 | +| `IX_mydo_job_dense_dispatch` 索引 | 1 | +| `bas_task_no` 类型 23 | 1 | +| WCS 补偿 Quartz 任务 | 1 | +| 指定通道出库菜单权限 | 1 | +| 通道出库预览权限 | 1 | +| 通道出库生成权限 | 1 | +| 出库作业人工下发权限 | 1 | + +## 真实接口验证 + +在新打包后端上登录并调用只读接口,得到以下结果: + +| 项目 | 结果 | +| --- | --- | +| 03 仓指定通道列表 | 12 条 | +| 通道编码为空的记录 | 0 条 | +| 验证通道 | `03010101` | +| 端头预览 | 1 托 | +| 预览签名 | 存在 | +| 预览合计库存 | 1 | +| 库存明细合计 | 1 | +| KPI 同层移库源通道 | 7 条 | +| KPI 同层移库目标通道 | 44 条 | + +预览合计与准确选中货位的库存明细合计一致,避免重复托盘码导致跨货位重复汇总。 + +## 页面验证 + +执行: + +```powershell +$env:WMS_TEST_USERNAME='admin' +$env:WMS_TEST_PASSWORD='admin123' +python scripts\verify_dense_outbound_ui.py +``` + +结果:退出码 0,浏览器控制台错误 0,请求失败 0。桌面视口宽度 1440px 和窄屏视口宽度 390px 的页面文档宽度均等于视口宽度,没有页面级横向溢出。 + +截图: + +- [出库作业 WCS 状态与人工下发](screenshots/2026-07-16-dense-outbound/outbound-job-desktop.png) +- [指定通道出库列表](screenshots/2026-07-16-dense-outbound/dense-lane-outbound-desktop.png) +- [真实数据出库预览弹窗](screenshots/2026-07-16-dense-outbound/dense-lane-outbound-dialog-real.png) +- [只读模拟数据完整弹窗](screenshots/2026-07-16-dense-outbound/dense-lane-outbound-dialog.png) +- [指定通道出库窄屏](screenshots/2026-07-16-dense-outbound/dense-lane-outbound-narrow.png) +- [KPI 货位平面图与指定移库](screenshots/2026-07-16-dense-outbound/kpi-plan-desktop.png) + +真实弹窗截图在预览加载遮罩消失且托盘行渲染完成后生成,能看到准确托盘、货位和库存数量。 + +## 六项需求映射 + +| 需求 | 实现与直接证据 | +| --- | --- | +| WCS 失败补偿、最多 5 次自动重试、人工下发、最外侧校验 | Outbox 状态机、Quartz 任务、统一下发服务;`OutboundDispatchServiceImplTest` 15 项通过,出库作业页面验证通过。 | +| 指定托盘被遮挡时自动生成类型 23 移库 | 出库分配调用遮挡移库编排;`OutStockoutServiceImplTest`、`RelocationJobServiceImplTest` 通过。 | +| KPI 平面图指定同层移库 | 同仓同层、不同通道及容量复核;KPI 测试通过,真实选项为源 7、目标 44,页面正常。 | +| 入库、普通出库、指定货位/通道、移库分别使用 WCS 地址 | 按 `in_or_out` 路由到 putaway、retrieve、location-outbound、relocation;`WcsClientTest` 和 dispatch 路由测试通过。 | +| 指定货位出库被遮挡时先生成移库 | `LocationOutboundServiceImpl` 接入统一遮挡编排;对应服务测试通过。 | +| 指定通道按端头 N 托整托出库 | 列表、库存明细、准确预览签名、出库口适配和生成流程已实现;真实预览 1 托且合计一致,桌面与窄屏页面验证通过。 | + +## 边界说明 + +- 本轮没有在现有业务库提交真实出库、移库或 WCS 失败作业;状态流转、库存搬移和失败补偿由自动化测试验证。 +- 真实 WCS 联调仍需在可控测试单据和 WCS 测试地址下执行,避免向生产设备下发任务。 +- 前端生产构建只有现有项目的包体积警告,不影响本次功能验收。 diff --git a/docs/testing/screenshots/2026-07-16-dense-outbound/dense-lane-outbound-desktop.png b/docs/testing/screenshots/2026-07-16-dense-outbound/dense-lane-outbound-desktop.png new file mode 100644 index 0000000..2e579e7 Binary files /dev/null and b/docs/testing/screenshots/2026-07-16-dense-outbound/dense-lane-outbound-desktop.png differ diff --git a/docs/testing/screenshots/2026-07-16-dense-outbound/dense-lane-outbound-dialog-real.png b/docs/testing/screenshots/2026-07-16-dense-outbound/dense-lane-outbound-dialog-real.png new file mode 100644 index 0000000..a1b5cd4 Binary files /dev/null and b/docs/testing/screenshots/2026-07-16-dense-outbound/dense-lane-outbound-dialog-real.png differ diff --git a/docs/testing/screenshots/2026-07-16-dense-outbound/dense-lane-outbound-dialog.png b/docs/testing/screenshots/2026-07-16-dense-outbound/dense-lane-outbound-dialog.png new file mode 100644 index 0000000..1a15f35 Binary files /dev/null and b/docs/testing/screenshots/2026-07-16-dense-outbound/dense-lane-outbound-dialog.png differ diff --git a/docs/testing/screenshots/2026-07-16-dense-outbound/dense-lane-outbound-narrow.png b/docs/testing/screenshots/2026-07-16-dense-outbound/dense-lane-outbound-narrow.png new file mode 100644 index 0000000..d72b380 Binary files /dev/null and b/docs/testing/screenshots/2026-07-16-dense-outbound/dense-lane-outbound-narrow.png differ diff --git a/docs/testing/screenshots/2026-07-16-dense-outbound/kpi-plan-desktop.png b/docs/testing/screenshots/2026-07-16-dense-outbound/kpi-plan-desktop.png new file mode 100644 index 0000000..5df2d92 Binary files /dev/null and b/docs/testing/screenshots/2026-07-16-dense-outbound/kpi-plan-desktop.png differ diff --git a/docs/testing/screenshots/2026-07-16-dense-outbound/outbound-job-desktop.png b/docs/testing/screenshots/2026-07-16-dense-outbound/outbound-job-desktop.png new file mode 100644 index 0000000..235b99e Binary files /dev/null and b/docs/testing/screenshots/2026-07-16-dense-outbound/outbound-job-desktop.png differ diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/dense/service/impl/DenseLaneOutboundServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/out/dense/service/impl/DenseLaneOutboundServiceImpl.java index 655a755..e4d32eb 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/out/dense/service/impl/DenseLaneOutboundServiceImpl.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/dense/service/impl/DenseLaneOutboundServiceImpl.java @@ -173,11 +173,14 @@ public class DenseLaneOutboundServiceImpl implements IDenseLaneOutboundService position.getCargoSpaceId(), position.getTrayCode()); position.setStockDetails(details == null ? new ArrayList<>() : details); }); - String[] trayCodes = selected.stream().map(DenseLanePosition::getTrayCode).toArray(String[]::new); - BigDecimal total = relocationJobMapper.sumTrayStockQuantity(trayCodes); + BigDecimal total = selected.stream() + .flatMap(position -> position.getStockDetails().stream()) + .map(MydoJobDetail::getStockQuantity) + .filter(quantity -> quantity != null) + .reduce(BigDecimal.ZERO, BigDecimal::add); DenseLaneOutboundPreview preview = new DenseLaneOutboundPreview(); preview.setTrays(selected); - preview.setTotalQuantity(total == null ? BigDecimal.ZERO : total); + preview.setTotalQuantity(total); preview.setPreviewToken(previewToken(request, selected)); return preview; } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/mapper/RelocationJobMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/mapper/RelocationJobMapper.java index 41d49c9..d3b5ce3 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/mapper/RelocationJobMapper.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/mapper/RelocationJobMapper.java @@ -1,6 +1,5 @@ package com.ruoyi.out.relocation.mapper; -import java.math.BigDecimal; import java.util.List; import java.util.Map; import com.ruoyi.in.job.domain.MydoJob; @@ -21,8 +20,6 @@ public interface RelocationJobMapper List selectRelocationSourcesForUpdate(RelocationGenerateRequest request); - BigDecimal sumTrayStockQuantity(@Param("trayCodes") String[] trayCodes); - int reserveTarget(Map params); int reserveSource(Map params); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/service/impl/RelocationJobServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/service/impl/RelocationJobServiceImpl.java index 477b942..c18279a 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/service/impl/RelocationJobServiceImpl.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/relocation/service/impl/RelocationJobServiceImpl.java @@ -82,9 +82,11 @@ public class RelocationJobServiceImpl implements IRelocationJobService .stream().limit(request.getTrayCount()).collect(Collectors.toList()); RelocationPreview preview = new RelocationPreview(); preview.setTrays(selected); - String[] trayCodes = selected.stream().map(DenseLanePosition::getTrayCode).toArray(String[]::new); - BigDecimal total = mapper.sumTrayStockQuantity(trayCodes); - preview.setTotalQuantity(total == null ? BigDecimal.ZERO : total); + BigDecimal total = selected.stream() + .map(DenseLanePosition::getStockQuantity) + .filter(quantity -> quantity != null) + .reduce(BigDecimal.ZERO, BigDecimal::add); + preview.setTotalQuantity(total); return preview; } diff --git a/ruoyi-admin/src/main/resources/mapper/out/DenseLaneMapper.xml b/ruoyi-admin/src/main/resources/mapper/out/DenseLaneMapper.xml index c1b4c0c..aa13165 100644 --- a/ruoyi-admin/src/main/resources/mapper/out/DenseLaneMapper.xml +++ b/ruoyi-admin/src/main/resources/mapper/out/DenseLaneMapper.xml @@ -21,6 +21,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + + + + + + + + + + + + + + + + + + + + + + resultMap="com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper.BasCargoSpaceResult"> select top 1 target_space.* from bas_cargo_space target_space with (UPDLOCK,HOLDLOCK,ROWLOCK) left join bas_cargo_area_config area_config @@ -235,7 +259,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" target_space.cargo_space_id asc - ;with reserved_targets as ( select distinct target_cargo_space_id as cargo_space_id @@ -282,7 +306,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" order by lane.group_coding - ;with lane_positions as ( select cargo.cargo_space_id, diff --git a/ruoyi-admin/src/main/resources/mapper/out/RelocationJobMapper.xml b/ruoyi-admin/src/main/resources/mapper/out/RelocationJobMapper.xml index 28d6e82..c9650f8 100644 --- a/ruoyi-admin/src/main/resources/mapper/out/RelocationJobMapper.xml +++ b/ruoyi-admin/src/main/resources/mapper/out/RelocationJobMapper.xml @@ -4,14 +4,49 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> - select * from mydo_job with (UPDLOCK,HOLDLOCK) where job_id = #{jobId} - - update bas_cargo_space set status = '3', tray_code = #{trayCode} diff --git a/ruoyi-admin/src/test/java/com/ruoyi/out/dense/mapper/DenseLaneMapperSqlTest.java b/ruoyi-admin/src/test/java/com/ruoyi/out/dense/mapper/DenseLaneMapperSqlTest.java index 63d92c6..9235cd9 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/out/dense/mapper/DenseLaneMapperSqlTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/out/dense/mapper/DenseLaneMapperSqlTest.java @@ -51,6 +51,7 @@ class DenseLaneMapperSqlTest String sql = section(xml, ""); + String summarySelect = section(xml, ""); + String stockSelect = section(xml, ""); + + assertThat(positionMap) + .contains("property=\"cargoSpaceId\" column=\"cargo_space_id\"") + .contains("property=\"stockQuantity\" column=\"stock_quantity\""); + assertThat(stockMap) + .contains("property=\"stockId\" column=\"stock_id\"") + .contains("property=\"stockinDetailId\" column=\"stockin_detail_id\"") + .contains("property=\"projectNo\" column=\"project_no\""); + assertThat(jobSelect) + .contains("resultMap=\"com.ruoyi.in.job.mapper.MydoJobMapper.MydoJobResult\""); + assertThat(stockSelect).contains("resultMap=\"StockDetailResult\""); + assertThat(sourceSelect).contains("resultMap=\"DenseLanePositionResult\""); + } + + @Test + void stockDetailQueryUsesActualBasStockSupplierColumns() throws Exception + { + String xml = Files.readString(resolveMapperXml()); + String stockSelect = section(xml, "