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 暴露不必要字段)。

业务规则

  1. 目标存在且未被软删除SELECT iIncrement, bDeleted FROM tModule WHERE iIncrement = {id}nullbDeleted = 1BizException(MOD_NOT_FOUND) (40421)。
  2. 子模块引用检查SELECT COUNT(*) FROM tModule WHERE iParentId = {id} AND bDeleted = 0> 0BizException(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): <subject> REQ-MOD-003 规范。
  • 不引入 docs/04 § 零 技术栈外的依赖。