2026-05-06-REQ-MOD-003.md 8.45 KB

req_id: REQ-MOD-003 date: 2026-05-06

spec_ref: docs/superpowers/specs/2026-05-06-REQ-MOD-003.md

REQ-MOD-003 模块删除 Implementation Plan

Execution: Parent skill feature-tdd executes this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 实现 DELETE /api/modules/{id}:在校验子模块未删除引用的前提下,对目标做软删除(写 bDeleted/tDeletedDate/sDeletedBy)。

Architecture: 复用 REQ-MOD-001/002 已建立的体系。Service 先 selectById 校验目标存在 + 未删除(40421),再 selectCount 子模块未删除引用(40912),最后用 mapper.update(entity, wrapper)bDeleted = 0 条件做并发安全的软删除(影响 0 行视为并发删除 → 40421)。返回精简 VO。

Tech Stack: 沿用 REQ-MOD-001/002(Spring Boot 3.2.5 + MyBatis-Plus 3.5.7 + JUnit 5 + Mockito)。


Schema 改动

无(软删除字段 bDeleted / tDeletedDate / sDeletedBy 在 V1 已建)。

文件变更清单

  • 修改: backend/src/main/java/com/xly/erp/common/response/ErrorCode.java — 追加 MOD_HAS_REFERENCES(40912, "存在子模块或外部业务引用,禁止删除")
  • 创建: backend/src/main/java/com/xly/erp/module/mod/vo/ModuleDeleteResultVO.java — 精简 VO(iIncrement + bDeleted)
  • 修改: backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java — 追加 delete(Integer id): ModuleDeleteResultVO
  • 修改: 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("/{id}")
  • 修改: backend/src/test/java/com/xly/erp/common/response/ApiResponseTest.java — 追加 1 个错误码断言
  • 修改: backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java — 追加 6 个 delete 单测
  • 修改: backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java — 追加 6 个 DELETE 集成测试

任务步骤

Task 1: 错误码 + 精简 VO

Files:

  • Modify: backend/src/main/java/com/xly/erp/common/response/ErrorCode.java
  • Create: backend/src/main/java/com/xly/erp/module/mod/vo/ModuleDeleteResultVO.java
  • Modify: backend/src/test/java/com/xly/erp/common/response/ApiResponseTest.java

API shape:

  • MOD_HAS_REFERENCES(40912, "存在子模块或外部业务引用,禁止删除")
  • ModuleDeleteResultVO 字段:Integer iIncrement + Boolean bDeleted(带 @Data + 静态工厂 of(Integer id, Boolean deleted)

  • Step 1.1 写失败断言

    • ApiResponseTest#errorCode_constantsMatchDocs05Spec 末尾追加: assertThat(ErrorCode.MOD_HAS_REFERENCES.getCode()).isEqualTo(40912);
    • 子会话 FAIL(枚举常量不存在)
  • Step 1.2 追加枚举常量 + 创建 VO

  • Step 1.3 子会话验证 ApiResponseTest 全绿

  • Step 1.4 提交

    • git commit -m "feat(common): error code MOD_HAS_REFERENCES + delete VO REQ-MOD-003"

Task 2: ModuleService.delete — 业务逻辑(mock 单元测试)

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(追加 6 个测试)

API shape:

  • interface ModuleService 追加:ModuleDeleteResultVO delete(Integer id)
  • 实现步骤(写在 plan 锁定):
    1. target = moduleMapper.selectById(id)target == null || target.bDeleted == trueBizException(MOD_NOT_FOUND)(40421)
    2. 子模块计数:childCount = moduleMapper.selectCount(LambdaQueryWrapper.eq(iParentId, id).eq(bDeleted, false))
      • childCount > 0BizException(MOD_HAS_REFERENCES)(40912)
    3. 构造只含软删除三件套 + 主键的更新 entity(避免触碰其他字段): ModuleEntity patch = new ModuleEntity(); patch.setIIncrement(id); patch.setBDeleted(true); patch.setTDeletedDate(LocalDateTime.now()); patch.setSDeletedBy(null); // FieldStrategy 默认 NOT_NULL,会被 MP 跳过;这里靠 IGNORED 或显式 update wrapper 写入。 方式选择:用 moduleMapper.update(patch, new LambdaUpdateWrapper<ModuleEntity>().eq(iIncrement, id).eq(bDeleted, false)),确保只更新仍未删除的目标,且 update 影响行数为并发兜底信号。
    4. int affected = moduleMapper.update(...)affected == 0BizException(MOD_NOT_FOUND)(视为目标在校验后被并发删除)
    5. return ModuleDeleteResultVO.of(id, true)
  • @Transactional(rollbackFor = Exception.class)

  • Step 2.1 写失败测试(6 个)

    • delete_targetNotFound_throws40421selectById → null
    • delete_targetAlreadyDeleted_throws40421:target.bDeleted=true
    • delete_hasUndeletedChildren_throws40912:selectCount > 0
    • delete_leafModule_returnsResult:selectCount=0、update 返回 1,断言 VO + 断言传给 update 的 entity 字段(bDeleted=true、tDeletedDate 非 null、iIncrement 正确)
    • delete_softDeletedChildren_doesNotBlock:selectCount=0(已删子不计入),update 返回 1,断言成功
    • delete_concurrentRace_throws40421:selectById 返回未删除目标,selectCount=0,update 返回 0 → 抛 40421
    • 测试方式:@ExtendWith(MockitoExtension.class) + ArgumentCaptor<ModuleEntity> 捕获 update 实参;用 any(Wrapper.class) 占位 wrapper
    • 子会话: FAIL(方法不存在)
  • Step 2.2 实现 delete

    • 注意:moduleMapper.update(entity, wrapper) 在 BaseMapper 里有重载,可能与 update(entity, T) 冲突。预期签名 int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper),调用为 moduleMapper.update(patch, wrapper)。Mockito stub 时用 when(moduleMapper.update(any(ModuleEntity.class), any(Wrapper.class))) 应能消歧;若仍 ambiguous,按 REQ-MOD-001 经验改用显式 cast (ModuleEntity) any() + (Wrapper) any()
  • Step 2.3 子会话确认 ModuleServiceImplTest 全部绿

    • 累计:6 (create) + 8 (update) + 6 (delete) = 20
  • Step 2.4 提交

    • git commit -m "feat(mod): delete module service REQ-MOD-003"

