--- 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 `(mod): 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=true` → `BizException(40400, "模块不存在或已删除")` 2. `moduleMapper.hasActiveChildren(id)` → true → `BizException(40901, "模块仍有未删除子节点")` 3. 构造 `Module entity`:`setIIncrement(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)=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";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 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 |