req_id: REQ-MOD-003 date: 2026-04-29
spec_ref: docs/superpowers/specs/2026-04-29-REQ-MOD-003.md
REQ-MOD-003 模块删除 Implementation Plan
Execution: Parent skill
feature-tddexecutes this plan task-by-task.
Goal: 在 MOD-001/002 已建工程基础上增量实现 DELETE /api/mod/modules/{id} 软删除接口,含目标存在性、子模块拦截两类校验,软删除后 bDeleted=1 + 审计字段。
Architecture: 复用现有 ModuleService / ModuleServiceImpl / ModuleController / ModuleMapper;新增 mapper.hasActiveChildren(id) + service.delete(id) + controller @DeleteMapping。SecurityConfig 已对 /api/mod/** permitAll,无需改。sDeletedBy 取 JWT principal 或回退 stub(与 MOD-001 sCreatedBy 同策略)。40902 外部引用拦截不实现——docs/03 当前 schema 中 tModule 无引用方表。
Tech Stack: Spring Boot 3.3.5 / MyBatis-Plus / JUnit 5 + Mockito + TestRestTemplate(沿用)。
Schema 改动
无(仅 UPDATE 软删除字段)。
文件变更清单
修改
-
backend/src/main/java/com/xly/erp/module/mod/mapper/ModuleMapper.java— 追加findActiveChildFlag+ defaulthasActiveChildren -
backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java— 追加void delete(Integer id)方法 -
backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java— 实现delete(...) -
backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java— 追加@DeleteMapping("/modules/{id}")端点 -
backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java— 追加 5 用例 -
backend/src/test/java/com/xly/erp/module/mod/mapper/ModuleMapperIT.java— 追加 1 用例 -
backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java— 追加 6 用例
任务步骤
全局:每 commit
<type>(mod): <subject> REQ-MOD-003;测试派发子会话;现有 41 用例全程绿。
Task 1: Mapper#hasActiveChildren + IT
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 1 FROM tModule WHERE iParentId = #{parentId} AND bDeleted = 0 LIMIT 1")Integer findActiveChildFlag(@Param("parentId") Integer parentId) default boolean hasActiveChildren(Integer parentId) { return findActiveChildFlag(parentId) != null; }-
Step 1: 写失败测试
ModuleMapperIT#hasActiveChildren_trueIfChildAliveExists_falseOtherwise- 准备:root(无 parent);child1(parent=root, bDeleted=0);child2(parent=root, bDeleted=1)
- 断言:
hasActiveChildren(root.id) == true - 用 JdbcTemplate
UPDATE tModule SET bDeleted=1 WHERE iIncrement=child1.id软删唯一活跃子节点 - 再次断言:
hasActiveChildren(root.id) == false hasActiveChildren(99999997) == false
Step 2: 实现 mapper 方法
-
Step 3: 子会话验证 PASS
- 命令:
cd backend && mvn -B test -Dtest=ModuleMapperIT
- 命令:
-
Step 4: Commit
git commit -m "feat(mod): mapper#hasActiveChildren for delete check REQ-MOD-003"
Task 2: Service#delete + 单测
Files:
- 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:
ModuleService#delete(Integer id) : void-
ModuleServiceImpl#delete(Integer id):-
Module original = moduleMapper.selectById(id)→ null 或bDeleted=true→BizException(40400, "模块不存在或已删除") -
moduleMapper.hasActiveChildren(id)→ true →BizException(40901, "模块仍有未删除子节点") - 构造
Module entity:setIIncrement(id)/setBDeleted(true)/setTDeletedDate(LocalDateTime.now())/setSDeletedBy(SecurityContextHelper.currentUserNo() ?: stub.getStubUserNo());其他字段 null moduleMapper.updateById(entity)
-
-
Step 1: 写失败测试(5 用例)
-
deleteWithValidId_softDeletes_andSetsAuditFields:mockselectById(10)=alive、hasActiveChildren(10)=false、updateById(any)=1;ArgumentCaptor 抓 entity;断言iIncrement=10/bDeleted=true/tDeletedDate != null/sDeletedBy="STUB_ADMIN"/ 其他字段 null -
deleteWithTargetNotFound_throws40400:selectById(99)=null;updateById永不调用 -
deleteWithTargetAlreadyDeleted_throws40400:selectById(10)返回bDeleted=true的 Module -
deleteWithActiveChildren_throws40901:hasActiveChildren(10)=true -
deleteSetsDeletedByFromAuthenticatedUser:SecurityContextHolder 注入 principal "BOB";ArgumentCaptorsDeletedBy="BOB" - 子会话先跑 → 5 用例 FAIL
-
-
Step 2: 实现 service
- 严格按 API shape 顺序
-
Step 3: 子会话验证 PASS
- 命令:
cd backend && mvn -B test -Dtest=ModuleServiceImplTest - 期望:13 (前) + 5 = 18 用例全绿
- 命令:
-
Step 4: Commit
git commit -m "feat(mod): module delete service + soft delete REQ-MOD-003"
Task 3: Controller DELETE + 6 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:
@DeleteMapping("/modules/{id}") public Result<Void> delete(@PathVariable Integer id)调
moduleService.delete(id);返回Result.ok()-
Step 1: 写失败测试(6 用例)
-
deleteValidId_with_jwt_returns200_andSoftDeletes:JdbcTemplate 直插 alive 行;DELETE 带 JWT="ADMIN001";期望code=0/data=null;JdbcTemplate 查bDeleted=1/sDeletedBy="ADMIN001"/tDeletedDate IS NOT NULL/sProcedureName不变 /sCreatedBy不变 -
deleteNonExistentId_returns40400:DELETE/api/mod/modules/99999996→code=40400 -
deleteAlreadyDeletedId_returns40400:JdbcTemplate 直插bDeleted=1行;DELETE →code=40400 -
deleteWithActiveChildren_returns40901:JdbcTemplate 直插 root + child(bDeleted=0, parent=root);DELETE root →code=40901;JdbcTemplate 查 root 仍bDeleted=0 -
deleteWithoutJwt_permitAllStub_returns200_andDeletedByIsSTUB:JdbcTemplate 直插 alive;无 token DELETE;DB 查sDeletedBy="STUB_ADMIN"/bDeleted=1 -
deleteTamperedJwt_returns20001:JdbcTemplate 直插 alive;Authorization "Bearer not.a.real.jwt" DELETE;code=20001;DB 查行bDeleted=0(filter 短路,service 未触发) - 6 用例先跑 → FAIL(controller 不存在 → 405/404)
-
Step 2: 实现 controller DELETE
-
Step 3: 子会话跑全量回归
- 命令:
cd backend && mvn -B test - 期望:MOD-001 26 + MOD-002 15 + MOD-003 新增 1(mapperIT) + 5(svc) + 6(it) = 53 用例全绿
- 命令:
-
Step 4: Commit
git commit -m "test(mod): module delete integration coverage REQ-MOD-003"
提交计划
| commit | 覆盖 |
|---|---|
feat(mod): mapper#hasActiveChildren for delete check REQ-MOD-003 |
Task 1 |
feat(mod): module delete service + soft delete REQ-MOD-003 |
Task 2 |
test(mod): module delete integration coverage REQ-MOD-003 |
Task 3 |