2026-05-06-REQ-MOD-003.md
5.75 KB
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 <accessToken> + 权限码 MOD:DELETE。本 REQ 沿用 SecurityConfig permitAll;Controller 上写 Javadoc:REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:DELETE')")。
输出 / 结果
HTTP 200,响应体:
{
"code": 200,
"message": "操作成功",
"data": {
"iIncrement": 12,
"bDeleted": true
},
"timestamp": 1746528600000
}
返回精简 VO:仅 iIncrement + bDeleted,足以让前端在表格里直接更新该行的删除标记。新增 ModuleDeleteResultVO 单独承载该结构(避免复用 ModuleVO 暴露不必要字段)。
业务规则
-
目标存在且未被软删除:
SELECT iIncrement, bDeleted FROM tModule WHERE iIncrement = {id}。null或bDeleted = 1→BizException(MOD_NOT_FOUND)(40421)。 -
子模块引用检查:
SELECT COUNT(*) FROM tModule WHERE iParentId = {id} AND bDeleted = 0。> 0→BizException(MOD_HAS_REFERENCES)(40912)。 -
外部业务引用:本期 schema 无其他业务表通过 FK 引用
tModule(V1 SQL 中无外部 FK 指向 tModule)。本 REQ 仅检查子模块;后续若新增模块引用 tModule(如菜单 / 权限分配),需在该模块的 service 层追加引用检查并复用MOD_HAS_REFERENCES。 -
软删除字段写入:
bDeleted = 1tDeletedDate = LocalDateTime.now()-
sDeletedBy = NULL(REQ-USR-004 后由登录上下文回填) - 其他字段保持原值
- 已删除模块不可再删:bDeleted=1 直接走 40421(与 #1 等效)。
- 重复请求语义:连续两次 DELETE 同一 id,第一次成功(200 + bDeleted=true),第二次 40421。非幂等——但响应可预测,不会破坏数据。
-
事务边界: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,无需额外检查。
依赖的接口
无(独立接口)。
验收标准
功能正确性
-
正向 — 叶子模块删除:先建一个无子模块的 root,DELETE,返回 200 +
data.iIncrement+data.bDeleted=true。DB 中bDeleted=1/tDeletedDate非空 /sDeletedBy=NULL/ 其他字段保持原值。 -
正向 — 删除后再 GET 列表(REQ-MOD-004)跳过该模块:本 REQ 不实现 GET,但通过
selectById后断言查询接口的过滤效果。 -
目标不存在:DELETE
/api/modules/999999,返回40421。 -
目标已软删除:手工 update bDeleted=1 后 DELETE,返回
40421。 -
存在未删除子模块:先建 parent + child,DELETE parent,返回
40912;DB 中 parent.bDeleted 仍为 0。 - 存在已删除子模块(不阻塞):先建 parent + child,DELETE child(成功),再 DELETE parent,应成功(已删子模块不计入引用)。
- 重复 DELETE:第二次返回 40421。
- 响应 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): <subject> REQ-MOD-003规范。 - 不引入 docs/04 § 零 技术栈外的依赖。