Commit f82673c929a36374d22feef658ae46d351233e55
1 parent
24196599
docs(mod): review approval REQ-MOD-003 round 2
Showing
4 changed files
with
318 additions
and
1 deletions
docs/08-模块任务管理.md
docs/superpowers/plans/2026-05-06-REQ-MOD-003.md
0 → 100644
| 1 | +--- | |
| 2 | +req_id: REQ-MOD-003 | |
| 3 | +date: 2026-05-06 | |
| 4 | +spec_ref: docs/superpowers/specs/2026-05-06-REQ-MOD-003.md | |
| 5 | +--- | |
| 6 | + | |
| 7 | +# REQ-MOD-003 模块删除 Implementation Plan | |
| 8 | + | |
| 9 | +> **Execution:** Parent skill `feature-tdd` executes this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. | |
| 10 | + | |
| 11 | +**Goal:** 实现 `DELETE /api/modules/{id}`:在校验子模块未删除引用的前提下,对目标做软删除(写 bDeleted/tDeletedDate/sDeletedBy)。 | |
| 12 | + | |
| 13 | +**Architecture:** 复用 REQ-MOD-001/002 已建立的体系。Service 先 selectById 校验目标存在 + 未删除(40421),再 selectCount 子模块未删除引用(40912),最后用 `mapper.update(entity, wrapper)` 加 `bDeleted = 0` 条件做并发安全的软删除(影响 0 行视为并发删除 → 40421)。返回精简 VO。 | |
| 14 | + | |
| 15 | +**Tech Stack:** 沿用 REQ-MOD-001/002(Spring Boot 3.2.5 + MyBatis-Plus 3.5.7 + JUnit 5 + Mockito)。 | |
| 16 | + | |
| 17 | +--- | |
| 18 | + | |
| 19 | +## Schema 改动 | |
| 20 | + | |
| 21 | +无(软删除字段 `bDeleted` / `tDeletedDate` / `sDeletedBy` 在 V1 已建)。 | |
| 22 | + | |
| 23 | +## 文件变更清单 | |
| 24 | + | |
| 25 | +- 修改: `backend/src/main/java/com/xly/erp/common/response/ErrorCode.java` — 追加 `MOD_HAS_REFERENCES(40912, "存在子模块或外部业务引用,禁止删除")` | |
| 26 | +- 创建: `backend/src/main/java/com/xly/erp/module/mod/vo/ModuleDeleteResultVO.java` — 精简 VO(iIncrement + bDeleted) | |
| 27 | +- 修改: `backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java` — 追加 `delete(Integer id): ModuleDeleteResultVO` | |
| 28 | +- 修改: `backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java` — 实现 delete | |
| 29 | +- 修改: `backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java` — 追加 `@DeleteMapping("/{id}")` | |
| 30 | +- 修改: `backend/src/test/java/com/xly/erp/common/response/ApiResponseTest.java` — 追加 1 个错误码断言 | |
| 31 | +- 修改: `backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java` — 追加 6 个 delete 单测 | |
| 32 | +- 修改: `backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java` — 追加 6 个 DELETE 集成测试 | |
| 33 | + | |
| 34 | +## 任务步骤 | |
| 35 | + | |
| 36 | +### Task 1: 错误码 + 精简 VO | |
| 37 | + | |
| 38 | +**Files:** | |
| 39 | +- Modify: `backend/src/main/java/com/xly/erp/common/response/ErrorCode.java` | |
| 40 | +- Create: `backend/src/main/java/com/xly/erp/module/mod/vo/ModuleDeleteResultVO.java` | |
| 41 | +- Modify: `backend/src/test/java/com/xly/erp/common/response/ApiResponseTest.java` | |
| 42 | + | |
| 43 | +**API shape:** | |
| 44 | +- `MOD_HAS_REFERENCES(40912, "存在子模块或外部业务引用,禁止删除")` | |
| 45 | +- `ModuleDeleteResultVO` 字段:`Integer iIncrement` + `Boolean bDeleted`(带 `@Data` + 静态工厂 `of(Integer id, Boolean deleted)`) | |
| 46 | + | |
| 47 | +- [ ] **Step 1.1 写失败断言** | |
| 48 | + - 在 `ApiResponseTest#errorCode_constantsMatchDocs05Spec` 末尾追加: | |
| 49 | + `assertThat(ErrorCode.MOD_HAS_REFERENCES.getCode()).isEqualTo(40912);` | |
| 50 | + - 子会话 FAIL(枚举常量不存在) | |
| 51 | + | |
| 52 | +- [ ] **Step 1.2 追加枚举常量 + 创建 VO** | |
| 53 | + | |
| 54 | +- [ ] **Step 1.3 子会话验证 ApiResponseTest 全绿** | |
| 55 | + | |
| 56 | +- [ ] **Step 1.4 提交** | |
| 57 | + - `git commit -m "feat(common): error code MOD_HAS_REFERENCES + delete VO REQ-MOD-003"` | |
| 58 | + | |
| 59 | +--- | |
| 60 | + | |
| 61 | +### Task 2: ModuleService.delete — 业务逻辑(mock 单元测试) | |
| 62 | + | |
| 63 | +**Files:** | |
| 64 | +- Modify: `backend/src/main/java/com/xly/erp/module/mod/service/ModuleService.java`(追加方法签名) | |
| 65 | +- Modify: `backend/src/main/java/com/xly/erp/module/mod/service/impl/ModuleServiceImpl.java` | |
| 66 | +- Modify: `backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java`(追加 6 个测试) | |
| 67 | + | |
| 68 | +**API shape:** | |
| 69 | +- `interface ModuleService` 追加:`ModuleDeleteResultVO delete(Integer id)` | |
| 70 | +- 实现步骤(写在 plan 锁定): | |
| 71 | + 1. `target = moduleMapper.selectById(id)`;`target == null || target.bDeleted == true` → `BizException(MOD_NOT_FOUND)`(40421) | |
| 72 | + 2. 子模块计数:`childCount = moduleMapper.selectCount(LambdaQueryWrapper.eq(iParentId, id).eq(bDeleted, false))` | |
| 73 | + - `childCount > 0` → `BizException(MOD_HAS_REFERENCES)`(40912) | |
| 74 | + 3. 构造**只含软删除三件套 + 主键**的更新 entity(避免触碰其他字段): | |
| 75 | + ``` | |
| 76 | + ModuleEntity patch = new ModuleEntity(); | |
| 77 | + patch.setIIncrement(id); | |
| 78 | + patch.setBDeleted(true); | |
| 79 | + patch.setTDeletedDate(LocalDateTime.now()); | |
| 80 | + patch.setSDeletedBy(null); // FieldStrategy 默认 NOT_NULL,会被 MP 跳过;这里靠 IGNORED 或显式 update wrapper 写入。 | |
| 81 | + ``` | |
| 82 | + **方式选择**:用 `moduleMapper.update(patch, new LambdaUpdateWrapper<ModuleEntity>().eq(iIncrement, id).eq(bDeleted, false))`,确保只更新仍未删除的目标,且 `update` 影响行数为并发兜底信号。 | |
| 83 | + 4. `int affected = moduleMapper.update(...)`;`affected == 0` → `BizException(MOD_NOT_FOUND)`(视为目标在校验后被并发删除) | |
| 84 | + 5. `return ModuleDeleteResultVO.of(id, true)` | |
| 85 | +- 标 `@Transactional(rollbackFor = Exception.class)` | |
| 86 | + | |
| 87 | +- [ ] **Step 2.1 写失败测试(6 个)** | |
| 88 | + - `delete_targetNotFound_throws40421`:`selectById` → null | |
| 89 | + - `delete_targetAlreadyDeleted_throws40421`:target.bDeleted=true | |
| 90 | + - `delete_hasUndeletedChildren_throws40912`:selectCount > 0 | |
| 91 | + - `delete_leafModule_returnsResult`:selectCount=0、update 返回 1,断言 VO + 断言传给 update 的 entity 字段(bDeleted=true、tDeletedDate 非 null、iIncrement 正确) | |
| 92 | + - `delete_softDeletedChildren_doesNotBlock`:selectCount=0(已删子不计入),update 返回 1,断言成功 | |
| 93 | + - `delete_concurrentRace_throws40421`:selectById 返回未删除目标,selectCount=0,update 返回 0 → 抛 40421 | |
| 94 | + - 测试方式:`@ExtendWith(MockitoExtension.class)` + `ArgumentCaptor<ModuleEntity>` 捕获 `update` 实参;用 `any(Wrapper.class)` 占位 wrapper | |
| 95 | + - 子会话: FAIL(方法不存在) | |
| 96 | + | |
| 97 | +- [ ] **Step 2.2 实现 delete** | |
| 98 | + - 注意:`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()`。 | |
| 99 | + | |
| 100 | +- [ ] **Step 2.3 子会话确认 ModuleServiceImplTest 全部绿** | |
| 101 | + - 累计:6 (create) + 8 (update) + 6 (delete) = 20 | |
| 102 | + | |
| 103 | +- [ ] **Step 2.4 提交** | |
| 104 | + - `git commit -m "feat(mod): delete module service REQ-MOD-003"` | |
| 105 | + | |
| 106 | +--- | |
| 107 | + | |
| 108 | +### Task 3: ModuleController DELETE 端点 + 端到端 IT | |
| 109 | + | |
| 110 | +**Files:** | |
| 111 | +- Modify: `backend/src/main/java/com/xly/erp/module/mod/controller/ModuleController.java` | |
| 112 | +- Modify: `backend/src/test/java/com/xly/erp/module/mod/controller/ModuleControllerIT.java`(追加 6 个集成用例) | |
| 113 | + | |
| 114 | +**API shape:** | |
| 115 | +- 新方法: | |
| 116 | + ``` | |
| 117 | + @DeleteMapping("/{id}") | |
| 118 | + public ApiResponse<ModuleDeleteResultVO> delete(@PathVariable Integer id) | |
| 119 | + ``` | |
| 120 | +- Javadoc:`REQ-MOD-003 模块删除 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:DELETE')")` | |
| 121 | + | |
| 122 | +- [ ] **Step 3.1 写失败测试(6 个)** | |
| 123 | + - `delete_validLeaf_returns200WithBDeletedTrue`:mapper.insert 一条 → DELETE → 断言 200 + data.bDeleted=true + selectById 验证 DB 中 bDeleted=true / tDeletedDate 非 null | |
| 124 | + - `delete_targetNotFound_returns40421`:DELETE /api/modules/999999 | |
| 125 | + - `delete_targetAlreadyDeleted_returns40421`:mapper.insert 一条并立刻把 bDeleted 置 true,DELETE 返回 40421 | |
| 126 | + - `delete_hasUndeletedChildren_returns40912`:parent + child(bDeleted=0),DELETE parent → 40912;selectById parent 验证 bDeleted 仍 false | |
| 127 | + - `delete_softDeletedChildren_doesNotBlock_returns200`:先 DELETE child(应成功),再 DELETE parent → 200 | |
| 128 | + - `delete_responseVOContainsOnlyIIncrementAndBDeleted`:断言 `$.data` 路径只有 iIncrement + bDeleted 两个字段(用 jsonPath `$.data.sProcedureName` 不存在) | |
| 129 | + - 测试方式:`@SpringBootTest @AutoConfigureMockMvc @Transactional @Rollback` + `@Autowired ModuleMapper` 直接预置数据 | |
| 130 | + - 子会话: FAIL(端点不存在) | |
| 131 | + | |
| 132 | +- [ ] **Step 3.2 实现 DELETE 端点** | |
| 133 | + - 子会话: PASS | |
| 134 | + | |
| 135 | +- [ ] **Step 3.3 跑全量 backend 测试** | |
| 136 | + - `cd backend && mvn -B test` | |
| 137 | + - 期望累计 34 + 1(error code 断言扩展) + 6(service delete unit) + 6(controller delete IT) = 47 个,全绿 | |
| 138 | + | |
| 139 | +- [ ] **Step 3.4 提交** | |
| 140 | + - `git commit -m "feat(mod): DELETE /api/modules/{id} controller REQ-MOD-003"` | |
| 141 | + | |
| 142 | +--- | |
| 143 | + | |
| 144 | +## 提交计划 | |
| 145 | + | |
| 146 | +- `feat(common): error code MOD_HAS_REFERENCES + delete VO REQ-MOD-003`(覆盖 Task 1) | |
| 147 | +- `feat(mod): delete module service REQ-MOD-003`(覆盖 Task 2) | |
| 148 | +- `feat(mod): DELETE /api/modules/{id} controller REQ-MOD-003`(覆盖 Task 3) | ... | ... |
docs/superpowers/reviews/2026-05-06-REQ-MOD-003.md
0 → 100644
| 1 | +--- | |
| 2 | +req_id: REQ-MOD-003 | |
| 3 | +date: 2026-05-06 | |
| 4 | +round: 2 | |
| 5 | +reviewer: superpower-code-reviewer | |
| 6 | +--- | |
| 7 | + | |
| 8 | +# Review: REQ-MOD-003 — round 2 | |
| 9 | + | |
| 10 | +## 结论 | |
| 11 | +approve | |
| 12 | + | |
| 13 | +## Must-fix | |
| 14 | +(无;round 1 两条 must_fix 已在 commit 2419659 中修复) | |
| 15 | + | |
| 16 | +## Nice-to-have | |
| 17 | + | |
| 18 | +- backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java:16 — `org.mockito.ArgumentMatchers` 仍未使用(round 1 提过,pre-existing;可顺手清掉)。 | |
| 19 | +- backend/src/test/java/com/xly/erp/module/mod/service/ModuleServiceImplTest.java:358/375/387 — `(Wrapper<ModuleEntity>) any()` 强转触发 unchecked 警告,可在类上 `@SuppressWarnings("unchecked")` 或就近用 `ArgumentMatchers.<Wrapper<ModuleEntity>>any()`。 | |
| 20 | +- 可选:增加 `delete_writesSDeletedByNull_onSoftDelete` 单元测试,用 ArgumentCaptor 捕 `LambdaUpdateWrapper` 解析 `wrapper.getSqlSet()` 断言含 `sDeletedBy=NULL / bDeleted=1 / tDeletedDate=...`,把 SET 子句的列覆盖也在单元层钉一遍(目前 SET 列内容仅由 IT 兜底)。 | |
| 21 | +- docs/05-API接口契约.md § REQ-MOD-003 写「40912 响应附 data.references」,spec 未实现;属于已知契约漂移,留给 module-report 时统一对齐。 | |
| 22 | + | |
| 23 | +## 反例 / 测试覆盖缺口 | |
| 24 | + | |
| 25 | +Round 1 两条 must_fix 均已落实: | |
| 26 | + | |
| 27 | +1. `ModuleServiceImpl.delete()` 改为 `moduleMapper.update(null, uw)` + `LambdaUpdateWrapper.set(BDeleted,true).set(TDeletedDate,now()).set(SDeletedBy,null)`;`eq(BDeleted,false)` 并发兜底保留;`affected==0 → 40421` 保留。三件套全部由 wrapper 显式声明,**绕开** `iParentId.FieldStrategy.IGNORED` 副作用。 | |
| 28 | +2. `ModuleControllerIT#delete_preservesOtherFields_onChildModule` 已新增:用自定义字段值(sDisplayType='接口' / sModuleType='AUDIT' / sManageDeptEn='OPS' / bShowPermission=true / sModuleNameZh='待保留中文名' / iSortOrder=7)建 child(parentId),DELETE 后 reload 断言 8 个字段全部保持原值 + bDeleted=true + tDeletedDate 非 null。 | |
| 29 | + | |
| 30 | +**单元测试降级合理性**:架构改动后 entity 参数为 null,原 ArgumentCaptor 对 entity 字段的断言失去对象;MP 真正写入的 SET 列在 wrapper 内部 SqlSegment,单元层断言列覆盖复杂度高且偏离职责。新 IT 在真实 MySQL 端到端验证「除三件套外其他列保持原值 + iParentId 不被清空」,比 mock 层 ArgumentCaptor 严格得多。`verify(moduleMapper).update((ModuleEntity) isNull(), ...)` 把"entity 参数必须是 null"这一架构不变量钉死,防止未来误回滚到 entity-driven update。整体是 mock 层小幅放宽 + IT 层显著加强的净增强。 | |
| 31 | + | |
| 32 | +非阻塞遗留:(a) docs/05 § REQ-MOD-003 `data.references` 描述与实现不一致;(b) 单元测试 `ArgumentMatchers` 未使用 import;(c) 重复 DELETE 集成层显式用例缺失(间接覆盖足够)。 | ... | ... |
docs/superpowers/specs/2026-05-06-REQ-MOD-003.md
0 → 100644
| 1 | +--- | |
| 2 | +req_id: REQ-MOD-003 | |
| 3 | +date: 2026-05-06 | |
| 4 | +module: module_mod | |
| 5 | +--- | |
| 6 | + | |
| 7 | +# Spec: REQ-MOD-003 — 模块删除 | |
| 8 | + | |
| 9 | +## 目标 | |
| 10 | + | |
| 11 | +实现后端 `DELETE /api/modules/{id}` 接口:对指定模块做**软删除**(写入 `bDeleted=1` / `tDeletedDate=now` / `sDeletedBy=NULL`),并在删除前校验子模块引用,避免破坏树结构完整性。 | |
| 12 | + | |
| 13 | +## 输入 / 触发 | |
| 14 | + | |
| 15 | +**接口**:`DELETE /api/modules/{id}`,无请求体。`{id}` = `tModule.iIncrement`。 | |
| 16 | + | |
| 17 | +**鉴权**:契约要求 `Authorization: Bearer <accessToken>` + 权限码 `MOD:DELETE`。本 REQ 沿用 SecurityConfig permitAll;Controller 上写 Javadoc:`REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:DELETE')")`。 | |
| 18 | + | |
| 19 | +## 输出 / 结果 | |
| 20 | + | |
| 21 | +**HTTP 200,响应体**: | |
| 22 | + | |
| 23 | +```json | |
| 24 | +{ | |
| 25 | + "code": 200, | |
| 26 | + "message": "操作成功", | |
| 27 | + "data": { | |
| 28 | + "iIncrement": 12, | |
| 29 | + "bDeleted": true | |
| 30 | + }, | |
| 31 | + "timestamp": 1746528600000 | |
| 32 | +} | |
| 33 | +``` | |
| 34 | + | |
| 35 | +返回精简 VO:仅 `iIncrement` + `bDeleted`,足以让前端在表格里直接更新该行的删除标记。新增 `ModuleDeleteResultVO` 单独承载该结构(避免复用 `ModuleVO` 暴露不必要字段)。 | |
| 36 | + | |
| 37 | +## 业务规则 | |
| 38 | + | |
| 39 | +1. **目标存在且未被软删除**:`SELECT iIncrement, bDeleted FROM tModule WHERE iIncrement = {id}`。`null` 或 `bDeleted = 1` → `BizException(MOD_NOT_FOUND)` (40421)。 | |
| 40 | +2. **子模块引用检查**:`SELECT COUNT(*) FROM tModule WHERE iParentId = {id} AND bDeleted = 0`。`> 0` → `BizException(MOD_HAS_REFERENCES)` (40912)。 | |
| 41 | +3. **外部业务引用**:本期 schema 无其他业务表通过 FK 引用 `tModule`(V1 SQL 中无外部 FK 指向 tModule)。本 REQ 仅检查子模块;后续若新增模块引用 tModule(如菜单 / 权限分配),需在该模块的 service 层追加引用检查并复用 `MOD_HAS_REFERENCES`。 | |
| 42 | +4. **软删除字段写入**: | |
| 43 | + - `bDeleted = 1` | |
| 44 | + - `tDeletedDate = LocalDateTime.now()` | |
| 45 | + - `sDeletedBy = NULL`(REQ-USR-004 后由登录上下文回填) | |
| 46 | + - 其他字段保持原值 | |
| 47 | +5. **已删除模块不可再删**:bDeleted=1 直接走 40421(与 #1 等效)。 | |
| 48 | +6. **重复请求语义**:连续两次 DELETE 同一 id,第一次成功(200 + bDeleted=true),第二次 40421。**非幂等**——但响应可预测,不会破坏数据。 | |
| 49 | +7. **事务边界**:service 方法 `@Transactional`,校验 + 软删除单事务内完成。 | |
| 50 | + | |
| 51 | +## 边界与约束 | |
| 52 | + | |
| 53 | +### 鉴权策略 | |
| 54 | + | |
| 55 | +沿用 REQ-MOD-001/002 SecurityConfig permitAll。 | |
| 56 | + | |
| 57 | +### 错误码映射 | |
| 58 | + | |
| 59 | +| 场景 | 错误码 | ErrorCode 枚举常量 | | |
| 60 | +|---|---|---| | |
| 61 | +| `{id}` 不存在或已软删除 | 40421 | `MOD_NOT_FOUND`(已存在) | | |
| 62 | +| 存在未软删除子模块 | 40912 | `MOD_HAS_REFERENCES`(**新增**) | | |
| 63 | +| 服务端兜底 | 50000 | `INTERNAL_ERROR` | | |
| 64 | + | |
| 65 | +### 并发 | |
| 66 | + | |
| 67 | +- 用 `moduleMapper.update(entity, wrapper)` 加 `bDeleted = 0` 条件,让"两个并发删除"中只有一个能写成功(影响行数 1),另一个影响 0 行。Service 检查影响行数:`= 0` → 返回 40421(视为已被并发删除)。 | |
| 68 | +- 不引入乐观锁版本号。 | |
| 69 | + | |
| 70 | +### 性能 | |
| 71 | + | |
| 72 | +- 子模块计数走 `idx_parent` 索引,O(1)。 | |
| 73 | + | |
| 74 | +## 依赖的 schema 表 / 字段 | |
| 75 | + | |
| 76 | +**写表**:`tModule` | |
| 77 | + | |
| 78 | +| 字段 | 行为 | | |
| 79 | +|---|---| | |
| 80 | +| `iIncrement` | 路径参数 `{id}` 定位行,**不修改** | | |
| 81 | +| `bDeleted` | 0 → 1 | | |
| 82 | +| `tDeletedDate` | 写入 `LocalDateTime.now()` | | |
| 83 | +| `sDeletedBy` | 写入 `NULL`(REQ-USR-004 后回填) | | |
| 84 | +| 其他全部字段 | **不修改** | | |
| 85 | + | |
| 86 | +**索引利用**: | |
| 87 | +- `pk_module`:定位 `{id}` | |
| 88 | +- `idx_parent`:子模块计数 | |
| 89 | + | |
| 90 | +**外键**:本期无其他表 FK 指向 tModule,无需额外检查。 | |
| 91 | + | |
| 92 | +## 依赖的接口 | |
| 93 | + | |
| 94 | +无(独立接口)。 | |
| 95 | + | |
| 96 | +## 验收标准 | |
| 97 | + | |
| 98 | +### 功能正确性 | |
| 99 | + | |
| 100 | +1. **正向 — 叶子模块删除**:先建一个无子模块的 root,DELETE,返回 200 + `data.iIncrement` + `data.bDeleted=true`。DB 中 `bDeleted=1` / `tDeletedDate` 非空 / `sDeletedBy=NULL` / 其他字段保持原值。 | |
| 101 | +2. **正向 — 删除后再 GET 列表(REQ-MOD-004)跳过该模块**:本 REQ 不实现 GET,但通过 `selectById` 后断言查询接口的过滤效果。 | |
| 102 | +3. **目标不存在**:DELETE `/api/modules/999999`,返回 `40421`。 | |
| 103 | +4. **目标已软删除**:手工 update bDeleted=1 后 DELETE,返回 `40421`。 | |
| 104 | +5. **存在未删除子模块**:先建 parent + child,DELETE parent,返回 `40912`;DB 中 parent.bDeleted 仍为 0。 | |
| 105 | +6. **存在已删除子模块(不阻塞)**:先建 parent + child,DELETE child(成功),再 DELETE parent,应成功(已删子模块不计入引用)。 | |
| 106 | +7. **重复 DELETE**:第二次返回 40421。 | |
| 107 | +8. **响应 VO 字段精简**:仅含 iIncrement + bDeleted(断言 sDisplayType / sProcedureName 等不在响应里)。 | |
| 108 | + | |
| 109 | +### 接口契约一致性 | |
| 110 | + | |
| 111 | +- 响应格式 `{code, message, data, timestamp}`。 | |
| 112 | +- 错误码:200 / 40421 / 40912 / 50000。 | |
| 113 | +- 不回显堆栈。 | |
| 114 | + | |
| 115 | +### 测试覆盖 | |
| 116 | + | |
| 117 | +- **单元测试** `ModuleServiceImplTest` 追加(mock ModuleMapper): | |
| 118 | + - delete_targetNotFound_throws40421 | |
| 119 | + - delete_targetAlreadyDeleted_throws40421 | |
| 120 | + - delete_hasUndeletedChildren_throws40912 | |
| 121 | + - delete_leafModule_writesSoftDeleteFields_returnsResult | |
| 122 | + - delete_softDeletedChildren_doesNotBlock | |
| 123 | + - delete_concurrentRace_throws40421(mock update 影响 0 行) | |
| 124 | + | |
| 125 | +- **集成测试** `ModuleControllerIT` 追加: | |
| 126 | + - delete_validLeaf_returns200WithBDeletedTrue(先 mapper.insert,再 DELETE,再 selectById 验证 bDeleted=true / tDeletedDate 非空) | |
| 127 | + - delete_targetNotFound_returns40421 | |
| 128 | + - delete_targetAlreadyDeleted_returns40421 | |
| 129 | + - delete_hasUndeletedChildren_returns40912 | |
| 130 | + - delete_softDeletedChildren_doesNotBlock_returns200 | |
| 131 | + - delete_responseVOContainsOnlyIIncrementAndBDeleted | |
| 132 | + | |
| 133 | +### 代码与文档 | |
| 134 | + | |
| 135 | +- `// REQ-MOD-003 模块删除` 注释贴在 Controller 方法、Service 方法、新增 ErrorCode 枚举常量上。 | |
| 136 | +- 提交按 `feat(mod): <subject> REQ-MOD-003` 规范。 | |
| 137 | +- 不引入 docs/04 § 零 技术栈外的依赖。 | ... | ... |