Task 3: ModuleController DELETE 端点 + 端到端 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(追加 6 个集成用例)

API shape:

  • 新方法: @DeleteMapping("/{id}") public ApiResponse<ModuleDeleteResultVO> delete(@PathVariable Integer id)
  • Javadoc:REQ-MOD-003 模块删除 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:DELETE')")

  • Step 3.1 写失败测试(6 个)

    • delete_validLeaf_returns200WithBDeletedTrue:mapper.insert 一条 → DELETE → 断言 200 + data.bDeleted=true + selectById 验证 DB 中 bDeleted=true / tDeletedDate 非 null
    • delete_targetNotFound_returns40421:DELETE /api/modules/999999
    • delete_targetAlreadyDeleted_returns40421:mapper.insert 一条并立刻把 bDeleted 置 true,DELETE 返回 40421
    • delete_hasUndeletedChildren_returns40912:parent + child(bDeleted=0),DELETE parent → 40912;selectById parent 验证 bDeleted 仍 false
    • delete_softDeletedChildren_doesNotBlock_returns200:先 DELETE child(应成功),再 DELETE parent → 200
    • delete_responseVOContainsOnlyIIncrementAndBDeleted:断言 $.data 路径只有 iIncrement + bDeleted 两个字段(用 jsonPath $.data.sProcedureName 不存在)
    • 测试方式:@SpringBootTest @AutoConfigureMockMvc @Transactional @Rollback + @Autowired ModuleMapper 直接预置数据
    • 子会话: FAIL(端点不存在)
  • Step 3.2 实现 DELETE 端点

    • 子会话: PASS
  • Step 3.3 跑全量 backend 测试

    • cd backend && mvn -B test
    • 期望累计 34 + 1(error code 断言扩展) + 6(service delete unit) + 6(controller delete IT) = 47 个,全绿
  • Step 3.4 提交

    • git commit -m "feat(mod): DELETE /api/modules/{id} controller REQ-MOD-003"

提交计划

  • feat(common): error code MOD_HAS_REFERENCES + delete VO REQ-MOD-003(覆盖 Task 1)
  • feat(mod): delete module service REQ-MOD-003(覆盖 Task 2)
  • feat(mod): DELETE /api/modules/{id} controller REQ-MOD-003(覆盖 Task 3)