2026-04-29-REQ-MOD-003.md 6.59 KB

req_id: REQ-MOD-003 date: 2026-04-29

module: module_mod

Spec: REQ-MOD-003 — 模块删除

目标

软删除一个已有模块(bDeleted=0 → 1 + 审计字段填充),并阻止破坏树形数据完整性的删除(已存在未删子模块时拒绝)。

输入 / 触发

HTTP 接口(docs/05 § REQ-MOD-003)

  • Method / Path: DELETE /api/mod/modules/{id}(path 参数 {id} = tModule.iIncrement
  • 无请求 body
  • Auth: 必需(沿用 MOD-001 stub:路径已在 SecurityConfig /api/mod/** permitAll,USR-004 完成后改 hasAuthority('SUPER_ADMIN')
  • Permission: 仅超级管理员(stub 期不强制)

鉴权与上下文

JWT Filter 解析 token 写 principal=sUserNo;伪造 token → code=20001;缺失 token → permitAll 透传。sDeletedBySecurityContextHelper.currentUserNo(),匿名状态回退 stubProps.stubUserNo(与 MOD-001 sCreatedBy 同策略)。

输出 / 结果

成功响应

{ "code": 0, "msg": "ok", "data": null }

持久化效果

UPDATE tModule SET bDeleted=1, tDeletedDate=NOW(), sDeletedBy='<userNo>' WHERE iIncrement={id}。其他字段保持原值(依赖 MyBatis-Plus FieldStrategy.NOT_NULL 仅写非 null 字段)。

业务规则

  1. 目标存在性SELECT * FROM tModule WHERE iIncrement={id};行不存在 bDeleted=1BizException(40400, "模块不存在或已删除")。已删模块再次 DELETE 也返回 40400(删除接口非幂等设计——业务上想表达"目标已不存在")。
  2. 子模块拦截:检查 tModule WHERE iParentId={id} AND bDeleted=0;存在 → BizException(40901, "模块仍有未删除子节点")。新增 mapper 方法 boolean hasActiveChildren(Integer parentId),实现用 @Select("SELECT 1 FROM tModule WHERE iParentId = #{parentId} AND bDeleted = 0 LIMIT 1") + Java default 包装。
  3. 外部引用拦截(40902):docs/05 列了该错误码,但 docs/03 § tModule 业务注记明确"与本期其他表无外键关系"——本期 schema 中不存在菜单/权限/角色表引用 tModule 的字段,本 REQ 不实现 40902 校验。spec 显式记录该决策:当 USR 或后续模块引入 tMenu / tRole 等表并通过 iModuleId 等字段引用 tModule 时,再回头扩展 ModuleService#delete 加引用查询。当前实现保持纯 MOD 模块自包含。
  4. 软删除字段填充:构造 Module entity 仅 set iIncrement / bDeleted=true / tDeletedDate=LocalDateTime.now() / sDeletedBy=<userNo or stub>;其他字段保持 null(NOT_NULL 跳过,避免覆盖原值)。
  5. 事务边界:复用类级 @Transactional(rollbackFor = Exception.class),包裹"目标查询 + 子模块查询 + UPDATE"。

边界与约束

  • id 不存在 / 已软删40400
  • 存在未删子模块40901
  • JWT 伪造20001(filter 短路)
  • JWT 缺失 → permitAll stub,正常 200 + sDeletedBy=STUB_ADMIN(USR-004 闭环后改 401)
  • 40902 外部引用拦截 → 本 REQ 不实现(docs/03 当前 schema 无引用方),spec 记录后续补点

实现范围与边界抉择

  1. 复用 MOD-001/002 工程:无新增依赖;仅在 ModuleService / ModuleServiceImpl / ModuleController / ModuleMapper 上做增量。
  2. 删除接口非幂等:连续 DELETE 同一 id,第二次返回 40400,不允许覆盖已删除记录的 tDeletedDate / sDeletedBy。这与"删除"语义对齐——目标不存在就是错。
  3. mapper 仅查 1 行不查全表hasActiveChildrenSELECT 1 ... LIMIT 1 避免全表扫描;与 MOD-001 findActiveFlagById 风格一致。
  4. 不做 40902:待引用方落地。

依赖的 schema 表 / 字段

写入表:tModule

字段 用途 来源
iIncrement path id,定位行 @PathVariable
bDeleted 软删除标记 service 设 true
tDeletedDate 软删除时间 LocalDateTime.now()
sDeletedBy 软删除操作人 JWT principal 或 stubProps.stubUserNo
其他字段 不动

读取表:tModule(含子模块查询)。

依赖的接口

无(本 REQ 内部使用 MOD-001/002 已有 mapper 工具方法 + 新增 hasActiveChildren)。

验收标准

单元测试(追加到 ModuleServiceImplTest

  • deleteWithValidId_softDeletes_andSetsAuditFields — Mock selectById(10) 返回 alive Module,hasActiveChildren(10)=false;ArgumentCaptor 抓 updateById 入参,断言 iIncrement=10 / bDeleted=true / tDeletedDate != null / sDeletedBy="STUB_ADMIN"(无认证上下文);其他字段 null。
  • deleteWithTargetNotFound_throws40400selectById(99)=null;不调 updateById
  • deleteWithTargetAlreadyDeleted_throws40400selectById(10) 返回 bDeleted=true 的 Module;不调 updateById
  • deleteWithActiveChildren_throws40901selectById(10) alive;hasActiveChildren(10)=true;不调 updateById
  • deleteSetsDeletedByFromAuthenticatedUser — SecurityContextHolder 注入 principal "BOB";ArgumentCaptor sDeletedBy="BOB"

Mapper IT(追加到 ModuleMapperIT

  • hasActiveChildren_trueIfChildAliveExists_falseOtherwise — 准备 root + alive child + deleted child;断言 hasActiveChildren(root)=true;删除 alive child 后再断言 hasActiveChildren(root)=false(用 JdbcTemplate UPDATE bDeleted=1)。

集成测试(追加到 ModuleControllerIT

  • deleteValidId_with_jwt_returns200_andSoftDeletes — 直插一行 alive;DELETE 带 JWT="ADMIN001";期望 code=0 / data=null;DB 查 bDeleted=1 / sDeletedBy="ADMIN001" / tDeletedDate IS NOT NULL;其他列保持原值(断 sProcedureName / sCreatedBy / sBrandsId)。
  • deleteNonExistentId_returns40400 — DELETE /api/mod/modules/99999997code=40400
  • deleteAlreadyDeletedId_returns40400 — 直插 bDeleted=1 行;DELETE → code=40400
  • deleteWithActiveChildren_returns40901 — 直插 root + alive child;DELETE root → code=40901;DB 查 root 仍 bDeleted=0
  • deleteWithoutJwt_permitAllStub_returns200_andDeletedByIsSTUB — 直插 alive;无 token DELETE;code=0;DB 查 sDeletedBy="STUB_ADMIN"
  • deleteTamperedJwt_returns20001 — Authorization 伪造 → code=20001,DB 行未被改动。

工程验收

  • cd backend && mvn -B test 全绿(含 MOD-001 26 + MOD-002 15 + MOD-003 新增 5 service + 1 mapperIT + 6 controllerIT = 53 用例)
  • DELETE 接口经路径白名单 /api/mod/** permitAll 通过
  • // REQ-MOD-001 stub: see USR-004 follow-up 锚点保持不动