2026-04-29-REQ-MOD-003.md 7.25 KB

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-tdd executes 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 + default hasActiveChildren
  • 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)

    1. Module original = moduleMapper.selectById(id) → null 或 bDeleted=trueBizException(40400, "模块不存在或已删除")
    2. moduleMapper.hasActiveChildren(id) → true → BizException(40901, "模块仍有未删除子节点")
    3. 构造 Module entitysetIIncrement(id) / setBDeleted(true) / setTDeletedDate(LocalDateTime.now()) / setSDeletedBy(SecurityContextHelper.currentUserNo() ?: stub.getStubUserNo());其他字段 null
    4. moduleMapper.updateById(entity)
  • Step 1: 写失败测试(5 用例)

    • deleteWithValidId_softDeletes_andSetsAuditFields:mock selectById(10)=alivehasActiveChildren(10)=falseupdateById(any)=1;ArgumentCaptor 抓 entity;断言 iIncrement=10 / bDeleted=true / tDeletedDate != null / sDeletedBy="STUB_ADMIN" / 其他字段 null
    • deleteWithTargetNotFound_throws40400selectById(99)=nullupdateById 永不调用
    • deleteWithTargetAlreadyDeleted_throws40400selectById(10) 返回 bDeleted=true 的 Module
    • deleteWithActiveChildren_throws40901hasActiveChildren(10)=true
    • deleteSetsDeletedByFromAuthenticatedUser:SecurityContextHolder 注入 principal "BOB";ArgumentCaptor sDeletedBy="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/99999996code=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