From 3a74a3f59840592c0c3f73d839d9ea1c62ceff7e Mon Sep 17 00:00:00 2001 From: liyongde <1419499670@qq.com> Date: Tue, 18 Aug 2026 18:44:13 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=AD=90=E5=8D=B7=E8=B4=A8?= =?UTF-8?q?=E6=A3=80=E4=B8=8D=E5=90=88=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-packing-area-point-management-design.html | 32 + ...da-sub-roll-quality-inspection-design.html | 21 + .../2026-08-12-auto-send-sub-roll-task.md | 42 + ...026-08-13-auto-sub-roll-down-call-empty.md | 43 + .../2026-08-13-query-package-point-info.md | 53 ++ .../2026-08-14-packing-area-dashboard.md | 103 +++ .../plans/2026-08-17-box-type-lash-default.md | 58 ++ .../plans/2026-08-17-box-type-max-num.md | 186 +++++ .../plans/2026-08-17-box-type-select-modal.md | 204 +++++ ...08-17-sub-package-relation-batch-create.md | 736 ++++++++++++++++++ .../2026-08-17-sub-package-relation-batch.md | 133 ++++ ...26-08-12-auto-send-sub-roll-task-design.md | 25 + ...13-auto-sub-roll-down-call-empty-design.md | 28 + ...026-08-14-packing-area-dashboard-design.md | 41 + .../module/lms/enums/ErrorCodeConstants.java | 2 + .../module/lms/enums/PackageTaskTypeEnum.java | 1 + .../packagepoint/PackagePdaServiceImpl.java | 78 +- .../lms/tasks/box/CheckFailedAgvTask.java | 94 +++ 18 files changed, 1879 insertions(+), 1 deletion(-) create mode 100644 docs/prototype_design/design/2026-08-18-pda-packing-area-point-management-design.html create mode 100644 docs/prototype_design/design/2026-08-18-pda-sub-roll-quality-inspection-design.html create mode 100644 docs/superpowers/plans/2026-08-12-auto-send-sub-roll-task.md create mode 100644 docs/superpowers/plans/2026-08-13-auto-sub-roll-down-call-empty.md create mode 100644 docs/superpowers/plans/2026-08-13-query-package-point-info.md create mode 100644 docs/superpowers/plans/2026-08-14-packing-area-dashboard.md create mode 100644 docs/superpowers/plans/2026-08-17-box-type-lash-default.md create mode 100644 docs/superpowers/plans/2026-08-17-box-type-max-num.md create mode 100644 docs/superpowers/plans/2026-08-17-box-type-select-modal.md create mode 100644 docs/superpowers/plans/2026-08-17-sub-package-relation-batch-create.md create mode 100644 docs/superpowers/plans/2026-08-17-sub-package-relation-batch.md create mode 100644 docs/superpowers/specs/2026-08-12-auto-send-sub-roll-task-design.md create mode 100644 docs/superpowers/specs/2026-08-13-auto-sub-roll-down-call-empty-design.md create mode 100644 docs/superpowers/specs/2026-08-14-packing-area-dashboard-design.md create mode 100644 nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/tasks/box/CheckFailedAgvTask.java diff --git a/docs/prototype_design/design/2026-08-18-pda-packing-area-point-management-design.html b/docs/prototype_design/design/2026-08-18-pda-packing-area-point-management-design.html new file mode 100644 index 00000000..292cde7c --- /dev/null +++ b/docs/prototype_design/design/2026-08-18-pda-packing-area-point-management-design.html @@ -0,0 +1,32 @@ + + + + + + 装箱区点位管理 - PDA 原型 + + + +

装箱区点位管理

点位信息

点位信息已加载,可修改点位状态后确认

+ + + diff --git a/docs/prototype_design/design/2026-08-18-pda-sub-roll-quality-inspection-design.html b/docs/prototype_design/design/2026-08-18-pda-sub-roll-quality-inspection-design.html new file mode 100644 index 00000000..e769ff91 --- /dev/null +++ b/docs/prototype_design/design/2026-08-18-pda-sub-roll-quality-inspection-design.html @@ -0,0 +1,21 @@ + + + + + + 子卷质检 - PDA 原型 + + + +

子卷质检

子卷信息

点位信息已加载,请确认后提交质检结果

