--- req_id: REQ-MOD-003 date: 2026-05-06 module: module_mod --- # Spec: REQ-MOD-003 — 模块删除 ## 目标 实现后端 `DELETE /api/modules/{id}` 接口:对指定模块做**软删除**(写入 `bDeleted=1` / `tDeletedDate=now` / `sDeletedBy=NULL`),并在删除前校验子模块引用,避免破坏树结构完整性。 ## 输入 / 触发 **接口**:`DELETE /api/modules/{id}`,无请求体。`{id}` = `tModule.iIncrement`。 **鉴权**:契约要求 `Authorization: Bearer ` + 权限码 `MOD:DELETE`。本 REQ 沿用 SecurityConfig permitAll;Controller 上写 Javadoc:`REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:DELETE')")`。 ## 输出 / 结果 **HTTP 200,响应体**: ```json { "code": 200, "message": "操作成功", "data": { "iIncrement": 12, "bDeleted": true }, "timestamp": 1746528600000 } ``` 返回精简 VO:仅 `iIncrement` + `bDeleted`,足以让前端在表格里直接更新该行的删除标记。新增 `ModuleDeleteResultVO` 单独承载该结构(避免复用 `ModuleVO` 暴露不必要字段)。 ## 业务规则 1. **目标存在且未被软删除**:`SELECT iIncrement, bDeleted FROM tModule WHERE iIncrement = {id}`。`null` 或 `bDeleted = 1` → `BizException(MOD_NOT_FOUND)` (40421)。 2. **子模块引用检查**:`SELECT COUNT(*) FROM tModule WHERE iParentId = {id} AND bDeleted = 0`。`> 0` → `BizException(MOD_HAS_REFERENCES)` (40912)。 3. **外部业务引用**:本期 schema 无其他业务表通过 FK 引用 `tModule`(V1 SQL 中无外部 FK 指向 tModule)。本 REQ 仅检查子模块;后续若新增模块引用 tModule(如菜单 / 权限分配),需在该模块的 service 层追加引用检查并复用 `MOD_HAS_REFERENCES`。 4. **软删除字段写入**: - `bDeleted = 1` - `tDeletedDate = LocalDateTime.now()` - `sDeletedBy = NULL`(REQ-USR-004 后由登录上下文回填) - 其他字段保持原值 5. **已删除模块不可再删**:bDeleted=1 直接走 40421(与 #1 等效)。 6. **重复请求语义**:连续两次 DELETE 同一 id,第一次成功(200 + bDeleted=true),第二次 40421。**非幂等**——但响应可预测,不会破坏数据。 7. **事务边界**:service 方法 `@Transactional`,校验 + 软删除单事务内完成。 ## 边界与约束 ### 鉴权策略 沿用 REQ-MOD-001/002 SecurityConfig permitAll。 ### 错误码映射 | 场景 | 错误码 | ErrorCode 枚举常量 | |---|---|---| | `{id}` 不存在或已软删除 | 40421 | `MOD_NOT_FOUND`(已存在) | | 存在未软删除子模块 | 40912 | `MOD_HAS_REFERENCES`(**新增**) | | 服务端兜底 | 50000 | `INTERNAL_ERROR` | ### 并发 - 用 `moduleMapper.update(entity, wrapper)` 加 `bDeleted = 0` 条件,让"两个并发删除"中只有一个能写成功(影响行数 1),另一个影响 0 行。Service 检查影响行数:`= 0` → 返回 40421(视为已被并发删除)。 - 不引入乐观锁版本号。 ### 性能 - 子模块计数走 `idx_parent` 索引,O(1)。 ## 依赖的 schema 表 / 字段 **写表**:`tModule` | 字段 | 行为 | |---|---| | `iIncrement` | 路径参数 `{id}` 定位行,**不修改** | | `bDeleted` | 0 → 1 | | `tDeletedDate` | 写入 `LocalDateTime.now()` | | `sDeletedBy` | 写入 `NULL`(REQ-USR-004 后回填) | | 其他全部字段 | **不修改** | **索引利用**: - `pk_module`:定位 `{id}` - `idx_parent`:子模块计数 **外键**:本期无其他表 FK 指向 tModule,无需额外检查。 ## 依赖的接口 无(独立接口)。 ## 验收标准 ### 功能正确性 1. **正向 — 叶子模块删除**:先建一个无子模块的 root,DELETE,返回 200 + `data.iIncrement` + `data.bDeleted=true`。DB 中 `bDeleted=1` / `tDeletedDate` 非空 / `sDeletedBy=NULL` / 其他字段保持原值。 2. **正向 — 删除后再 GET 列表(REQ-MOD-004)跳过该模块**:本 REQ 不实现 GET,但通过 `selectById` 后断言查询接口的过滤效果。 3. **目标不存在**:DELETE `/api/modules/999999`,返回 `40421`。 4. **目标已软删除**:手工 update bDeleted=1 后 DELETE,返回 `40421`。 5. **存在未删除子模块**:先建 parent + child,DELETE parent,返回 `40912`;DB 中 parent.bDeleted 仍为 0。 6. **存在已删除子模块(不阻塞)**:先建 parent + child,DELETE child(成功),再 DELETE parent,应成功(已删子模块不计入引用)。 7. **重复 DELETE**:第二次返回 40421。 8. **响应 VO 字段精简**:仅含 iIncrement + bDeleted(断言 sDisplayType / sProcedureName 等不在响应里)。 ### 接口契约一致性 - 响应格式 `{code, message, data, timestamp}`。 - 错误码:200 / 40421 / 40912 / 50000。 - 不回显堆栈。 ### 测试覆盖 - **单元测试** `ModuleServiceImplTest` 追加(mock ModuleMapper): - delete_targetNotFound_throws40421 - delete_targetAlreadyDeleted_throws40421 - delete_hasUndeletedChildren_throws40912 - delete_leafModule_writesSoftDeleteFields_returnsResult - delete_softDeletedChildren_doesNotBlock - delete_concurrentRace_throws40421(mock update 影响 0 行) - **集成测试** `ModuleControllerIT` 追加: - delete_validLeaf_returns200WithBDeletedTrue(先 mapper.insert,再 DELETE,再 selectById 验证 bDeleted=true / tDeletedDate 非空) - delete_targetNotFound_returns40421 - delete_targetAlreadyDeleted_returns40421 - delete_hasUndeletedChildren_returns40912 - delete_softDeletedChildren_doesNotBlock_returns200 - delete_responseVOContainsOnlyIIncrementAndBDeleted ### 代码与文档 - `// REQ-MOD-003 模块删除` 注释贴在 Controller 方法、Service 方法、新增 ErrorCode 枚举常量上。 - 提交按 `feat(mod): REQ-MOD-003` 规范。 - 不引入 docs/04 § 零 技术栈外的依赖。