737 lines
28 KiB
Markdown
737 lines
28 KiB
Markdown
# 子卷包装关系批量新增实现计划
|
||
|
||
> **面向 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<SubPackageRelationBatchCreateItemReqVO> 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<Failure> 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<BoxTypeDO> selectListByBoxTypes(Collection<String> boxTypes) {
|
||
return selectList(BoxTypeDO::getBoxType, boxTypes);
|
||
}
|
||
```
|
||
|
||
`SubPackageRelationMapper` 增加:
|
||
|
||
```java
|
||
default Long selectCountByPackageBoxSnAndBoxType(String packageBoxSn, String boxType) {
|
||
return selectCount(new LambdaQueryWrapper<SubPackageRelationDO>()
|
||
.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
|
||
<dependency>
|
||
<groupId>org.mockito</groupId>
|
||
<artifactId>mockito-inline</artifactId>
|
||
<scope>test</scope>
|
||
</dependency>
|
||
```
|
||
|
||
- [ ] **步骤 2:编写失败测试**
|
||
|
||
```java
|
||
@ExtendWith(MockitoExtension.class)
|
||
class SubPackageRelationBatchSaveServiceTest {
|
||
|
||
@Mock
|
||
private SubPackageRelationMapper subPackageRelationMapper;
|
||
|
||
@InjectMocks
|
||
private SubPackageRelationBatchSaveService service;
|
||
|
||
@Test
|
||
void shouldInsertWholeGroup() {
|
||
List<SubPackageRelationDO> 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<SubPackageRelationDO> 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<String, BoxTypeDO>`。
|
||
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<SubPackageRelationBatchCreateRespVO> 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<LmsSubPackageRelationApi.BatchCreateResult>(
|
||
'/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<Record<keyof BatchFormRow, string>>;
|
||
|
||
export function createBatchRow(): BatchFormRow;
|
||
export function copyBatchRow(source: BatchFormRow): BatchFormRow;
|
||
export function validateBatchRows(rows: BatchFormRow[]): Map<number, BatchFieldErrors>;
|
||
export function applyBatchResult(
|
||
rows: BatchFormRow[],
|
||
result: LmsSubPackageRelationApi.BatchCreateResult,
|
||
): { rowErrors: Map<string, string>; 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<BatchFormRow[]>([createBatchRow()]);
|
||
const viewMode = ref<'card' | 'table'>('table');
|
||
const activeClientKey = ref<string>();
|
||
const fieldErrors = ref(new Map<number, BatchFieldErrors>());
|
||
const rowErrors = ref(new Map<string, string>());
|
||
|
||
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
|
||
<BatchFormModal @success="handleRefresh" />
|
||
```
|
||
|
||
- [ ] **步骤 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:记录验证证据**
|
||
|
||
交付说明必须列出实际运行的每条命令、退出码和关键结果;如果检查被现有依赖或工具链阻塞,明确给出原始错误摘要,不得声称对应检查通过。
|