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 透传。sDeletedBy 取 SecurityContextHelper.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 字段)。
业务规则
-
目标存在性:
SELECT * FROM tModule WHERE iIncrement={id};行不存在 或bDeleted=1→BizException(40400, "模块不存在或已删除")。已删模块再次 DELETE 也返回 40400(删除接口非幂等设计——业务上想表达"目标已不存在")。 -
子模块拦截:检查
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 包装。 -
外部引用拦截(40902):docs/05 列了该错误码,但 docs/03 § tModule 业务注记明确"与本期其他表无外键关系"——本期 schema 中不存在菜单/权限/角色表引用 tModule 的字段,本 REQ 不实现 40902 校验。spec 显式记录该决策:当 USR 或后续模块引入
tMenu/tRole等表并通过iModuleId等字段引用 tModule 时,再回头扩展 ModuleService#delete 加引用查询。当前实现保持纯 MOD 模块自包含。 -
软删除字段填充:构造
Module entity仅 setiIncrement/bDeleted=true/tDeletedDate=LocalDateTime.now()/sDeletedBy=<userNo or stub>;其他字段保持 null(NOT_NULL 跳过,避免覆盖原值)。 -
事务边界:复用类级
@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 记录后续补点
实现范围与边界抉择
-
复用 MOD-001/002 工程:无新增依赖;仅在
ModuleService/ModuleServiceImpl/ModuleController/ModuleMapper上做增量。 -
删除接口非幂等:连续 DELETE 同一 id,第二次返回 40400,不允许覆盖已删除记录的
tDeletedDate/sDeletedBy。这与"删除"语义对齐——目标不存在就是错。 -
mapper 仅查 1 行不查全表:
hasActiveChildren用SELECT 1 ... LIMIT 1避免全表扫描;与 MOD-001findActiveFlagById风格一致。 - 不做 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— MockselectById(10)返回 alive Module,hasActiveChildren(10)=false;ArgumentCaptor 抓updateById入参,断言iIncrement=10/bDeleted=true/tDeletedDate != null/sDeletedBy="STUB_ADMIN"(无认证上下文);其他字段 null。 -
deleteWithTargetNotFound_throws40400—selectById(99)=null;不调updateById。 -
deleteWithTargetAlreadyDeleted_throws40400—selectById(10)返回bDeleted=true的 Module;不调updateById。 -
deleteWithActiveChildren_throws40901—selectById(10)alive;hasActiveChildren(10)=true;不调updateById。 -
deleteSetsDeletedByFromAuthenticatedUser— SecurityContextHolder 注入 principal"BOB";ArgumentCaptorsDeletedBy="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/99999997;code=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锚点保持不动