Files
huachuang/docs/superpowers/plans/2026-07-22-outbound-create-plan.md
2026-07-22 14:23:13 +08:00

411 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 出库单新增功能实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development推荐或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 按确认的布局实现出库单新增页,通过库存箱号展开全部可用子卷或手工新增物料汇总,并事务保存 `wms_iostorinv` 主表和 `wms_iostorinvdtl` 明细。
**架构:** 前端由主弹窗、库存选择弹窗、手工汇总弹窗组成,页面只维护现有表字段和临时展示字段。后端提供可用库存查询/箱号展开接口与聚合创建接口;聚合服务调用 base 编码 API、重新计算汇总值并在一个事务内写入主表和明细。
**技术栈:** Java 17、Spring Boot、MyBatis XML、OpenFeign、Vue 3、TypeScript、Ant Design Vue Next、Vben Form、Vxe Grid、Vitest
---
## 文件结构
### 后端
- 修改 `nl-module-wms/nl-module-wms-server/pom.xml`:引入 `nl-module-base-api`
- 创建 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/framework/rpc/config/RpcConfiguration.java`:注册 `CodeGenApi` Feign 客户端。
- 创建 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java`:聚合创建请求及内部明细对象。
- 创建 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryPageReqVO.java`:库存筛选与分页参数。
- 创建 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/AvailableInventoryRespVO.java`:库存与页面展示结果。
- 创建 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/ExpandAvailableInventoryReqVO.java`:仓库和已选箱号集合。
- 修改 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java`:声明库存查询与按箱号展开方法。
- 修改 `nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml`:实现跨仓库结构和组盘库存的 SQL。
- 修改 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvService.java`:声明库存查询、展开和聚合创建方法。
- 修改 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java`:实现校验、编码生成、汇总和事务写入。
- 修改 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java`:暴露三个管理端接口。
- 修改 `nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java`:增加编码和库存失效错误码。
- 创建 `nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java`Spring 集成测试。
### 前端
- 修改 `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv/index.ts`:增加聚合请求、库存结果类型和接口函数。
- 创建 `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.ts`:纯函数计算明细数、总重量和去重键。
- 创建 `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts`:汇总联动的红绿测试。
- 创建 `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue`:库存筛选、多选和确认展开。
- 创建 `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/manual-detail.vue`:物料汇总新增。
- 修改 `nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue`:实现确认布局、明细联动和聚合提交。
---
## 任务 1建立后端请求契约与 RPC 依赖
- [ ] **步骤 1先创建 Spring 集成测试骨架并验证当前编译失败**
`IostorInvServiceLocalSpringTest.java` 中使用 `@SpringBootTest``@ActiveProfiles("local")``@Resource IostorInvService`,先引用尚不存在的 `IostorInvCreateReqVO``createOutbound`
```java
@SpringBootTest
@ActiveProfiles("local")
class IostorInvServiceLocalSpringTest {
@Resource
private IostorInvService iostorInvService;
@Test
void createOutboundRejectsEmptyDetails() {
IostorInvCreateReqVO reqVO = new IostorInvCreateReqVO();
Assertions.assertThrows(ConstraintViolationException.class,
() -> iostorInvService.createOutbound(reqVO));
}
}
```
运行:
```bash
mvn -pl nl-module-wms/nl-module-wms-server -am -DskipTests compile
```
预期FAIL提示 `IostorInvCreateReqVO``createOutbound` 不存在。
- [ ] **步骤 2增加 base API 依赖和 Feign 注册**
在 WMS server 的 `pom.xml` 增加:
```xml
<dependency>
<groupId>cn.nl.cloud</groupId>
<artifactId>nl-module-base-api</artifactId>
<version>${revision}</version>
</dependency>
```
创建 `RpcConfiguration`,使用 `@Configuration(value = "wmsRpcConfiguration", proxyBeanMethods = false)``@EnableFeignClients(clients = CodeGenApi.class)`
- [ ] **步骤 3创建带注解校验的聚合请求对象**
`IostorInvCreateReqVO` 顶层包含 `@NotEmpty billType``@NotEmpty storId``@NotNull bizDate``remark``@NotEmpty @Valid List<Detail> details`;内部 `Detail` 包含:
```java
@NotEmpty(message = "物料编码不能为空")
private String materialCode;
private String materialId;
private String pcsn;
@NotNull(message = "出库重量不能为空")
@DecimalMin(value = "0.001", message = "出库重量必须大于0")
private BigDecimal planQty;
private String qtyUnitId;
private String qtyUnitName;
private String sourceBillCode;
private String sourceBillType;
private String sourceBilldtlId;
private String remark;
```
不接收 `billCode``billStatus``detailCount``totalWeight``seqNo`,这些字段全部由服务端生成。
- [ ] **步骤 4声明 Service 方法并验证测试从编译失败进入校验失败/通过**
```java
@Validated
public interface IostorInvService {
String createOutbound(@Valid IostorInvCreateReqVO reqVO);
}
```
运行:
```bash
mvn -pl nl-module-wms/nl-module-wms-server -am -Dtest=IostorInvServiceLocalSpringTest -Dsurefire.failIfNoSpecifiedTests=false test
```
预期:测试能够启动且空请求因参数校验被拒绝;若 local 数据源不可用,保留测试并记录环境阻塞,继续用编译验证生产契约。
- [ ] **步骤 5提交契约变更**
```bash
git add nl-module-wms/nl-module-wms-server/pom.xml nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/framework/rpc/config/RpcConfiguration.java nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo/IostorInvCreateReqVO.java nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvService.java nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java
git commit -m "feat: 定义出库单聚合创建契约"
```
## 任务 2实现可用库存查询和按箱号展开
- [ ] **步骤 1扩展失败测试定义箱号展开行为**
在集成测试准备同一仓库下箱号 `BOX-001` 的两个可用子卷和另一个仓库的同箱数据,断言:
```java
List<AvailableInventoryRespVO> rows = iostorInvService.expandAvailableInventory(
"STOR-001", List.of("BOX-001", "BOX-001"));
Assertions.assertEquals(2, rows.size());
Assertions.assertTrue(rows.stream().allMatch(row -> "BOX-001".equals(row.getVehicleCode())));
```
运行指定测试预期FAIL方法和响应对象尚不存在。
- [ ] **步骤 2创建查询参数和响应对象**
`AvailableInventoryPageReqVO extends PageParam`,字段为 `@NotEmpty storId``materialCode``vehicleCode``pcsn``AvailableInventoryRespVO` 使用库存现有字段:`vehicleCode``pcsn``materialId``materialCode``materialName`(仅返回展示)、`availableQty`、单位、来源字段及仓库标识。`ExpandAvailableInventoryReqVO` 包含 `@NotEmpty storId``@NotEmpty List<String> vehicleCodes`
- [ ] **步骤 3声明 Mapper 方法**
```java
PageResult<AvailableInventoryRespVO> selectAvailableInventoryPage(AvailableInventoryPageReqVO reqVO);
List<AvailableInventoryRespVO> selectAvailableInventoryByVehicleCodes(
@Param("storId") String storId,
@Param("vehicleCodes") Collection<String> vehicleCodes);
```
- [ ] **步骤 4在 XML 实现统一库存 SQL**
基础关联为 `wms_group_plate gp INNER JOIN wms_structattr sa ON sa.storagevehicle_code = gp.vehicle_code LEFT JOIN base_materialbase mb ON mb.material_id = gp.material_id AND mb.is_deleted = 0`,固定条件为 `sa.stor_id = #{storId}``gp.status = '可用'``gp.qty - gp.frozen_qty > 0` 和 WMS 两表 `is_deleted = 0`。分页查询增加物料、箱号、子卷号筛选;展开查询使用 `<foreach>` 的箱号集合。结果中 `available_qty = gp.qty - gp.frozen_qty`,物料名称从 `mb.material_name` 返回但不写入明细表。
- [ ] **步骤 5Service 对箱号去重后查询全部子卷**
```java
List<String> distinctVehicleCodes = vehicleCodes.stream().distinct().toList();
return iostorinvDtlMapper.selectAvailableInventoryByVehicleCodes(storId, distinctVehicleCodes);
```
控制器提供:
```java
@GetMapping("/availableInventoryPage")
public CommonResult<PageResult<AvailableInventoryRespVO>> getAvailableInventoryPage(
@Valid AvailableInventoryPageReqVO reqVO)
@PostMapping("/expandAvailableInventory")
public CommonResult<List<AvailableInventoryRespVO>> expandAvailableInventory(
@Valid @RequestBody ExpandAvailableInventoryReqVO reqVO)
```
- [ ] **步骤 6运行集成测试和 Mapper 编译**
```bash
mvn -pl nl-module-wms/nl-module-wms-server -am -Dtest=IostorInvServiceLocalSpringTest -Dsurefire.failIfNoSpecifiedTests=false test
```
预期:同箱两子卷全部返回、重复箱号不产生重复行、其他仓库数据不返回。
- [ ] **步骤 7提交库存查询变更**
```bash
git add nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/vo nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/dal/mysql/iostorinvdtl/IostorinvDtlMapper.java nl-module-wms/nl-module-wms-server/src/main/resources/mapper/iostorinvdtl/IostorinvDtlMapper.xml nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv/IostorInvController.java nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java
git commit -m "feat: 支持按箱号展开可用库存子卷"
```
## 任务 3实现主表与明细事务保存
- [ ] **步骤 1补充失败测试**
构造两条重量 `12.500``7.250` 的明细,创建后查询数据库并断言:主表 `ioType = "OUT"``billStatus = "生成"``detailCount = 2``totalWeight = 19.750`;明细序号为 1/2、`assignQty = 0``unassignQty = planQty`。测试请求不传任何汇总字段。
- [ ] **步骤 2实现编码生成和事务写入**
`createOutbound` 标记 `@Transactional(rollbackFor = Exception.class)`
1. 构造 `CodeGenerateReqDTO`,设置项目现有出入库规则编码 `IO_CODE`
2. 调用 `CodeGenApi.generate`,检查 `CommonResult.isSuccess()` 且编码非空;失败抛出中文业务异常。
3. 手工逐字段创建 `IostorInvDO`,固定 `ioType = "OUT"``billStatus = "生成"`
4. 使用 `details.stream().map(Detail::getPlanQty).reduce(BigDecimal.ZERO, BigDecimal::add)` 计算总重量并设置明细数。
5. 插入主表后,循环构造 `IostorinvDtlDO`,写入关联 ID、连续序号、物料、`pcsn`、计划重量、分配数量、单位、来源和备注。
- [ ] **步骤 3增加控制器端点和错误码**
```java
@PostMapping("/createOutbound")
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')")
public CommonResult<String> createOutbound(@Valid @RequestBody IostorInvCreateReqVO reqVO) {
return success(iostorInvService.createOutbound(reqVO));
}
```
错误码仅增加“单据号生成失败”和“所选库存已不可用”,不新增数据库字段或表。
- [ ] **步骤 4验证红绿与事务回滚**
运行集成测试;再让测试用编码 API 失败配置触发异常,断言主表和明细数量均未增加。
- [ ] **步骤 5提交聚合保存**
```bash
git add nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/controller/admin/iostorinv nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java
git commit -m "feat: 事务保存出库单及明细"
```
## 任务 4扩展前端 API 与明细汇总纯函数
- [ ] **步骤 1先写失败的 Vitest 测试**
```ts
import { describe, expect, it } from 'vitest';
import { summarizeDetails } from './detail-summary';
describe('summarizeDetails', () => {
it('按明细条数和出库重量计算汇总', () => {
expect(summarizeDetails([{ planQty: 12.5 }, { planQty: 7.25 }])).toEqual({
detailCount: 2,
totalWeight: 19.75,
});
});
});
```
运行:
```bash
pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben vitest run apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts --dom
```
预期FAIL模块不存在。
- [ ] **步骤 2实现最小汇总函数**
```ts
export function summarizeDetails(details: Array<{ planQty?: number }>) {
return {
detailCount: details.length,
totalWeight: details.reduce((sum, item) => sum + Number(item.planQty || 0), 0),
};
}
```
- [ ] **步骤 3在 API 层增加明确类型和三个接口**
定义 `OutboundDetail``OutboundCreateReq``AvailableInventory``AvailableInventoryPageReq`,并增加:
```ts
export const getAvailableInventoryPage = (params: AvailableInventoryPageReq) =>
requestClient.get<PageResult<AvailableInventory>>('/wms/iostor-inv/availableInventoryPage', { params });
export const expandAvailableInventory = (storId: string, vehicleCodes: string[]) =>
requestClient.post<AvailableInventory[]>('/wms/iostor-inv/expandAvailableInventory', { storId, vehicleCodes });
export const createOutbound = (data: OutboundCreateReq) =>
requestClient.post<string>('/wms/iostor-inv/createOutbound', data);
```
- [ ] **步骤 4运行 Vitest 和类型检查**
```bash
pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben vitest run apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts --dom
pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben --filter @vben/web-antdv-next typecheck
```
预期:汇总测试通过,新增 API 类型无错误。
- [ ] **步骤 5提交前端基础能力**
```bash
git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv/index.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.ts nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts
git commit -m "feat: 增加出库单前端聚合接口与汇总逻辑"
```
## 任务 5实现库存选择与手工汇总弹窗
- [ ] **步骤 1实现库存选择弹窗**
`inventory-select.vue` 接收主弹窗传入的 `storId`表单筛选物料编码、箱号、子卷号Vxe Grid 使用服务端分页和多选。确认时只收集选中行的 `vehicleCode`,调用 `expandAvailableInventory`;返回结果逐子卷映射为明细,`planQty` 默认为 `availableQty`。勾选过程不自动修改其他子卷的选中状态。
- [ ] **步骤 2实现手工汇总弹窗**
复用 `views/base/materialbase/components/MaterialSelectModal.vue` 选择一个物料,填写大于 0 的总出库重量和备注;确认对象必须显式包含:
```ts
{
materialId,
materialCode,
materialName,
planQty,
qtyUnitId,
qtyUnitName,
vehicleCode: undefined,
pcsn: undefined,
sapBatchNo: undefined,
remark,
}
```
- [ ] **步骤 3运行前端类型检查**
```bash
pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben --filter @vben/web-antdv-next typecheck
```
预期:两个弹窗 props、emit、API 结果映射均无类型错误。
- [ ] **步骤 4提交两个弹窗**
```bash
git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/inventory-select.vue nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/manual-detail.vue
git commit -m "feat: 增加出库库存选择和手工汇总弹窗"
```
## 任务 6按参考图重构新增主弹窗
- [ ] **步骤 1实现主表布局与默认值**
重构 `form.vue` 为宽屏弹窗:单据号显示“保存时自动生成”;仓库和业务类型为必选下拉;状态只读“生成”;业务日期使用 `dayjs()` 默认为当天;明细数和总重量只读;备注跨列。仓库使用 `getBsrealStorAttrSimpleList`,业务类型从现有字典缓存中过滤出库业务类型。
- [ ] **步骤 2实现明细表与两种入口**
列顺序严格按确认原型序号、物料编码、物料名称、箱号、子卷号、SAP 批次号、出库重量、单位、源单号、明细备注、操作。出库重量可编辑;删除、库存返回、手工汇总返回均调用 `summarizeDetails` 更新只读汇总。
- [ ] **步骤 3实现仓库切换和提交**
仓库值发生实际变化且明细非空时清空明细。提交只发送 `billType``storId``bizDate``remark``wms_iostorinvdtl` 可持久化字段不得发送箱号、物料名称、SAP 批次号、单据号、状态和汇总字段。
- [ ] **步骤 4运行前端测试、类型检查与构建**
```bash
pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben vitest run apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts --dom
pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben --filter @vben/web-antdv-next typecheck
pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben --filter @vben/web-antdv-next build
```
预期:测试、类型检查和生产构建均以退出码 0 完成。
- [ ] **步骤 5提交主弹窗**
```bash
git add nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv/modules/form.vue
git commit -m "feat: 按参考布局实现出库单新增页"
```
## 任务 7端到端核对与最终验证
- [ ] **步骤 1核对数据库字段边界**
检查最终 diff确认没有 DDL、迁移脚本或 DO 新字段;前端展示字段没有进入 `createOutbound` 请求;所有明细只通过 `IostorinvDtlMapper` 写入 `wms_iostorinvdtl`
- [ ] **步骤 2运行后端完整模块验证**
```bash
mvn -pl nl-module-wms/nl-module-wms-server -am clean test -DskipTests=false
```
预期WMS server 及依赖模块编译成功,测试失败数为 0。
- [ ] **步骤 3运行前端完整验证**
```bash
pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben vitest run apps/web-antdv-next/src/views/wms/iostorinv/modules/detail-summary.test.ts --dom
pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben --filter @vben/web-antdv-next typecheck
pnpm --dir nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben --filter @vben/web-antdv-next build
```
预期:三条命令均退出码 0。
- [ ] **步骤 4人工验收关键流程**
启动前后端后确认新增页默认值正确未选仓库不能选择库存选择任一库存记录会按箱号带回全部子卷勾选本身不联动手工汇总的箱号、子卷号、SAP 批次号为空;明细汇总实时变化;切换仓库清空明细;保存后主表和全部明细同时存在。
- [ ] **步骤 5提交验证阶段必要修正**
仅当验证产生修正时提交相关文件:
```bash
git add nl-module-wms nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/wms/iostorinv nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/wms/iostorinv
git commit -m "fix: 完善出库单新增流程验证问题"
```