+ + + diff --git a/docs/superpowers/plans/2026-08-12-auto-send-sub-roll-task.md b/docs/superpowers/plans/2026-08-12-auto-send-sub-roll-task.md new file mode 100644 index 00000000..ccddb7d1 --- /dev/null +++ b/docs/superpowers/plans/2026-08-12-auto-send-sub-roll-task.md @@ -0,0 +1,42 @@ +# 子卷下线送待检区任务实现计划 + +> **面向 AI 代理的工作者:** 在当前功能分支按步骤实现并逐项验证。 + +**目标:** 完成子卷下线点与待检区点位的并发安全配对和锁定,并由定时任务触发。 + +**架构:** Mapper XML 提供稳定排序查询和条件更新;业务 Service 在事务内完成点位抢锁、配对及无目的点时的起点释放;Job 仅调用业务入口。任务创建暂保留明确的 TODO。 + +**技术栈:** Java 17、Spring Boot、MyBatis、MySQL、XXL-JOB。 + +--- + +### 任务 1:实现点位查询和锁操作 + +**文件:** +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/packagepoint/PackagePointMapper.java` +- 修改:`nl-module-lms/nl-module-lms-server/src/main/resources/mapper/packagepoint/PackagePointMapper.xml` + +- [ ] 声明可用点位查询、条件加锁、解锁 Mapper 方法。 +- [ ] 在 XML 中按点位类型、状态、锁状态和删除标记查询,并稳定排序。 +- [ ] 条件加锁仅允许 `lock_status = '0'` 更新为 `'1'`。 + +### 任务 2:实现业务配对并接入 Job + +**文件:** +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackageBoxBusinessService.java` +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackageBoxBusinessServiceImpl.java` +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/job/box/BoxAutoJob.java` + +- [ ] 在 Service 接口增加自动创建子卷送检任务入口。 +- [ ] 在事务中查询起点和目的点,逐一条件抢锁并配对。 +- [ ] 目的点不足时释放未配对起点,配对成功后调用任务模块创建运输任务。 +- [ ] Job 调用 Service 业务入口。 + +### 任务 3:同步表结构并验证 + +**文件:** +- 修改:`sql/mysql/lms_package_point.sql` + +- [ ] 在建表脚本补充非空且默认未锁定的 `lock_status` 字段。 +- [ ] 编译 `nl-module-lms-server` 及其依赖模块,要求 Maven 退出码为 0。 +- [ ] 检查 Git 差异,确认没有覆盖用户已有修改。 diff --git a/docs/superpowers/plans/2026-08-13-auto-sub-roll-down-call-empty.md b/docs/superpowers/plans/2026-08-13-auto-sub-roll-down-call-empty.md new file mode 100644 index 00000000..c3739971 --- /dev/null +++ b/docs/superpowers/plans/2026-08-13-auto-sub-roll-down-call-empty.md @@ -0,0 +1,43 @@ +# 子卷下线补充空架子实现计划 + +**目标:** 完成 `autoSubRollDownCallEmptyJob` 的空架 AGV 任务自动创建及点位状态回调。 + +**架构:** 复用现有点位查询和条件锁 Mapper;业务 Service 在事务内按优先级配对并创建任务;独立 AGV 回调类维护起点、目的点状态;Job 仅负责调度。 + +**技术栈:** Java 17、Spring Boot、MyBatis、XXL-JOB、任务模块 API。 + +--- + +### 任务 1:增加空架任务类型和业务入口 + +**文件:** +- 修改:`nl-module-lms/nl-module-lms-api/src/main/java/cn/code/nl/module/lms/enums/PackageTaskTypeEnum.java` +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackageBoxBusinessService.java` +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/job/box/BoxAutoJob.java` + +- [x] 新增子卷下线补空架 AGV 任务类型。 +- [x] 新增业务方法并由定时器记录日志后调用。 + +### 任务 2:实现点位分配和任务创建 + +**文件:** +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackageBoxBusinessServiceImpl.java` + +- [x] 查询下线空位、装箱空载具、待检空载具和待检空位。 +- [x] 每个下线空位优先配装箱空载具,其次配待检空载具。 +- [x] 将剩余装箱空载具配到待检空位。 +- [x] 条件锁定两端点位并创建空架 AGV 任务。 + +### 任务 3:实现空架任务回调 + +**文件:** +- 创建:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/tasks/box/SubRollCallEmptyAgvTask.java` + +- [x] 完成时将目的点更新为空载具并解锁。 +- [x] 取消时解锁两端点位。 +- [x] 取货时将起点更新为空位、清空库存信息并解锁。 + +### 任务 4:验证 + +- [x] 编译 LMS 服务及其依赖模块,确认 Maven 退出码为 0。 +- [x] 检查 Git 差异,确认只修改本功能相关文件且未覆盖用户改动。 diff --git a/docs/superpowers/plans/2026-08-13-query-package-point-info.md b/docs/superpowers/plans/2026-08-13-query-package-point-info.md new file mode 100644 index 00000000..aaa9bdfa --- /dev/null +++ b/docs/superpowers/plans/2026-08-13-query-package-point-info.md @@ -0,0 +1,53 @@ +# 查询装箱区点位信息实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** PDA 端仅传入点位编码,返回装箱区点位的点位编码、子卷号和载具号。 + +**架构:** 保持现有 `ManaQualityCheckReqVO` 作为请求对象,由 `PackagePdaService` 复用装箱区点位服务完成查询和不存在校验。Controller 使用 `PackagePointRespVO` 作为响应类型,并手动映射需求指定的三个字段。 + +**技术栈:** Java 17、Spring Boot、Jakarta Validation、MyBatis-Plus + +--- + +### 任务 1:补齐 PDA 点位查询接口 + +**文件:** +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/pda/vo/ManaQualityCheckReqVO.java` +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackagePdaService.java` +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackagePdaServiceImpl.java` +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/pda/PackagePdaController.java` + +- [x] **步骤 1:校验点位编码** + +在 `ManaQualityCheckReqVO.pointCode` 上添加 `@NotEmpty(message = "点位编码不能为空")`。 + +- [x] **步骤 2:声明并实现查询服务** + +在 `PackagePdaService` 声明: + +```java +PackagePointDO queryPointInfo(String pointCode); +``` + +实现类通过 `PackagePointService.getPackagePointByCode(pointCode, false)` 查询;结果为空时抛出 `PACKAGE_POINT_NOT_EXISTS`。 + +- [x] **步骤 3:完成 Controller 响应映射** + +将接口返回类型改为 `CommonResult`,并只设置: + +```java +response.setPointCode(packagePoint.getPointCode()); +response.setContainerName(packagePoint.getContainerName()); +response.setVehicleCode(packagePoint.getVehicleCode()); +``` + +- [x] **步骤 4:编译验证** + +运行: + +```powershell +mvn -pl nl-module-lms/nl-module-lms-server -am -DskipTests compile +``` + +预期:构建成功,无 Java 编译错误。 diff --git a/docs/superpowers/plans/2026-08-14-packing-area-dashboard.md b/docs/superpowers/plans/2026-08-14-packing-area-dashboard.md new file mode 100644 index 00000000..aebab3e3 --- /dev/null +++ b/docs/superpowers/plans/2026-08-14-packing-area-dashboard.md @@ -0,0 +1,103 @@ +# 装箱区信息看板实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 创建一个包含 66 个点位、CAD 式平面布局和完整 Demo 交互的独立装箱区信息看板 HTML。 + +**架构:** 使用一个 HTML 文件内嵌样式、确定性模拟数据和交互脚本。数据生成、点位渲染、统计计算、地图变换与用户交互使用独立函数组织,不依赖 API、CDN 或构建工具。 + +**技术栈:** HTML5、CSS3、原生 JavaScript、浏览器 localStorage 与 Clipboard API。 + +--- + +## 文件结构 + +- 创建:`packingAreaDashboard.html`,承载页面结构、视觉样式、模拟数据、布局渲染和交互。 +- 创建:`docs/superpowers/specs/2026-08-14-packing-area-dashboard-design.md`,记录已确认设计。 +- 创建:`docs/superpowers/plans/2026-08-14-packing-area-dashboard.md`,记录实现和验证步骤。 + +### 任务 1:建立页面骨架与大屏视觉 + +**文件:** +- 创建:`packingAreaDashboard.html` + +- [ ] **步骤 1:建立 1920 × 1080 舞台** + +创建 `#stage`、顶部标题、KPI、工具条、地图和侧栏结构,并使用 `scaleStage()` 根据窗口尺寸等比缩放。 + +- [ ] **步骤 2:实现深色科技风样式** + +使用径向渐变、网格背景、青蓝描边、玻璃面板和细微辉光,避免外部字体与图片依赖。 + +### 任务 2:生成 66 个点位与模拟数据 + +**文件:** +- 修改:`packingAreaDashboard.html` + +- [ ] **步骤 1:定义点位编码生成函数** + +`rangeCodes(prefix, start, end)` 必须生成补零后的编码;北区 6 个、中区 42 个、南区 18 个。 + +- [ ] **步骤 2:定义确定性模拟数据** + +`createMockPoint(code, index, zone)` 根据索引生成稳定的库存状态、卷号、箱号、进入时间和停留时长,刷新页面后数据仍保持一致。 + +- [ ] **步骤 3:渲染点位卡片** + +`renderSlot(point, className)` 输出编码、状态、卷号、箱号和停留时长,并附带搜索、选中和右键菜单所需数据属性。 + +### 任务 3:绘制北中南平面布局和 AGV 通道 + +**文件:** +- 修改:`packingAreaDashboard.html` + +- [ ] **步骤 1:渲染北区** + +使用 6 列网格呈现 `ZJXX_01` 至 `ZJXX_06`,在区域上下绘制横向 AGV 通道。 + +- [ ] **步骤 2:渲染中区** + +使用 7 行 × 6 列网格呈现 `DJQ_01` 至 `DJQ_42`,在两列点位之间插入纵向白色通道,并在区域周边绘制闭合通道。 + +- [ ] **步骤 3:渲染南区** + +两行分别呈现 8 个 `ZXQ` 点位,在右侧呈现 `ZXDJW_01` 和 `ZXDJW_02`,使用不同区域标签区分装箱区 1 和装箱区 2。 + +### 任务 4:实现统计与交互 + +**文件:** +- 修改:`packingAreaDashboard.html` + +- [ ] **步骤 1:计算 KPI 和队列** + +`updateDashboard()` 根据点位状态和超时阈值刷新总数、占用、空闲、超时、占用率及装箱队列。 + +- [ ] **步骤 2:实现卷号搜索定位** + +`locateRoll()` 匹配完整或部分卷号,选中第一个结果,将地图平移到点位附近并显示结果提示。 + +- [ ] **步骤 3:实现地图操作** + +`fitMap()`、`applyTransform()`、滚轮事件和拖动事件共同支持适配、缩放、平移与双击复位。 + +- [ ] **步骤 4:实现点位详情与右键复制** + +点位单击后调用 `selectPoint()` 更新详情;右键菜单根据字段是否存在启用复制按钮,并使用 Clipboard API 或文本选择回退方案复制。 + +### 任务 5:执行交付验证 + +**文件:** +- 验证:`packingAreaDashboard.html` + +- [ ] **步骤 1:执行静态结构检查** + +确认文件包含 66 个点位的生成规则、无 `fetch()` 和外部资源引用,并检查脚本语法。 + +- [ ] **步骤 2:执行浏览器视觉检查** + +以 1920 × 1080 视口打开本地文件,确认三大区域、KPI、侧栏和点位内容没有遮挡或主体溢出。 + +- [ ] **步骤 3:执行交互检查** + +验证卷号搜索、点位选择、缩放、复位和右键菜单,确认浏览器控制台无错误。 + diff --git a/docs/superpowers/plans/2026-08-17-box-type-lash-default.md b/docs/superpowers/plans/2026-08-17-box-type-lash-default.md new file mode 100644 index 00000000..249f8489 --- /dev/null +++ b/docs/superpowers/plans/2026-08-17-box-type-lash-default.md @@ -0,0 +1,58 @@ +# 木箱类型捆扎选项默认值实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 新增木箱类型时,“是否一次捆扎”和“是否二次捆扎”默认选中为 `true`,编辑时保持接口原值。 + +**架构:** 复用现有 Vben 表单 schema 的默认值能力,在两个布尔字段的 schema 顶层设置默认值。编辑弹窗既有的 `setValues` 会继续使用详情数据覆盖默认值。 + +**技术栈:** Vue 3、TypeScript、Vben Form、Ant Design Vue + +--- + +## 文件结构 + +- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/data.ts`:定义新增/编辑表单字段及默认值。 + +### 任务 1:设置捆扎选项默认值 + +**文件:** +- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/data.ts` + +- [x] **步骤 1:为一次捆扎字段增加默认值** + +在 `needLashOne` 的 `componentProps` 后增加 schema 顶层属性: + +```ts + defaultValue: true, +``` + +- [x] **步骤 2:为二次捆扎字段增加默认值** + +在 `needLashTwo` 的 `componentProps` 后增加相同的 schema 顶层属性: + +```ts + defaultValue: true, +``` + +- [x] **步骤 3:静态验证改动** + +运行: + +```powershell +Select-String -Path nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/data.ts -Pattern "fieldName: 'needLashOne'|fieldName: 'needLashTwo'|defaultValue: true" -Context 0,10 +``` + +预期:新增/编辑表单中的两个字段各包含一个 schema 顶层 `defaultValue: true`,搜索表单不包含默认值。 + +- [x] **步骤 4:检查编辑回填不受影响** + +运行: + +```powershell +Select-String -Path nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/modules/form.vue -Pattern "formApi.setValues\(formData.value\)" +``` + +预期:仍能找到编辑详情回填语句,接口原值会覆盖 schema 默认值。 + +按项目约定,用户未授权测试,因此不新增或运行测试。 diff --git a/docs/superpowers/plans/2026-08-17-box-type-max-num.md b/docs/superpowers/plans/2026-08-17-box-type-max-num.md new file mode 100644 index 00000000..4364c3a3 --- /dev/null +++ b/docs/superpowers/plans/2026-08-17-box-type-max-num.md @@ -0,0 +1,186 @@ +# 木箱类型最大子卷数实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 为木箱类型补齐 `maxNum` 的后端持久化与校验、前端新增编辑输入和列表展示能力。 + +**架构:** 沿用木箱类型现有的 MyBatis Plus、Bean 转换和 Vben 表单/表格链路,在数据对象、请求响应 VO、前端 API 类型和两个表格入口中增加同名字段。前端负责默认值及输入体验,后端以 Bean Validation 保证值为不小于 1 的必填整数。 + +**技术栈:** Java 17、Spring Boot、Jakarta Validation、MyBatis Plus、Vue 3、TypeScript、Vben Admin、Ant Design Vue `InputNumber`、VXE Table。 + +--- + +## 文件结构 + +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/dataobject/boxtype/BoxTypeDO.java` —— 持久化 `max_num`。 +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeSaveReqVO.java` —— 接收并校验新增、编辑请求。 +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeRespVO.java` —— 详情、分页和导出返回字段。 +- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/boxtype/index.ts` —— 声明前端 API 字段类型。 +- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/data.ts` —— 新增编辑表单和管理列表配置。 +- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue` —— 复用选择弹窗展示字段。 + +### 任务 1:补齐后端最大子卷数字段链路 + +**文件:** +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/dataobject/boxtype/BoxTypeDO.java:50` +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeSaveReqVO.java:29` +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeRespVO.java:38` + +- [ ] **步骤 1:在数据对象中声明持久化字段** + +在尺寸字段之后增加: + +```java + /** + * 最大子卷数 + */ + private Integer maxNum; +``` + +MyBatis Plus 根据项目现有驼峰映射将 `maxNum` 映射到 `max_num`,不修改 Mapper XML。 + +- [ ] **步骤 2:在保存请求中增加必填正整数校验** + +在尺寸字段之后增加: + +```java + @Schema(description = "最大子卷数", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "最大子卷数不能为空") + @Min(value = 1, message = "最大子卷数不能小于1") + private Integer maxNum; +``` + +`Integer` 保证 JSON 反序列化后的类型为整数,`@NotNull` 和 `@Min(1)` 同时覆盖空值、0 和负数。 + +- [ ] **步骤 3:在响应及导出模型中返回字段** + +在尺寸字段之后增加: + +```java + @Schema(description = "最大子卷数", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("最大子卷数") + private Integer maxNum; +``` + +- [ ] **步骤 4:执行后端静态差异检查** + +运行: + +```powershell +git -c safe.directory=D:/Code/Work/nl/huachuang diff --check -- nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/dataobject/boxtype/BoxTypeDO.java nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeSaveReqVO.java nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeRespVO.java +``` + +预期:命令无输出,退出码为 `0`。 + +### 任务 2:增加前端输入和展示 + +**文件:** +- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/boxtype/index.ts:14` +- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/data.ts:158` +- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue:36` + +- [ ] **步骤 1:扩展木箱类型 API 类型** + +在尺寸字段后增加必填数字字段: + +```ts + maxNum: number; // 最大子卷数 +``` + +- [ ] **步骤 2:在新增编辑表单中增加整数输入框** + +在尺寸字段组之后增加: + +```ts + { + fieldName: 'maxNum', + label: '最大子卷数', + rules: 'required', + component: 'InputNumber', + defaultValue: 1, + componentProps: { + min: 1, + placeholder: '请输入最大子卷数', + precision: 0, + step: 1, + }, + }, +``` + +新增表单使用 `defaultValue: 1`;编辑表单继续由现有详情回填覆盖默认值。`precision: 0` 限制小数,`min: 1` 禁止 0 和负数。 + +- [ ] **步骤 3:在管理列表中展示最大子卷数** + +在尺寸列之后增加: + +```ts + { + field: 'maxNum', + title: '最大子卷数', + minWidth: 120, + }, +``` + +- [ ] **步骤 4:在木箱类型选择弹窗中展示最大子卷数** + +在 `baseColumns` 的尺寸列之后增加: + +```ts + { field: 'maxNum', title: '最大子卷数', minWidth: 120 }, +``` + +- [ ] **步骤 5:执行前端 ESLint** + +在 `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben` 目录运行: + +```powershell +node node_modules/eslint/bin/eslint.js apps/web-antdv-next/src/api/lms/boxtype/index.ts apps/web-antdv-next/src/views/lms/boxtype/data.ts apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue +``` + +预期:无 ESLint 错误,退出码为 `0`。 + +### 任务 3:验证完整字段链路 + +**文件:** +- 验证:任务 1 和任务 2 中列出的全部文件 + +- [ ] **步骤 1:搜索确认字段覆盖范围** + +运行: + +```powershell +rg -n "maxNum|最大子卷数" nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/boxtype nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype +``` + +预期:DO、SaveReqVO、RespVO、API 类型、表单、管理列表和选择弹窗均有匹配项。 + +- [ ] **步骤 2:编译 LMS 后端模块及其依赖** + +在仓库根目录运行: + +```powershell +mvn -pl nl-module-lms/nl-module-lms-server -am -DskipTests compile +``` + +预期:输出 `BUILD SUCCESS`。 + +- [ ] **步骤 3:检查最终差异格式** + +运行: + +```powershell +git -c safe.directory=D:/Code/Work/nl/huachuang diff --check -- nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/dataobject/boxtype/BoxTypeDO.java nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeSaveReqVO.java nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeRespVO.java nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/boxtype/index.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/data.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue +``` + +预期:命令无输出,退出码为 `0`。 + +- [ ] **步骤 4:审阅差异,确保不包含数据库脚本或无关重构** + +运行: + +```powershell +git -c safe.directory=D:/Code/Work/nl/huachuang diff -- nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/dataobject/boxtype/BoxTypeDO.java nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeSaveReqVO.java nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/boxtype/vo/BoxTypeRespVO.java nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/boxtype/index.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/data.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue +``` + +预期:只包含已有用户改动及本计划的 `maxNum` 字段修改;不新增数据库迁移文件,不改动无关业务逻辑。 + diff --git a/docs/superpowers/plans/2026-08-17-box-type-select-modal.md b/docs/superpowers/plans/2026-08-17-box-type-select-modal.md new file mode 100644 index 00000000..ab477c45 --- /dev/null +++ b/docs/superpowers/plans/2026-08-17-box-type-select-modal.md @@ -0,0 +1,204 @@ +# 木箱类型选择弹窗实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 新增一个可复用的木箱类型选择弹窗,通过 `multiple` 参数支持单选和多选,并通过 `select` 事件返回选中记录。 + +**架构:** 组件复用现有 `getBoxTypePage` 分页接口和 Vben VxeGrid/Modal 组合。弹窗打开时根据 `multiple` 动态设置 radio 或 checkbox 选择列,关闭时清理选择状态。 + +**技术栈:** Vue 3、TypeScript、Vben Modal、Vben VxeGrid、Ant Design Vue + +--- + +## 文件结构 + +- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue`:封装木箱类型分页查询、单选/多选和选中结果回传。 + +### 任务 1:创建木箱类型选择弹窗 + +**文件:** +- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue` + +- [x] **步骤 1:创建组件目录和文件** + +组件完整实现: + +```vue + + + +``` + +- [x] **步骤 2:运行 ESLint** + +在 `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben` 目录运行: + +```powershell +node node_modules/eslint/bin/eslint.js apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue +``` + +预期:退出码为 `0`,无 ESLint 错误。 + +- [x] **步骤 3:检查未选择提示和动态选择列** + +运行: + +```powershell +Select-String -Path apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue -Pattern "multiple \? 'checkbox' : 'radio'|请至少选择一条木箱类型|emit\('select'" +``` + +预期:三个模式均能匹配。 + +- [x] **步骤 4:检查 Git 差异** + +运行: + +```powershell +git diff --check -- apps/web-antdv-next/src/views/lms/boxtype/components/BoxTypeSelectModal.vue +``` + +预期:退出码为 `0`。 + +按项目约定,本任务不新增或运行测试。 diff --git a/docs/superpowers/plans/2026-08-17-sub-package-relation-batch-create.md b/docs/superpowers/plans/2026-08-17-sub-package-relation-batch-create.md new file mode 100644 index 00000000..ff4b438e --- /dev/null +++ b/docs/superpowers/plans/2026-08-17-sub-package-relation-batch-create.md @@ -0,0 +1,736 @@ +# 子卷包装关系批量新增实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 为子卷包装关系提供可在卡片/表格间自由切换的批量新增弹窗,并由后端按“木箱码 + 木箱类型”分组校验容量、隔离保存和逐行返回失败原因。 + +**架构:** 前端批量弹窗以单一 `rows` 数组驱动两种视图,通过稳定 `clientKey` 映射后端错误;木箱类型复用现有选择弹窗,状态复用 `sub_package_status` 字典。后端新增独立批量 VO 和接口,由主 Service 完成逐行校验、分组和容量判断,再调用独立 `REQUIRES_NEW` 保存服务逐组写入,保证失败组不影响合法组。 + +**技术栈:** Java 17、Spring Boot、Jakarta Validation、MyBatis-Plus、JUnit 5、Mockito、Vue 3、TypeScript、Vben Modal、Antdv Next、Vitest。 + +--- + +## 文件结构 + +### 后端新增 + +- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/vo/SubPackageRelationBatchCreateReqVO.java`:批量请求容器。 +- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/vo/SubPackageRelationBatchCreateItemReqVO.java`:单条批量录入数据。 +- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/vo/SubPackageRelationBatchCreateRespVO.java`:成功/失败数量和失败行结构。 +- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationBatchSaveService.java`:每个业务分组的独立事务写入器。 +- `nl-module-lms/nl-module-lms-server/src/test/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationServiceImplTest.java`:分组、容量、部分成功和失败映射单元测试。 +- `nl-module-lms/nl-module-lms-server/src/test/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationBatchSaveServiceTest.java`:分组批量写入委托测试。 + +### 后端修改 + +- `nl-module-lms/nl-module-lms-server/pom.xml`:补充 Mockito Inline 测试依赖。 +- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/SubPackageRelationController.java`:暴露 `POST /batch-create`。 +- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationService.java`:声明批量创建方法。 +- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationServiceImpl.java`:逐行校验、类型查询、分组、容量校验、逐组保存和响应组装。 +- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/subpackagerelation/SubPackageRelationMapper.java`:统计相同木箱码和木箱类型已有记录。 +- `nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/boxtype/BoxTypeMapper.java`:批量查询木箱类型。 + +### 前端新增 + +- `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.ts`:批量行创建、复制、校验和失败结果归并的纯函数。 +- `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.test.ts`:纯函数单元测试。 +- `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form.vue`:批量新增弹窗、双视图和木箱类型选择组件接入。 + +### 前端修改 + +- `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts`:增加 `SUB_PACKAGE_STATUS: 'sub_package_status'`。 +- `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/subpackagerelation/index.ts`:增加批量请求/响应类型和 API 方法。 +- `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/index.vue`:增加批量弹窗和工具栏入口。 + +## 任务 1:定义后端批量契约和查询能力 + +**文件:** +- 创建:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/vo/SubPackageRelationBatchCreateReqVO.java` +- 创建:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/vo/SubPackageRelationBatchCreateItemReqVO.java` +- 创建:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/vo/SubPackageRelationBatchCreateRespVO.java` +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/subpackagerelation/SubPackageRelationMapper.java` +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/boxtype/BoxTypeMapper.java` + +- [ ] **步骤 1:创建独立批量请求实体** + +请求容器只对列表本身做 Jakarta Validation,行字段由 Service 手动校验,以便返回逐行错误而不是整次请求直接返回 400: + +```java +@Data +@Schema(description = "管理后台 - 子卷包装关系批量新增 Request VO") +public class SubPackageRelationBatchCreateReqVO { + + @NotEmpty(message = "批量新增数据不能为空") + @Schema(description = "批量新增数据", requiredMode = Schema.RequiredMode.REQUIRED) + private List items; +} +``` + +单条实体字段固定为: + +```java +@Data +@Schema(description = "管理后台 - 子卷包装关系批量新增单条数据") +public class SubPackageRelationBatchCreateItemReqVO { + private String clientKey; + private String packageBoxSn; + private String boxType; + private String boxLength; + private String boxWidth; + private String boxHigh; + private String qualityGuaranPeriod; + private String dateOfFgInbound; + private String containerName; + private String status; +} +``` + +- [ ] **步骤 2:创建结构化批量响应实体** + +```java +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "管理后台 - 子卷包装关系批量新增 Response VO") +public class SubPackageRelationBatchCreateRespVO { + private Integer successCount; + private Integer failureCount; + private List failures; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Failure { + private String clientKey; + private Integer rowIndex; + private String errorCode; + private String message; + } +} +``` + +- [ ] **步骤 3:增加 Mapper 查询方法** + +`BoxTypeMapper` 增加: + +```java +default List selectListByBoxTypes(Collection boxTypes) { + return selectList(BoxTypeDO::getBoxType, boxTypes); +} +``` + +`SubPackageRelationMapper` 增加: + +```java +default Long selectCountByPackageBoxSnAndBoxType(String packageBoxSn, String boxType) { + return selectCount(new LambdaQueryWrapper() + .eq(SubPackageRelationDO::getPackageBoxSn, packageBoxSn) + .eq(SubPackageRelationDO::getBoxType, boxType)); +} +``` + +- [ ] **步骤 4:编译后端契约** + +运行: + +```powershell +mvn -pl nl-module-lms/nl-module-lms-server -am -DskipTests compile +``` + +预期:`BUILD SUCCESS`,新 VO 和 Mapper 默认方法编译通过。 + +- [ ] **步骤 5:提交后端契约** + +```powershell +git add nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/vo nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/boxtype/BoxTypeMapper.java nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/subpackagerelation/SubPackageRelationMapper.java +git commit -m "feat: 定义子卷包装关系批量新增契约" +``` + +## 任务 2:测试并实现分组事务写入器 + +**文件:** +- 修改:`nl-module-lms/nl-module-lms-server/pom.xml` +- 创建:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationBatchSaveService.java` +- 创建:`nl-module-lms/nl-module-lms-server/src/test/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationBatchSaveServiceTest.java` + +- [ ] **步骤 1:增加 Mockito Inline 测试依赖** + +在 `pom.xml` 的测试依赖区增加: + +```xml + + org.mockito + mockito-inline + test + +``` + +- [ ] **步骤 2:编写失败测试** + +```java +@ExtendWith(MockitoExtension.class) +class SubPackageRelationBatchSaveServiceTest { + + @Mock + private SubPackageRelationMapper subPackageRelationMapper; + + @InjectMocks + private SubPackageRelationBatchSaveService service; + + @Test + void shouldInsertWholeGroup() { + List group = List.of( + SubPackageRelationDO.builder().packageBoxSn("BX001").boxType("A").build(), + SubPackageRelationDO.builder().packageBoxSn("BX001").boxType("A").build()); + + service.saveGroup(group); + + verify(subPackageRelationMapper).insertBatch(group); + } +} +``` + +- [ ] **步骤 3:运行测试确认失败** + +运行: + +```powershell +mvn -pl nl-module-lms/nl-module-lms-server -Dtest=SubPackageRelationBatchSaveServiceTest test +``` + +预期:FAIL,提示 `SubPackageRelationBatchSaveService` 不存在。 + +- [ ] **步骤 4:实现最小独立事务写入器** + +```java +@Service +public class SubPackageRelationBatchSaveService { + + @Resource + private SubPackageRelationMapper subPackageRelationMapper; + + @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) + public void saveGroup(List group) { + subPackageRelationMapper.insertBatch(group); + } +} +``` + +- [ ] **步骤 5:运行测试确认通过** + +运行同一步骤 3。预期:测试 PASS。 + +- [ ] **步骤 6:提交事务写入器** + +```powershell +git add nl-module-lms/nl-module-lms-server/pom.xml nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationBatchSaveService.java nl-module-lms/nl-module-lms-server/src/test/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationBatchSaveServiceTest.java +git commit -m "feat: 增加包装关系分组事务写入器" +``` + +## 任务 3:测试并实现批量业务编排 + +**文件:** +- 创建:`nl-module-lms/nl-module-lms-server/src/test/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationServiceImplTest.java` +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationService.java` +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationServiceImpl.java` + +- [ ] **步骤 1:编写容量刚好满足和超限整组失败测试** + +使用 `MockitoExtension` mock `SubPackageRelationMapper`、`BoxTypeMapper` 和 `SubPackageRelationBatchSaveService`。构造两组数据: + +```java +private SubPackageRelationBatchCreateItemReqVO item( + String clientKey, String packageBoxSn, String boxType, String containerName) { + SubPackageRelationBatchCreateItemReqVO item = new SubPackageRelationBatchCreateItemReqVO(); + item.setClientKey(clientKey); + item.setPackageBoxSn(packageBoxSn); + item.setBoxType(boxType); + item.setBoxLength("1200"); + item.setBoxWidth("1000"); + item.setBoxHigh("900"); + item.setQualityGuaranPeriod("180"); + item.setDateOfFgInbound("2026-08-17"); + item.setContainerName(containerName); + item.setStatus("0"); + return item; +} +``` + +核心断言: + +```java +when(boxTypeMapper.selectListByBoxTypes(Set.of("A"))) + .thenReturn(List.of(BoxTypeDO.builder().boxType("A").maxNum(3).build())); +when(subPackageRelationMapper.selectCountByPackageBoxSnAndBoxType("BX001", "A")) + .thenReturn(1L); + +SubPackageRelationBatchCreateRespVO result = service.batchCreateSubPackageRelation(req( + item("k1", "BX001", "A", "R1"), + item("k2", "BX001", "A", "R2")))); + +assertEquals(2, result.getSuccessCount()); +assertEquals(0, result.getFailureCount()); +verify(batchSaveService).saveGroup(argThat(group -> group.size() == 2)); +``` + +将已有数量改为 `2L` 后断言:成功数为 0、失败数为 2、`errorCode` 为 `BOX_CAPACITY_EXCEEDED`、两个 `clientKey` 都返回,且 `batchSaveService` 未被调用。 + +- [ ] **步骤 2:编写部分成功测试** + +构造 `BX001/A` 超限、`BX002/A` 合法两个分组,断言: + +```java +assertEquals(1, result.getSuccessCount()); +assertEquals(2, result.getFailureCount()); +assertEquals(Set.of("k1", "k2"), result.getFailures().stream() + .map(SubPackageRelationBatchCreateRespVO.Failure::getClientKey) + .collect(Collectors.toSet())); +verify(batchSaveService, times(1)).saveGroup(argThat(group -> + group.size() == 1 && "BX002".equals(group.get(0).getPackageBoxSn()))); +``` + +- [ ] **步骤 3:编写木箱类型不存在、必填失败和保存异常测试** + +分别断言: + +- 类型不存在返回 `BOX_TYPE_NOT_FOUND`,对应分组不保存。 +- 空 `packageBoxSn` 返回 `REQUIRED_FIELD_MISSING`,`rowIndex` 为请求中的一基行号。 +- `batchSaveService.saveGroup` 对一个合法分组抛出异常时,该组返回 `GROUP_SAVE_FAILED`,后续合法组仍调用保存器并计入成功数。 + +- [ ] **步骤 4:运行测试确认失败** + +运行: + +```powershell +mvn -pl nl-module-lms/nl-module-lms-server -Dtest=SubPackageRelationServiceImplTest test +``` + +预期:FAIL,提示批量方法和依赖尚不存在。 + +- [ ] **步骤 5:声明 Service 方法并注入依赖** + +`SubPackageRelationService` 增加: + +```java +SubPackageRelationBatchCreateRespVO batchCreateSubPackageRelation( + SubPackageRelationBatchCreateReqVO reqVO); +``` + +`SubPackageRelationServiceImpl` 注入 `BoxTypeMapper` 和 `SubPackageRelationBatchSaveService`。 + +- [ ] **步骤 6:实现逐行校验、分组与容量判断** + +实现顺序必须是: + +1. 遍历请求项,按原始下标生成一基 `rowIndex`。 +2. 校验 `clientKey`、`packageBoxSn`、`boxType`、`qualityGuaranPeriod`、`dateOfFgInbound`、`containerName`、`status` 非空;失败项加入 `failures`,不参与分组。 +3. 批量查询去重后的木箱类型并建立 `Map`。 +4. 按不可变分组键 `packageBoxSn + "\u0000" + boxType` 对合法项分组,同时保留每项的 `clientKey` 和 `rowIndex`。 +5. 类型不存在时整组追加 `BOX_TYPE_NOT_FOUND`。 +6. 查询相同木箱码和类型的数据库已有数量;当 `existing + group.size() > maxNum` 时整组追加 `BOX_CAPACITY_EXCEEDED`。 +7. 将合法项手动映射为 `SubPackageRelationDO`,只设置批量请求允许的业务字段。 + +不得使用 `BeanUtils.copyProperties`;使用显式 setter 或 builder 映射字段。 + +- [ ] **步骤 7:实现逐组隔离保存和响应汇总** + +逐个合法分组调用 `batchSaveService.saveGroup(group)`。成功时累加该组条数;捕获运行时异常时,为该组所有请求项追加 `GROUP_SAVE_FAILED`,然后继续处理下一组。 + +最终返回: + +```java +return SubPackageRelationBatchCreateRespVO.builder() + .successCount(successCount) + .failureCount(failures.size()) + .failures(failures) + .build(); +``` + +- [ ] **步骤 8:运行 Service 测试确认通过** + +运行同一步骤 4。预期:全部测试 PASS。 + +- [ ] **步骤 9:提交批量业务逻辑** + +```powershell +git add nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/subpackagerelation nl-module-lms/nl-module-lms-server/src/test/java/cn/code/nl/module/lms/service/subpackagerelation/SubPackageRelationServiceImplTest.java +git commit -m "feat: 实现包装关系分组批量保存" +``` + +## 任务 4:暴露后端批量创建接口 + +**文件:** +- 修改:`nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/SubPackageRelationController.java` + +- [ ] **步骤 1:增加 Controller 方法** + +```java +@PostMapping("/batch-create") +@Operation(summary = "批量创建子卷包装关系") +@PreAuthorize("@ss.hasPermission('lms:sub-package-relation:create')") +public CommonResult batchCreateSubPackageRelation( + @Valid @RequestBody SubPackageRelationBatchCreateReqVO reqVO) { + return success(subPackageRelationService.batchCreateSubPackageRelation(reqVO)); +} +``` + +- [ ] **步骤 2:运行 LMS Service 测试和编译** + +```powershell +mvn -pl nl-module-lms/nl-module-lms-server -Dtest=SubPackageRelationServiceImplTest,SubPackageRelationBatchSaveServiceTest test +mvn -pl nl-module-lms/nl-module-lms-server -am -DskipTests compile +``` + +预期:测试全部 PASS,编译 `BUILD SUCCESS`。 + +- [ ] **步骤 3:提交接口** + +```powershell +git add nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/subpackagerelation/SubPackageRelationController.java +git commit -m "feat: 暴露包装关系批量新增接口" +``` + +## 任务 5:定义前端字典、类型和 API + +**文件:** +- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts` +- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/subpackagerelation/index.ts` + +- [ ] **步骤 1:增加 LMS 状态字典常量** + +在 `LMS_DICT` 中增加: + +```ts +SUB_PACKAGE_STATUS: 'sub_package_status', // 子卷包装关系状态 +``` + +- [ ] **步骤 2:增加批量请求和响应类型** + +```ts +export interface BatchCreateItem { + clientKey: string; + packageBoxSn: string; + boxType: string; + boxLength?: string; + boxWidth?: string; + boxHigh?: string; + qualityGuaranPeriod: string; + dateOfFgInbound: string; + containerName: string; + status: string; +} + +export interface BatchCreateFailure { + clientKey: string; + rowIndex: number; + errorCode: string; + message: string; +} + +export interface BatchCreateResult { + successCount: number; + failureCount: number; + failures: BatchCreateFailure[]; +} +``` + +- [ ] **步骤 3:增加批量 API 方法** + +```ts +export function batchCreateSubPackageRelation( + items: LmsSubPackageRelationApi.BatchCreateItem[], +) { + return requestClient.post( + '/lms/sub-package-relation/batch-create', + { items }, + ); +} +``` + +- [ ] **步骤 4:运行目标应用类型检查并记录基线** + +```powershell +pnpm -F @vben/web-antdv-next typecheck +``` + +预期:PASS;若现有工程基线报错,保存完整输出,后续验证只允许新增文件不增加错误。 + +- [ ] **步骤 5:提交前端契约** + +```powershell +git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/packages/constants/src/dict-enum.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/subpackagerelation/index.ts +git commit -m "feat: 增加包装关系批量新增前端契约" +``` + +## 任务 6:测试并实现批量行模型 + +**文件:** +- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.ts` +- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.test.ts` + +- [ ] **步骤 1:编写失败的纯函数测试** + +测试必须覆盖: + +```ts +describe('batch form model', () => { + it('creates a blank row with status 0 and a stable key', () => { + const row = createBatchRow(); + expect(row.status).toBe('0'); + expect(row.clientKey).toBeTruthy(); + }); + + it('copies reusable values and clears unique identifiers', () => { + const copied = copyBatchRow(sourceRow); + expect(copied.boxType).toBe(sourceRow.boxType); + expect(copied.boxLength).toBe(sourceRow.boxLength); + expect(copied.packageBoxSn).toBe(''); + expect(copied.containerName).toBe(''); + expect(copied.clientKey).not.toBe(sourceRow.clientKey); + }); + + it('returns field errors for every incomplete row', () => { + const errors = validateBatchRows([createBatchRow()]); + expect(errors.get(0)).toEqual(expect.objectContaining({ + packageBoxSn: expect.any(String), + boxType: expect.any(String), + containerName: expect.any(String), + })); + }); + + it('keeps only backend failure rows and maps messages by clientKey', () => { + const result = applyBatchResult(rows, { + successCount: 1, + failureCount: 1, + failures: [{ clientKey: rows[1]!.clientKey, rowIndex: 2, errorCode: 'BOX_CAPACITY_EXCEEDED', message: '容量超限' }], + }); + expect(result.rows).toHaveLength(1); + expect(result.rows[0]!.clientKey).toBe(rows[1]!.clientKey); + expect(result.rowErrors.get(rows[1]!.clientKey)).toBe('容量超限'); + }); +}); +``` + +- [ ] **步骤 2:运行测试确认失败** + +在前端仓库根目录运行: + +```powershell +pnpm exec vitest run apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.test.ts +``` + +预期:FAIL,提示模型模块或导出函数不存在。 + +- [ ] **步骤 3:实现最小批量行模型** + +导出以下稳定类型和函数: + +```ts +export interface BatchFormRow extends LmsSubPackageRelationApi.BatchCreateItem {} +export type BatchFieldErrors = Partial>; + +export function createBatchRow(): BatchFormRow; +export function copyBatchRow(source: BatchFormRow): BatchFormRow; +export function validateBatchRows(rows: BatchFormRow[]): Map; +export function applyBatchResult( + rows: BatchFormRow[], + result: LmsSubPackageRelationApi.BatchCreateResult, +): { rowErrors: Map; rows: BatchFormRow[] }; +``` + +`clientKey` 使用 `crypto.randomUUID()`;如果运行环境不可用,则使用时间戳与自增序号组合的模块内 fallback。必填错误信息使用明确中文字段名。 + +- [ ] **步骤 4:运行测试确认通过** + +运行同一步骤 2。预期:测试 PASS。 + +- [ ] **步骤 5:提交批量行模型** + +```powershell +git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.test.ts +git commit -m "feat: 增加包装关系批量录入模型" +``` + +## 任务 7:实现卡片/表格自由切换弹窗 + +**文件:** +- 创建:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form.vue` + +- [ ] **步骤 1:建立弹窗状态和木箱类型选择接入** + +组件状态固定为: + +```ts +const emit = defineEmits<{ success: [] }>(); +const rows = ref([createBatchRow()]); +const viewMode = ref<'card' | 'table'>('table'); +const activeClientKey = ref(); +const fieldErrors = ref(new Map()); +const rowErrors = ref(new Map()); + +const [BoxTypeSelectModal, boxTypeSelectModalApi] = useVbenModal({ + connectedComponent: BoxTypeSelectModalComponent, +}); +``` + +点击任一行木箱类型时设置 `activeClientKey` 并以 `{ multiple: false }` 打开选择弹窗;`select` 回调按 `clientKey` 精确回填 `boxType`、`boxLength`、`boxWidth`、`boxHigh`。 + +- [ ] **步骤 2:实现行操作和双视图共享数据** + +新增、复制、删除、清空都只修改 `rows`。删除最后一条或清空时恢复 `[createBatchRow()]`。视图切换只修改 `viewMode`。 + +卡片视图按记录渲染两栏表单;表格视图使用原生 `table` 或 Antdv Next `Table` 的横向可编辑布局。两种视图必须包含相同字段和相同 `v-model` 数据源,尺寸字段只读。 + +- [ ] **步骤 3:接入状态字典和日期控件** + +```ts +const statusOptions = getDictOptions(DICT_TYPE.SUB_PACKAGE_STATUS); +``` + +状态使用 `Select`,新记录默认 `'0'`。入库日期使用日期控件并在请求前格式化为 `YYYY-MM-DD`;不得把 Dayjs 对象传给后端。 + +- [ ] **步骤 4:实现提交和部分成功处理** + +确认按钮逻辑: + +```ts +const validation = validateBatchRows(rows.value); +if (validation.size > 0) { + fieldErrors.value = validation; + message.warning('请检查必填项'); + return; +} + +const result = await batchCreateSubPackageRelation(rows.value); +const applied = applyBatchResult(rows.value, result); +if (result.successCount > 0) emit('success'); +if (result.failureCount === 0) { + await modalApi.close(); + message.success(`成功新增 ${result.successCount} 条子卷包装关系`); + return; +} +rows.value = applied.rows; +rowErrors.value = applied.rowErrors; +message.warning(`成功 ${result.successCount} 条,失败 ${result.failureCount} 条`); +``` + +`finally` 中解锁弹窗。关闭弹窗时重置所有状态,防止下次打开残留数据和错误。 + +- [ ] **步骤 5:执行组件静态检查** + +```powershell +pnpm exec eslint apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form.vue apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.ts +pnpm -F @vben/web-antdv-next typecheck +``` + +预期:无新增 ESLint 或 TypeScript 错误。 + +- [ ] **步骤 6:提交批量弹窗** + +```powershell +git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form.vue +git commit -m "feat: 实现包装关系批量新增弹窗" +``` + +## 任务 8:接入列表入口并完成联调验证 + +**文件:** +- 修改:`nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/index.vue` + +- [ ] **步骤 1:接入批量弹窗** + +```ts +import BatchForm from './modules/batch-form.vue'; + +const [BatchFormModal, batchFormModalApi] = useVbenModal({ + connectedComponent: BatchForm, + destroyOnClose: true, +}); + +function handleBatchCreate() { + batchFormModalApi.open(); +} +``` + +模板中增加: + +```vue + +``` + +- [ ] **步骤 2:增加工具栏按钮** + +在普通新增按钮后增加: + +```ts +{ + label: '批量新增', + type: 'primary', + icon: ACTION_ICON.ADD, + auth: ['lms:sub-package-relation:create'], + onClick: handleBatchCreate, +}, +``` + +- [ ] **步骤 3:运行后端定向测试和编译** + +```powershell +mvn -pl nl-module-lms/nl-module-lms-server -Dtest=SubPackageRelationServiceImplTest,SubPackageRelationBatchSaveServiceTest test +mvn -pl nl-module-lms/nl-module-lms-server -am -DskipTests compile +``` + +预期:测试 PASS,编译 `BUILD SUCCESS`。 + +- [ ] **步骤 4:运行前端测试和检查** + +```powershell +pnpm exec vitest run apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.test.ts +pnpm exec eslint apps/web-antdv-next/src/views/lms/subpackagerelation/index.vue apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form.vue apps/web-antdv-next/src/views/lms/subpackagerelation/modules/batch-form-model.ts +pnpm -F @vben/web-antdv-next typecheck +``` + +预期:测试 PASS,目标文件静态检查通过,类型检查无新增错误。 + +- [ ] **步骤 5:手工联调验收** + +按以下顺序检查: + +1. 列表页能打开批量弹窗。 +2. 新增两行,在表格输入后切到卡片,数据保持一致。 +3. 选择木箱类型,确认回填目标行和尺寸。 +4. 确认状态选项来自 `sub_package_status`,新增行默认 `0`。 +5. 提交一个合法组,弹窗关闭且列表刷新。 +6. 提交一个超限组和一个合法组,合法组保存,超限组保留并逐行显示容量错误。 +7. 修改失败行后再次提交,成功后弹窗关闭。 + +- [ ] **步骤 6:提交列表接入** + +```powershell +git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/subpackagerelation/index.vue +git commit -m "feat: 接入包装关系批量新增入口" +``` + +## 任务 9:最终一致性检查 + +**文件:** +- 检查本计划列出的全部新增和修改文件。 + +- [ ] **步骤 1:检查工作区变更范围** + +```powershell +git status --short +git diff --check +git diff --stat +``` + +预期:没有空白错误;不修改用户已有的配置、原型和其他计划文件。 + +- [ ] **步骤 2:核对规格覆盖** + +逐项确认:独立接口、独立 VO、原型字段范围、木箱类型弹窗、尺寸回填、状态字典默认值、双视图共享数据、容量统计包含数据库已有数量、超限整组失败、合法组独立保存、逐行错误展示全部已实现。 + +- [ ] **步骤 3:记录验证证据** + +交付说明必须列出实际运行的每条命令、退出码和关键结果;如果检查被现有依赖或工具链阻塞,明确给出原始错误摘要,不得声称对应检查通过。 diff --git a/docs/superpowers/plans/2026-08-17-sub-package-relation-batch.md b/docs/superpowers/plans/2026-08-17-sub-package-relation-batch.md new file mode 100644 index 00000000..c49c0db7 --- /dev/null +++ b/docs/superpowers/plans/2026-08-17-sub-package-relation-batch.md @@ -0,0 +1,133 @@ +# 子卷包装关系批量新增 PC 原型实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 创建一个符合现有 Vben Admin + Ant Design 风格、可在 PC 上完整演示批量新增子卷包装关系流程的单文件 HTML 原型。 + +**架构:** 使用一个自包含 HTML 文件承载页面结构、CSS 设计令牌与原生 JavaScript 状态管理。以行数组作为唯一数据源,集中实现渲染、木箱尺寸联动、复制/删除、校验和本地模拟提交,避免远程依赖与构建步骤。 + +**技术栈:** HTML5、CSS3、原生 JavaScript、Playwright 或可用浏览器工具进行交互验证。 + +--- + +## 文件结构 + +- 创建:`docs/prototype_design/design/2026-08-17-sub-package-relation-batch-design.html`——完整页面、弹窗、样式、模拟数据和交互逻辑。 +- 修改:`docs/superpowers/plans/2026-08-17-sub-package-relation-batch.md`——执行后勾选任务状态。 + +### 任务 1:建立页面骨架与视觉体系 + +**文件:** +- 创建:`docs/prototype_design/design/2026-08-17-sub-package-relation-batch-design.html` + +- [x] **步骤 1:定义可验证的静态结构约束** + +确认 HTML 必须包含 `#batchModal`、`#editableBody`、`#addRowBtn`、`#clearBtn`、`#cancelBtn` 和 `#submitBtn`,并包含 11 个业务字段对应的输入控件生成逻辑。 + +- [x] **步骤 2:创建完整后台页面骨架** + +实现侧边栏、顶栏、面包屑、查询区、子卷包装关系列表和遮罩层,让弹窗处于真实页面上下文中。 + +- [x] **步骤 3:实现 Ant Design 风格令牌** + +在 `:root` 中定义主色 `#1677ff`、边框 `#d9d9d9`、页面底色 `#f5f5f5`、正文色 `#1f2329`、圆角和阴影;实现按钮、输入框、表格、提示条、弹窗及键盘聚焦状态。 + +- [x] **步骤 4:验证静态结构** + +运行: + +```powershell +rg -n 'batchModal|editableBody|addRowBtn|clearBtn|cancelBtn|submitBtn' docs/prototype_design/design/2026-08-17-sub-package-relation-batch-design.html +``` + +预期:六个关键 ID 均至少出现一次。 + +### 任务 2:实现批量录入核心交互 + +**文件:** +- 修改:`docs/prototype_design/design/2026-08-17-sub-package-relation-batch-design.html` + +- [x] **步骤 1:定义行数据与木箱类型** + +定义 `rows` 数组、递增行键和以下演示字典: + +```js +const boxTypes = { + 'BX-A': { length: 1200, width: 1000, high: 900 }, + 'BX-B': { length: 1100, width: 900, high: 850 }, + 'BX-C': { length: 800, width: 600, high: 600 }, +}; +``` + +- [x] **步骤 2:实现表格渲染与字段绑定** + +每行渲染 `package_box_sn`、`box_weight`、`quality_guaran_period`、`sale_order_name`、`customer_name`、`date_of_fg_inbound`、`container_name`、`box_type`、`box_length`、`box_width`、`box_high`,其中尺寸字段只读。 + +- [x] **步骤 3:实现木箱类型联动** + +监听 `box_type` 变化,将选中类型对应的长、宽、高写入当前行;清空类型时同步清空三个尺寸并重新渲染统计。 + +- [x] **步骤 4:实现行级操作** + +实现新增、复制和删除。复制时保留公共数据并清空 `package_box_sn`、`container_name`;删除到最后一行时用新空行替换。 + +- [x] **步骤 5:实现批量清空** + +点击“清空全部”弹出确认层,确认后恢复 3 个空行,取消后保持原数据。 + +### 任务 3:实现校验、提交与关闭流程 + +**文件:** +- 修改:`docs/prototype_design/design/2026-08-17-sub-package-relation-batch-design.html` + +- [x] **步骤 1:实现逐行必填校验** + +要求七个可编辑业务字段和 `box_type` 均有值,`box_weight` 必须大于 0;错误记录为 `{ rowKey, field, message }` 并映射到对应单元格。 + +- [x] **步骤 2:实现重复值校验** + +使用两个 `Map` 检查 `package_box_sn` 和 `container_name`,第二次出现及其首次出现位置都标红,并显示“与第 N 行重复”。 + +- [x] **步骤 3:实现错误摘要与定位** + +提交失败时显示顶部红色摘要,更新错误数量,并聚焦首个无效控件;输入修正后清除当前字段错误。 + +- [x] **步骤 4:实现本地模拟提交** + +校验通过后显示加载状态,短暂延迟后关闭弹窗、向背景列表追加记录,并显示“成功新增 N 条子卷包装关系”。 + +- [x] **步骤 5:实现关闭保护与重新打开** + +有内容时点击取消、关闭或遮罩显示放弃确认;空表直接关闭。背景页“批量新增”按钮重新打开弹窗并恢复 3 行。 + +### 任务 4:浏览器验证与视觉修正 + +**文件:** +- 修改:`docs/prototype_design/design/2026-08-17-sub-package-relation-batch-design.html` +- 修改:`docs/superpowers/plans/2026-08-17-sub-package-relation-batch.md` + +- [x] **步骤 1:以目标视口打开原型** + +使用 1440 × 900 视口加载本地 HTML,确认弹窗未超出视口,表格只在内容区内部滚动,底部按钮始终可见。 + +- [x] **步骤 2:走通核心流程** + +依次操作新增行、选择 BX-B、确认尺寸为 1100 × 900 × 850、复制行、删除行、清空取消、空表提交校验、重复值校验和有效提交。 + +- [x] **步骤 3:检查运行错误** + +确认控制台无 JavaScript 错误、无远程资源失败、所有按钮可点击、Tab 键焦点可见。 + +- [x] **步骤 4:修正视觉问题** + +重点检查表头分组、冻结列、横向滚动、错误提示、背景遮罩、弹窗间距和 12 至 14px 的信息密度,修复裁切或重叠。 + +- [x] **步骤 5:完成静态检查** + +运行: + +```powershell +rg -n 'package_box_sn|box_weight|quality_guaran_period|sale_order_name|customer_name|date_of_fg_inbound|container_name|box_type|box_length|box_width|box_high' docs/prototype_design/design/2026-08-17-sub-package-relation-batch-design.html +``` + +预期:11 个字段名全部存在;HTML 无远程 `http://` 或 `https://` 依赖。 diff --git a/docs/superpowers/specs/2026-08-12-auto-send-sub-roll-task-design.md b/docs/superpowers/specs/2026-08-12-auto-send-sub-roll-task-design.md new file mode 100644 index 00000000..70a342ae --- /dev/null +++ b/docs/superpowers/specs/2026-08-12-auto-send-sub-roll-task-design.md @@ -0,0 +1,25 @@ +# 子卷下线送待检区任务设计 + +## 目标 + +定时扫描未锁定的子卷下线点,为每个点位分配一个未锁定的待检区点位,并在后续创建运输任务前锁定起点和目的点。 + +## 点位筛选 + +- 起点:`point_type = '1'`、`point_status = '3'`、`lock_status = '0'`、`deleted = 0`。 +- 目的点:`point_type = '2'`、`point_status = '1'`、`lock_status = '0'`、`deleted = 0`。 +- 两类点位均按 `sort_seq`、`point_id` 升序处理,保证分配顺序稳定。 + +## 并发与事务 + +业务方法在一个数据库事务中运行。点位加锁使用带原状态条件的更新:仅当 `lock_status = '0'` 时更新为 `'1'`,并以更新行数判断是否抢锁成功。 + +每个起点先尝试抢锁,再依次尝试目的点。目的点抢锁失败时继续尝试下一个;没有目的点可用时,将本次已抢到的起点恢复为未锁定并结束本轮扫描。运行期间发生异常时由事务回滚本轮全部数据库修改。 + +## 任务创建 + +起点和目的点都锁定成功后,通过 `TransportTaskApi.createTransportTask` 创建运输任务。任务归属为 LMS,业务类型为 `SUB_ROLL_DOWN`,业务标识及物料编码使用子卷号,取放货点使用起点和目的点编码,回调处理器为 `subRollDownAgvTask`。任务创建失败时抛出异常,使点位锁随当前事务回滚。 + +## 调用边界 + +`BoxAutoJob` 只负责日志和调用 `PackageBoxBusinessService`。点位查询、配对、条件加锁和必要的解锁均由 `PackageBoxBusinessServiceImpl` 组织,数据库语句集中在 `PackagePointMapper.xml`。 diff --git a/docs/superpowers/specs/2026-08-13-auto-sub-roll-down-call-empty-design.md b/docs/superpowers/specs/2026-08-13-auto-sub-roll-down-call-empty-design.md new file mode 100644 index 00000000..4cea6297 --- /dev/null +++ b/docs/superpowers/specs/2026-08-13-auto-sub-roll-down-call-empty-design.md @@ -0,0 +1,28 @@ +# 子卷下线补充空架子设计 + +## 目标 + +定时扫描装箱区域点位,为子卷下线空位优先补充装箱点空载具;装箱点不足时使用待检点空载具补充;下线点无剩余空位后,将装箱点剩余空载具转运到待检点空位。 + +## 分配顺序 + +1. 目的点为未锁定的下线点空位,起点优先选择未锁定的装箱点空载具。 +2. 当前下线点无法取得装箱点空载具时,改用未锁定的待检点空载具。 +3. 下线点候选处理完后,将剩余的装箱点空载具与待检点空位配对。 +4. 各类候选点均沿用 `sort_seq`、`point_id` 升序,保证任务创建顺序稳定。 + +## 并发与任务创建 + +每次配对先条件锁定目的点,再按优先级条件锁定起点。锁定和释放都同时校验候选查询时的点位状态,避免查询与更新之间的空档窗口内状态已变化却仍然更新成功。抢锁失败时继续尝试下一候选点;没有可用起点时释放已锁定的目的点。起点和目的点均锁定后,通过 `TransportTaskApi` 创建 AGV 运输任务,回调处理器为 `subRollCallEmptyAgvTask`。业务方法运行在事务中,任务创建异常时回滚点位锁。 + +空架任务以载具号作为业务标识和载具编码,不设置物料编码。任务类型新增为子卷下线补空架 AGV 任务。 + +## 回调状态 + +- 取货完成:起点改为空位、清空载具和容器信息并解锁。 +- 任务完成:目的点改为空载具、写入载具号、清空容器信息并解锁。 +- 任务取消:起点和目的点仅解锁,保留原有库存状态。 + +## 调用边界 + +`BoxAutoJob` 只记录开始/结束日志并调用 `PackageBoxBusinessService`。点位筛选、抢锁、分配和任务创建均放在 `PackageBoxBusinessServiceImpl`;任务状态回写放在 `SubRollCallEmptyAgvTask`。 diff --git a/docs/superpowers/specs/2026-08-14-packing-area-dashboard-design.md b/docs/superpowers/specs/2026-08-14-packing-area-dashboard-design.md new file mode 100644 index 00000000..46fd9f05 --- /dev/null +++ b/docs/superpowers/specs/2026-08-14-packing-area-dashboard-design.md @@ -0,0 +1,41 @@ +# 装箱区信息看板设计规格 + +## 目标 + +创建一个无需接口和构建工具即可直接打开的独立 HTML Demo。页面延续 `index4.html` 的点位信息、搜索定位、缩放拖动和右键复制功能,并采用 `rollInfoDashboard.html` 的深色科技大屏视觉。 + +## 页面结构 + +- 固定 1920 × 1080 设计舞台,随浏览器窗口等比缩放。 +- 顶部包含品牌标识、看板标题、当前日期时间和演示数据标识。 +- 第二行包含总点位、占用、空闲、超时、占用率五项 KPI。 +- 工具条包含状态图例、卷号搜索、超时阈值、地图缩放比例和复位按钮。 +- 主体左侧为平面布置图,右侧为装箱队列和当前选中点位详情。 + +## 平面布局 + +- 北区为一排 6 个暂存点位:`ZJXX_01` 至 `ZJXX_06`。 +- 中区为 7 排 6 列待检点位:`DJQ_01` 至 `DJQ_42`,按从上到下、从左到右顺序编号。 +- 南区第一排为装箱区 1:`ZXQ_01` 至 `ZXQ_08`,右侧为 `ZXDJW_01`。 +- 南区第二排为装箱区 2:`ZXQ_09` 至 `ZXQ_16`,右侧为 `ZXDJW_02`。 +- 区域间和点位组之间使用白色发光线表示 AGV 通道,配合方向箭头表达行驶路径。 + +## 点位数据与状态 + +每个点位使用确定性的本地模拟数据,展示点位编码、点位名称、库存状态、卷号、箱号、进入时间和停留时长。状态包含空闲、空载具、有子卷、合格品和管制品;超过用户设置阈值的占用点位显示红色超时效果。 + +## 交互 + +- 输入完整或部分卷号后定位并高亮匹配点位。 +- 鼠标滚轮缩放地图,按住左键拖动,双击或点击复位按钮恢复适配视图。 +- 单击点位更新右侧详情。 +- 右键点位弹出菜单,可复制卷号或箱号;不可用的复制项禁用。 +- 超时阈值和缩放比例即时更新,并保存在浏览器本地。 + +## 验证标准 + +- HTML 可直接打开且不访问任何 API 或外部资源。 +- 页面包含 6 个北区点位、42 个中区点位、16 个装箱区点位和 2 个装箱等待位,共 66 个点位。 +- 1920 × 1080 下无主体溢出,缩小窗口时保持等比展示。 +- 搜索、选中、缩放、拖动、复位和右键菜单可以正常使用。 + diff --git a/nl-module-lms/nl-module-lms-api/src/main/java/cn/code/nl/module/lms/enums/ErrorCodeConstants.java b/nl-module-lms/nl-module-lms-api/src/main/java/cn/code/nl/module/lms/enums/ErrorCodeConstants.java index 76dfc98a..ba08f456 100644 --- a/nl-module-lms/nl-module-lms-api/src/main/java/cn/code/nl/module/lms/enums/ErrorCodeConstants.java +++ b/nl-module-lms/nl-module-lms-api/src/main/java/cn/code/nl/module/lms/enums/ErrorCodeConstants.java @@ -59,6 +59,8 @@ public interface ErrorCodeConstants { ErrorCode PACKAGE_POINT_INSPECTION_PENDING_EMPTY_NOT_EXISTS = new ErrorCode(200006, "没有可用的待检空位"); ErrorCode PACKAGE_POINT_QUALITY_TASK_CREATE_FAILED = new ErrorCode(200007, "创建质检合格AGV任务失败"); ErrorCode POINT_EXECUTE_REFUGE = new ErrorCode(200008, "{}的点位不允许操作{}"); + ErrorCode PACKAGE_POINT_UNQUALIFIED_EMPTY_NOT_EXISTS = new ErrorCode(200009, "没有可用的不合格空位"); + ErrorCode PACKAGE_POINT_CHECK_FAILED_TASK_CREATE_FAILED = new ErrorCode(200010, "创建质检不合格AGV任务失败"); // ========== 子卷包装关系 ========== ErrorCode SUB_PACKAGE_RELATION_NOT_EXISTS = new ErrorCode(300001, "子卷包装关系不存在"); diff --git a/nl-module-lms/nl-module-lms-api/src/main/java/cn/code/nl/module/lms/enums/PackageTaskTypeEnum.java b/nl-module-lms/nl-module-lms-api/src/main/java/cn/code/nl/module/lms/enums/PackageTaskTypeEnum.java index 85849516..8ad86277 100644 --- a/nl-module-lms/nl-module-lms-api/src/main/java/cn/code/nl/module/lms/enums/PackageTaskTypeEnum.java +++ b/nl-module-lms/nl-module-lms-api/src/main/java/cn/code/nl/module/lms/enums/PackageTaskTypeEnum.java @@ -14,6 +14,7 @@ public enum PackageTaskTypeEnum { PACKAGE_AGV("010603", "装箱AGV任务"), PACKAGE_BOX_TRUSS("010604", "子卷装箱桁架任务"), RE_CHECK_QUALIFIED_AGV("010605", "复检合格AGV任务"), + CHECK_FAILED_AGV("010606", "质检不合格AGV任务"), ; /** * 字典键值 diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackagePdaServiceImpl.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackagePdaServiceImpl.java index 7713aaa5..7c2d2fee 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackagePdaServiceImpl.java +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/packagepoint/PackagePdaServiceImpl.java @@ -58,6 +58,12 @@ public class PackagePdaServiceImpl implements PackagePdaService { /** 复检合格任务回调处理器编码 */ private static final String RE_CHECK_QUALIFIED_HANDLE_CODE = "reCheckQualifiedAgvTask"; + /** 质检不合格业务类型 */ + private static final String CHECK_FAILED_BIZ_TYPE = "CHECK_FAILED"; + + /** 质检不合格任务回调处理器编码 */ + private static final String CHECK_FAILED_HANDLE_CODE = "checkFailedAgvTask"; + /** ACS 搬运任务类型 */ private static final String ACS_MOVE_TASK_TYPE = "7"; @@ -152,7 +158,7 @@ public class PackagePdaServiceImpl implements PackagePdaService { if (manaQualityCheckPdaReqVO.getIsQualified()) { doCheckedQualified(manaQualityCheckPdaReqVO); } else { - // todo: 做不合格的操作 + doCheckedFailed(manaQualityCheckPdaReqVO); } } catch (ServiceException ex) { throw ex; @@ -169,6 +175,11 @@ public class PackagePdaServiceImpl implements PackagePdaService { } } + /** + * 执行人工质检合格操作 + * + * @param manaQualityCheckPdaReqVO 人工质检参数 + */ public void doCheckedQualified(ManaQualityCheckPdaReqVO manaQualityCheckPdaReqVO) { PackagePointDO currentPoint = packagePointService.getPackagePointByCode( manaQualityCheckPdaReqVO.getPointCode(), false); @@ -241,4 +252,69 @@ public class PackagePdaServiceImpl implements PackagePdaService { log.info("复检合格AGV任务创建成功,任务标识={},起点编码={},目的点编码={}", taskResult.getData(), currentPoint.getPointCode(), destinationPoint.getPointCode()); } + + /** + * 执行人工质检不合格操作 + * + * @param manaQualityCheckPdaReqVO 人工质检参数 + */ + private void doCheckedFailed(ManaQualityCheckPdaReqVO manaQualityCheckPdaReqVO) { + PackagePointDO currentPoint = packagePointService.getPackagePointByCode( + manaQualityCheckPdaReqVO.getPointCode(), false); + if (currentPoint == null) { + throw exception(PACKAGE_POINT_NOT_EXISTS); + } + if (!BoxPointTypeEnum.INSPECTION_PENDING_POINT.getCode().equals(currentPoint.getPointType())) { + throw exception(POINT_EXECUTE_REFUGE, + Objects.requireNonNull(BoxPointTypeEnum.getByCode(currentPoint.getPointType())).getDesc(), + "质检不合格"); + } + + if (packagePointMapper.tryLockPoint( + currentPoint.getPointId(), currentPoint.getPointStatus()) == 0) { + throw exception(PACKAGE_POINT_LOCK_FAILED); + } + + List unqualifiedPointList = packagePointMapper.selectAvailablePointList( + BoxPointTypeEnum.UNQUALIFIED_POINT.getCode(), BoxPointStatusEnum.EMPTY.getCode()); + if (CollectionUtils.isEmpty(unqualifiedPointList)) { + throw exception(PACKAGE_POINT_UNQUALIFIED_EMPTY_NOT_EXISTS); + } + + PackagePointDO destinationPoint = null; + for (PackagePointDO unqualifiedPoint : unqualifiedPointList) { + if (packagePointMapper.tryLockPoint( + unqualifiedPoint.getPointId(), unqualifiedPoint.getPointStatus()) > 0) { + destinationPoint = unqualifiedPoint; + break; + } + } + if (destinationPoint == null) { + throw exception(PACKAGE_POINT_UNQUALIFIED_EMPTY_NOT_EXISTS); + } + + TransportTaskCreateReqDTO task = new TransportTaskCreateReqDTO(); + task.setTaskName("质检不合格AGV任务"); + task.setOwnerService(TaskOwnerServiceEnum.LMS.name()); + task.setBizType(CHECK_FAILED_BIZ_TYPE); + task.setBizId(currentPoint.getContainerName()); + task.setTaskType(PackageTaskTypeEnum.CHECK_FAILED_AGV.getCode()); + task.setHandleCode(CHECK_FAILED_HANDLE_CODE); + task.setAcsTaskType(ACS_MOVE_TASK_TYPE); + task.setAgvSystemType(AGV_SYSTEM_TYPE); + task.setPointCode1(currentPoint.getPointCode()); + task.setPointCode2(destinationPoint.getPointCode()); + task.setVehicleCode(currentPoint.getVehicleCode()); + task.setMaterialCode(currentPoint.getContainerName()); + task.setProductArea(currentPoint.getProductArea()); + task.setIsAutoIssue("1"); + task.setIsCreateFinish(true); + CommonResult taskResult = transportTaskApi.createTransportTask(task); + if (taskResult.isError()) { + log.error("创建质检不合格AGV任务失败,task={},返回={}", task, taskResult); + throw exception(PACKAGE_POINT_CHECK_FAILED_TASK_CREATE_FAILED); + } + log.info("质检不合格AGV任务创建成功,任务标识={},起点编码={},目的点编码={}", + taskResult.getData(), currentPoint.getPointCode(), destinationPoint.getPointCode()); + } } diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/tasks/box/CheckFailedAgvTask.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/tasks/box/CheckFailedAgvTask.java new file mode 100644 index 00000000..cf8cba26 --- /dev/null +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/tasks/box/CheckFailedAgvTask.java @@ -0,0 +1,94 @@ +package cn.code.nl.module.lms.tasks.box; + +import cn.code.nl.framework.execute.core.AbstractTask; +import cn.code.nl.framework.execute.core.dto.TaskExecuteDTO; +import cn.code.nl.module.lms.dal.dataobject.packagepoint.PackagePointDO; +import cn.code.nl.module.lms.dal.mysql.packagepoint.PackagePointMapper; +import cn.code.nl.module.lms.enums.BoxPointStatusEnum; +import cn.code.nl.module.task.api.TransportTaskApi; +import cn.code.nl.module.task.dto.TaskInfoDTO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 质检不合格 AGV 任务 + * + * @Author: liyongde + * @Date: 2026/8/18 + */ +@Slf4j +@Component("checkFailedAgvTask") +public class CheckFailedAgvTask extends AbstractTask { + + /** 运输任务接口 */ + @Resource + private TransportTaskApi transportTaskApi; + + /** 装箱区点位 Mapper */ + @Resource + private PackagePointMapper packagePointMapper; + + /** + * 任务完成后将目的点更新为管制品 + * + * @param taskExecuteDTO 任务执行信息 + */ + @Override + public void doHandleFinish(TaskExecuteDTO taskExecuteDTO) { + TaskInfoDTO taskInfoDTO = transportTaskApi.getTaskById(taskExecuteDTO.getTaskId()).getCheckedData(); + PackagePointDO endPoint = packagePointMapper.selectByPointCode(taskInfoDTO.getPointCode2(), false); + PackagePointDO updatePoint = new PackagePointDO(); + updatePoint.setPointId(endPoint.getPointId()); + updatePoint.setVehicleCode(taskInfoDTO.getVehicleCode()); + updatePoint.setContainerName(taskInfoDTO.getMaterialCode()); + updatePoint.setPointStatus(BoxPointStatusEnum.CONTROLLED.getCode()); + updatePoint.setLockStatus("0"); + packagePointMapper.updateById(updatePoint); + log.info("质检不合格任务{}执行完成,管制品已存入不合格点{}", + taskExecuteDTO.getTaskId(), taskInfoDTO.getPointCode2()); + } + + /** + * 任务取消后解锁起点和目的点 + * + * @param taskExecuteDTO 任务执行信息 + */ + @Override + public void doHandleCancel(TaskExecuteDTO taskExecuteDTO) { + TaskInfoDTO taskInfoDTO = transportTaskApi.getTaskById(taskExecuteDTO.getTaskId()).getCheckedData(); + PackagePointDO startPoint = packagePointMapper.selectByPointCode(taskInfoDTO.getPointCode1(), false); + PackagePointDO endPoint = packagePointMapper.selectByPointCode(taskInfoDTO.getPointCode2(), false); + PackagePointDO updateStartPoint = new PackagePointDO(); + updateStartPoint.setPointId(startPoint.getPointId()); + updateStartPoint.setLockStatus("0"); + PackagePointDO updateEndPoint = new PackagePointDO(); + updateEndPoint.setPointId(endPoint.getPointId()); + updateEndPoint.setLockStatus("0"); + packagePointMapper.updateBatch(List.of(updateStartPoint, updateEndPoint)); + log.info("质检不合格任务{}已取消,起点{}和目的点{}已解锁", taskExecuteDTO.getTaskId(), + taskInfoDTO.getPointCode1(), taskInfoDTO.getPointCode2()); + } + + /** + * 取货完成后释放待检点 + * + * @param taskExecuteDTO 任务执行信息 + */ + @Override + public void doHandlePicked(TaskExecuteDTO taskExecuteDTO) { + TaskInfoDTO taskInfoDTO = transportTaskApi.getTaskById(taskExecuteDTO.getTaskId()).getCheckedData(); + PackagePointDO startPoint = packagePointMapper.selectByPointCode(taskInfoDTO.getPointCode1(), false); + PackagePointDO updatePoint = new PackagePointDO(); + updatePoint.setPointId(startPoint.getPointId()); + updatePoint.setLockStatus("0"); + updatePoint.setPointStatus(BoxPointStatusEnum.EMPTY.getCode()); + updatePoint.setVehicleCode(""); + updatePoint.setContainerName(""); + packagePointMapper.updateById(updatePoint); + log.info("质检不合格任务{}取货完成,待检点{}已释放", + taskExecuteDTO.getTaskId(), taskInfoDTO.getPointCode1()); + } +}