2026-04-29-REQ-MOD-002.md 12.5 KB

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-tdd executes 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,新增 UpdateModuleDTOModuleService#update、controller @PutMappingmapper.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.javarequestMatchers(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) == nullselectParentIdById(child.id) == root.idselectParentIdById(deleted.id) == nullselectParentIdById(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

    1. Module original = moduleMapper.selectById(id);若 null 或 original.getBDeleted()==trueBizException(40400, "模块不存在或已删除")
    2. 枚举校验同 MOD-001 → 40010
    3. iParentId 校验(4 类)(在本 task 不全实现,仅留 hook,详见 Task 4):本 task 暂只对 null/合法路径走通;非 null 时调 existsActiveById 但暂不做自指/环检测,留 Task 4 加。
    4. 构造 Module entity:仅 set iIncrement 和可改 7 字段(其余 null);bShowPermission null → false
    5. moduleMapper.updateById(entity) 返回 id
  • Step 1: 写失败测试

    • ModuleServiceImplTest 追加 3 用例:
    • updateWithValidDto_invokesUpdateById_withEditableFieldsOnly
    • updateWithTargetNotFound_throws40400
    • updateWithBShowPermissionNull_setsFalseInEntity
    • mock 准备:moduleMapper.selectById(id) 返回 stub Module(含原 sProcedureName / sCreatedBy);moduleMapper.updateById(any(Module.class)) 返回 1
    • ArgumentCaptor 抓传给 updateById 的 entity,断言:iIncrement 是路径 id;sProcedureName == nullsCreatedBy == nulltCreateDate == nullsBrandsId == nullsSubsidiaryId == 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 实现):

  1. 目标存在 / 枚举(已在 Task 3 实现)
  2. 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)=trueselectParentIdById(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=0data.iIncrement 等于 ① 的 id;④ JdbcTemplate 查行:sModuleNameZh="新名"sDisplayType="前端业务"sProcedureName="sp_test_orig"(保留)、sCreatedBy="ORIG_USER"(保留)
  • 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/99999999 body 合法 → code=40400
    • putInvalidDisplayType_returns40010sDisplayType="火星"code=40010
    • putSelfParent_returns40021 — body iParentId == path idcode=40021
    • putCyclicParent_returns40021 — 准备:先插 root,再插 child(parent=root);PUT root 把 iParentId=child.idcode=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.jwtcode=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