req_id: REQ-MOD-002 date: 2026-04-29
spec_ref: docs/superpowers/specs/2026-04-29-REQ-MOD-002.md
REQ-MOD-002 模块修改 Implementation Plan
Execution: Parent skill
feature-tddexecutes this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: 在 MOD-001 已有工程基础上增量实现 PUT /api/mod/modules/{id},更新 7 个可编辑字段,保留 sProcedureName / sCreatedBy / 标准列;含目标存在性、枚举、父链合法性(自指 / 不存在 / 环)四类校验。
Architecture: 复用 ModuleService / ModuleServiceImpl / ModuleController / ModuleMapper / Module entity,新增 UpdateModuleDTO、ModuleService#update、controller @PutMapping、mapper.selectParentIdById(轻量父链查询)。SecurityConfig 路径白名单从 POST /api/mod/modules 扩展为 /api/mod/** 以覆盖 MOD-002~004。环检测在 service 层用循环回溯,最大深度 50。
Tech Stack: Spring Boot 3.3.5 / MyBatis-Plus / Spring Security(已有);JUnit 5 + Mockito + TestRestTemplate(已有)。
Schema 改动
无(tModule schema 已满足;本 REQ 仅 UPDATE 操作)。
文件变更清单
新增
-
backend/src/main/java/com/xly/erp/module/mod/dto/UpdateModuleDTO.java— 入参 DTO(无sProcedureName字段)
修改
-
backend/src/main/java/com/xly/erp/common/security/SecurityConfig.java—requestMatchers(POST, "/api/mod/modules")→requestMatchers("/api/mod/**"),stub 注释保持 -
backend/src/main/java/com/xly/erp/module/mod/mapper/ModuleMapper.java— 追加Integer selectParentIdById(Integer id)注解 SELECT 方法(仅查 iParentId 列,不取整行) -
backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java— 追加Integer update(Integer id, UpdateModuleDTO dto)方法 -
backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java— 实现update(...):目标存在性 → 枚举 → iParentId 三重校验 → mapper.updateById -
backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java— 追加@PutMapping("/modules/{id}")端点 -
backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java— 追加 7 个 update 用例 -
backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java— 追加 7 个 PUT IT 用例 -
backend/src/test/java/com/xly/erp/module/mod/mapper/ModuleMapperIT.java— 追加 1 个selectParentIdById用例
任务步骤
全局约束:每 commit 形如
<type>(mod): <subject> REQ-MOD-002;测试派发到子会话执行;现有 26 用例全程保持绿。
Task 1: SecurityConfig 路径白名单扩范围
Files:
- Modify:
backend/src/main/java/com/xly/erp/common/security/SecurityConfig.java:25
API shape:
- 改一行:
requestMatchers(HttpMethod.POST, "/api/mod/modules").permitAll()→requestMatchers("/api/mod/**").permitAll() 注释保留
// REQ-MOD-001 stub: see USR-004 follow-up(语义不变,覆盖范围扩大)-
Step 1: 修改 SecurityConfig
- 单行 Edit;不动任何其他类
-
Step 2: 子会话验证 PASS
- 命令:
cd backend && mvn -B test -Dtest=ModuleControllerIT - 期望:现有 7 用例全绿(permitAll 范围扩大不收紧已有路径)
- 命令:
-
Step 3: Commit
git commit -m "refactor(mod): widen permitAll stub to /api/mod/** REQ-MOD-002"
Task 2: ModuleMapper 追加父 ID 查询
Files:
- Modify:
backend/src/main/java/com/xly/erp/module/mod/mapper/ModuleMapper.java - Modify:
backend/src/test/java/com/xly/erp/module/mod/mapper/ModuleMapperIT.java
API shape:
-
@Select("SELECT iParentId FROM tModule WHERE iIncrement = #{id} AND bDeleted = 0")Integer selectParentIdById(@Param("id") Integer id)- 命中行的
iParentId可能为 NULL(根模块)→ 返回 null - 未命中(不存在 / 已软删)→ 返回 null(与"根模块"在调用方语义不同,需要调用方明确校验存在性)
- 命中行的
-
Step 1: 写失败测试
ModuleMapperIT#selectParentIdById_returnsNullForRootOrMissing_andValueForChild- 准备 3 行:root(iParentId=NULL)、child(iParentId=root)、deleted(iParentId=root, bDeleted=1)
- 断言:
selectParentIdById(root.id) == null;selectParentIdById(child.id) == root.id;selectParentIdById(deleted.id) == null;selectParentIdById(99999999) == null
Step 2: 实现 mapper 方法
-
Step 3: 子会话验证 PASS
- 命令:
cd backend && mvn -B test -Dtest=ModuleMapperIT
- 命令:
-
Step 4: Commit
git commit -m "feat(mod): mapper#selectParentIdById for cycle check REQ-MOD-002"
Task 3: UpdateModuleDTO + Service.update 合法路径
Files:
- Create:
backend/src/main/java/com/xly/erp/module/mod/dto/UpdateModuleDTO.java - Modify:
backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java - Modify:
backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java - Modify:
backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java
API shape:
-
UpdateModuleDTO字段(带@JsonProperty锁定 JSON 名 + Bean Validation):@NotBlank String sDisplayType@NotBlank @Size(max=50) String sModuleType@NotBlank @Size(max=50) String sManageDeptEn-
Boolean bShowPermission(可空) @NotBlank @Size(max=100) String sModuleNameZh-
Integer iParentId(可空) -
Integer iSortOrder(可空) - 不含
sProcedureName
ModuleService#update(Integer id, UpdateModuleDTO dto) : Integer-
ModuleServiceImpl#update:-
Module original = moduleMapper.selectById(id);若 null 或original.getBDeleted()==true→BizException(40400, "模块不存在或已删除") - 枚举校验同 MOD-001 → 40010
-
iParentId校验(4 类)(在本 task 不全实现,仅留 hook,详见 Task 4):本 task 暂只对 null/合法路径走通;非 null 时调existsActiveById但暂不做自指/环检测,留 Task 4 加。 - 构造
Module entity:仅 setiIncrement和可改 7 字段(其余 null);bShowPermissionnull → false -
moduleMapper.updateById(entity)返回id
-
-
Step 1: 写失败测试
- 在
ModuleServiceImplTest追加 3 用例: updateWithValidDto_invokesUpdateById_withEditableFieldsOnlyupdateWithTargetNotFound_throws40400updateWithBShowPermissionNull_setsFalseInEntity- mock 准备:
moduleMapper.selectById(id)返回 stub Module(含原 sProcedureName / sCreatedBy);moduleMapper.updateById(any(Module.class))返回 1 - ArgumentCaptor 抓传给
updateById的 entity,断言:iIncrement是路径 id;sProcedureName == null;sCreatedBy == null;tCreateDate == null;sBrandsId == null;sSubsidiaryId == null;可改字段被透传 - 子会话先跑 → FAIL(方法不存在)
- 在
-
Step 2: 实现 DTO + Service
- DTO 与 CreateModuleDTO 平行结构;Service 实现仅覆盖 Task 3 三个用例所需逻辑
-
Step 3: 子会话验证 PASS
- 命令:
cd backend && mvn -B test -Dtest=ModuleServiceImplTest - 期望:6 (MOD-001) + 3 (本 task) = 9 用例全绿
- 命令:
-
Step 4: Commit
git commit -m "feat(mod): module update dto + service happy path REQ-MOD-002"
Task 4: Service 校验分支(枚举 + iParentId 三类)
Files:
- Modify:
backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java - Modify:
backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java
API shape: 不变(仅补 update 方法内部校验逻辑)
校验顺序(service 实现):
- 目标存在 / 枚举(已在 Task 3 实现)
- 若
iParentId != null: a.iParentId.equals(id)→BizException(40021, "父模块不能指向自身")b.!moduleMapper.existsActiveById(iParentId)→BizException(40021, "父模块不存在或已删除")c. 环检测:Integer cur = iParentId; for (int depth = 0; cur != null && depth < 50; depth++) { if (cur.equals(id)) throw BizException(40021,"父模块链构成环路"); cur = moduleMapper.selectParentIdById(cur); };若 depth 达到 50 →BizException(40021, "父模块链超过最大层级")
-
Step 1: 写失败测试
- 在
ModuleServiceImplTest追加 4 用例: updateWithInvalidDisplayType_throws40010-
updateWithSelfParentId_throws40021(msg contains "自身") -
updateWithMissingParent_throws40021(msg contains "父模块不存在") -
updateWithCyclicParent_throws40021(msg contains "环路";mock 编排:existsActiveById(parent)=true;selectParentIdById(parent) = id) - 子会话先跑 → 4 用例 FAIL
- 在
-
Step 2: 实现校验分支
- 严格按上述 a/b/c 顺序,a 在 b 之前(自指应当先于存在性,否则用户传自身 id 在父表里又恰好不存在时报错信息会误导)
-
Step 3: 子会话验证 PASS
- 命令:
cd backend && mvn -B test -Dtest=ModuleServiceImplTest - 期望:6 + 3 + 4 = 13 用例全绿
- 命令:
-
Step 4: Commit
git commit -m "feat(mod): module update parent validation REQ-MOD-002"
Task 5: ModuleController PUT + IT 正常路径
Files:
- Modify:
backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java - Modify:
backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java
API shape:
@PutMapping("/modules/{id}") public Result<Map<String,Integer>> update(@PathVariable Integer id, @Valid @RequestBody UpdateModuleDTO dto)返回
Result.ok(Map.of("iIncrement", moduleService.update(id, dto)))-
Step 1: 写失败测试
ModuleControllerIT#putValidBody_with_jwt_returns200_andUpdatesEditableFields- 步骤:① JdbcTemplate 直插一行原始数据(含 sProcedureName="sp_test_orig"、sCreatedBy="ORIG_USER"、sBrandsId="XLY"、sModuleNameZh="原名");② PUT body 改 sModuleNameZh="新名"、sDisplayType="前端业务";带 JWT="ADMIN001";③ 期望
code=0,data.iIncrement等于 ① 的 id;④ JdbcTemplate 查行:sModuleNameZh="新名"、sDisplayType="前端业务"、sProcedureName="sp_test_orig"(保留)、sCreatedBy="ORIG_USER"(保留)
- 步骤:① JdbcTemplate 直插一行原始数据(含 sProcedureName="sp_test_orig"、sCreatedBy="ORIG_USER"、sBrandsId="XLY"、sModuleNameZh="原名");② PUT body 改 sModuleNameZh="新名"、sDisplayType="前端业务";带 JWT="ADMIN001";③ 期望
-
Step 2: 实现 controller PUT
- 与 POST 同结构
-
Step 3: 子会话验证 PASS
- 命令:
cd backend && mvn -B test -Dtest='ModuleControllerIT#putValidBody_with_jwt_returns200_andUpdatesEditableFields'
- 命令:
-
Step 4: Commit
git commit -m "feat(mod): PUT /api/mod/modules/{id} controller REQ-MOD-002"
Task 6: IT 异常路径补全 + 全量回归
Files:
- Modify:
backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java
API shape: 不新增(覆盖 PUT 接口 6 条异常路径)
-
Step 1: 在 IT 中追加 6 个用例
-
putNonExistentId_returns40400— PUT/api/mod/modules/99999999body 合法 →code=40400 -
putInvalidDisplayType_returns40010—sDisplayType="火星"→code=40010 -
putSelfParent_returns40021— bodyiParentId == path id→code=40021 -
putCyclicParent_returns40021— 准备:先插 root,再插 child(parent=root);PUT root 把iParentId=child.id→code=40021 -
putWithoutJwt_permitAllStub_returns200_andDoesNotChangeCreatedBy— 先插原行(sCreatedBy="ORIG_USER"),无 token PUT;期望code=0,DB 中 sCreatedBy 仍为 "ORIG_USER"(不被覆盖为 STUB_ADMIN) -
putTamperedJwt_returns20001— Authorization 头Bearer not.a.real.jwt→code=20001 - 6 个用例先跑 → 期望 FAIL(部分分支需 Task 4/5 已经覆盖;这里主要补 IT 端到端)
-
-
Step 2: 让测试通过
- service / controller / config 已实现;本 task 主要排查 RestTemplate 行为(4xx 是否抛、URL 拼接、JSON parse);不应当为让测试通过新增业务分支
-
Step 3: 子会话跑全量回归
- 命令:
cd backend && mvn -B test - 期望:MOD-001 26 用例 + MOD-002 新增 1(mapperIT) + 7(serviceTest) + 7(controllerIT) = 41 用例全绿
- 命令:
-
Step 4: Commit
git commit -m "test(mod): module update integration coverage REQ-MOD-002"
提交计划
| commit | 覆盖 |
|---|---|
refactor(mod): widen permitAll stub to /api/mod/** REQ-MOD-002 |
Task 1 |
feat(mod): mapper#selectParentIdById for cycle check REQ-MOD-002 |
Task 2 |
feat(mod): module update dto + service happy path REQ-MOD-002 |
Task 3 |
feat(mod): module update parent validation REQ-MOD-002 |
Task 4 |
feat(mod): PUT /api/mod/modules/{id} controller REQ-MOD-002 |
Task 5 |
test(mod): module update integration coverage REQ-MOD-002 |
Task 6 |