2026-05-06-REQ-MOD-002.md 9.36 KB

req_id: REQ-MOD-002 date: 2026-05-06

module: module_mod

Spec: REQ-MOD-002 — 模块修改

目标

实现后端 PUT /api/modules/{id} 接口:在不破坏唯一性 / 树结构完整性的前提下,更新已有模块的可编辑字段,返回最新模块 VO。

输入 / 触发

接口PUT /api/modules/{id},Content-Type application/json{id} = tModule.iIncrement

Request bodyModuleUpdateDTO)字段——与 REQ-MOD-001 输入相比剥除 sProcedureName(不可改,contract 约束);其余 7 个业务字段含义和校验规则保持一致:

字段 类型 必填 校验 / 取值 落库列
sDisplayType String 枚举:手机端 / 前端业务 / 系统配置 / 接口 tModule.sDisplayType
sModuleType String 长度 1-50 tModule.sModuleType
sManageDeptEn String 长度 1-50 tModule.sManageDeptEn
bShowPermission Boolean 默认保持原值;显式传 null 视为不变 tModule.bShowPermission
sModuleNameZh String 长度 1-100 tModule.sModuleNameZh
iParentId Integer 可空(设为根模块);非空必须存在且未软删除;不能等于 {id} 自身或其后代 tModule.iParentId
iSortOrder Integer 默认保持原值;非负整数 tModule.iSortOrder

sProcedureName 不在 DTO 中:Jackson 反序列化时若客户端误传将被忽略(@JsonIgnoreProperties(ignoreUnknown = true) 由 Jackson 默认行为兜底;不抛错)。前端 UI 应把该字段渲染为只读。

PUT 语义:本接口采用全量替换语义。请求体中显式存在的字段均落库;若未提供(JSON 中 key 缺失或值为 null),按字段下方"必填"列:必填字段缺失 → 40010;可选字段缺失 → 保持数据库原值。

鉴权:契约要求 Authorization: Bearer <accessToken> + 权限码 MOD:UPDATE。本 REQ 沿用 REQ-MOD-001 的 SecurityConfig permitAll 占位(REQ-USR-004 后回头收紧);Controller 写注释 // REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:UPDATE')")

输出 / 结果

HTTP 200,响应体(统一响应格式):

{
  "code": 200,
  "message": "操作成功",
  "data": {
    "iIncrement": 12,
    "sDisplayType": "前端业务",
    "sProcedureName": "sp_audit_user_module",
    "sModuleType": "USR",
    "sManageDeptEn": "IT",
    "bShowPermission": true,
    "sModuleNameZh": "用户管理(修订)",
    "iParentId": 3,
    "iSortOrder": 5,
    "tCreateDate": "2026-05-06T10:30:00",
    "bDeleted": false
  },
  "timestamp": 1746528600000
}

VO 复用 REQ-MOD-001 的 ModuleVO(11 个字段)。

业务规则

  1. 目标模块必须存在且未软删除SELECT ... WHERE iIncrement = {id} AND bDeleted = 0。不存在或已删 → 40421
  2. sProcedureName 不可改:DTO 不接受该字段;后端读取目标记录后保留原 sProcedureName 不变。
  3. iParentId 自引用校验
    • iParentId 等于路径参数 {id}(自引用)→ 40921
    • iParentIdtModule 中不存在或已软删除 → 40411
    • iParentId{id} 的后代(沿 iParentId 链向下走,深度上限 5 层与 docs/03 § tModule 业务注记一致)→ 40921
  4. 保留字段iIncrement / sId / sBrandsId / sSubsidiaryId / tCreateDate / sCreatedBy / bDeleted / tDeletedDate / sDeletedBy 在本接口不被修改
  5. bShowPermission / iSortOrder 部分更新:DTO 中为 null → 保持原值;显式传值 → 覆盖。
  6. 审计:本 REQ 暂不维护"最近修改时间"和"修改人"列(schema 未规划相关字段,docs/03 也未要求)。后续若需,按 V_n migration 加列同步更新 docs/03。
  7. 多租户字段不写入:与 REQ-MOD-001 一致,本接口不动 sBrandsId / sSubsidiaryId

边界与约束

鉴权策略(本 REQ 限定)

沿用 REQ-MOD-001:SecurityConfig permitAll;Controller 上写说明性注释 // REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:UPDATE')")

事务

  • Service 方法标 @Transactional(rollbackFor = Exception.class)。读取目标模块 → 校验 → 更新 全在同一事务。
  • 父模块校验 + 后代环路检查需多次 selectById,事务内可能产生几次小查询;本期数据量低,不做缓存优化。

并发

  • moduleMapper.updateById(entity) 走 PK 更新;不引入乐观锁版本号(schema 没规划 version 列)。
  • 并发同时更新同一模块时遵循"后写覆盖"语义,可接受。需要更强一致性时另开 REQ。

性能

  • 后代环路检查用迭代 BFS(队列),每次查 selectList(eq("iParentId", ...)) 拿子节点;深度上限 5 层 + 单层节点数受限于业务,不做递归 SQL。

错误码映射(与 docs/05 对齐)

场景 错误码
必填字段缺失 / 类型错误 / 长度超限 / 枚举非法 40010
{id} 模块不存在或已软删除 40421
iParentId 指向不存在 / 已删模块 40411
iParentId == {id} 或为 {id} 的后代 40921
服务端兜底 50000

docs/05 列出的 40911(sProcedureName 冲突)在本实现里不会触发(DTO 不接受 sProcedureName);保留契约文档不变即可。 新增错误码 40921 需补到 ErrorCode 枚举(命名 MOD_PARENT_LOOP);40421 命名 MOD_NOT_FOUND

依赖的 schema 表 / 字段

写表tModule(详见 docs/03 § tModule)

字段 行为
iIncrement 路径参数 {id} 定位行,不修改
sId / sBrandsId / sSubsidiaryId / tCreateDate / sCreatedBy 不修改
sDisplayType 入参覆盖
sProcedureName 不修改(保留原值)
sModuleType 入参覆盖
sManageDeptEn 入参覆盖
bShowPermission 入参非 null 覆盖;null 保留
sModuleNameZh 入参覆盖
iParentId 入参覆盖(含 null 设根)
iSortOrder 入参非 null 覆盖;null 保留
bDeleted / tDeletedDate / sDeletedBy 不修改

索引利用

  • 主键定位 {id}
  • idx_parent / fk_module_parent:iParentId 校验时按父链 / 子链查询

外键fk_module_parent 仍兜底;应用层环路检查在写入前显式拦截。

依赖的接口

无(本接口独立工作;与 REQ-MOD-001 并列同模块同 schema)。

验收标准

功能正确性

  1. 正向 — 全量更新非父字段:传入合法的 7 个字段(不含 iParentId 自引用),返回 200 + 最新 VO;DB 中查询新值与入参一致;sProcedureName / tCreateDate 与原值相同。
  2. 正向 — 设置父模块:先建 root + child,再 PUT /api/modules/{child_id}iParentId 改到另一个 sibling;返回 200,DB 中 iParentId 更新成功。
  3. 正向 — 清空父模块(设为根)PUT 时显式传 "iParentId": null,DB 中 iParentId 变 NULL。
  4. 正向 — 部分字段保留原值:DTO 中 bShowPermission / iSortOrder 传 null,DB 中保留原值。
  5. 目标不存在PUT /api/modules/999999,返回 200 + code=40421
  6. 目标已软删除:先把模块 bDeleted 置 1(直接 DB UPDATE 模拟),再 PUT,返回 40421
  7. 必填缺失:DTO 缺 sModuleNameZh,返回 40010
  8. 枚举非法sDisplayType="X",返回 40010
  9. 长度超限sModuleType = 51 字符,返回 40010
  10. iParentId 自引用PUT /api/modules/{id}iParentId 设为 {id} 本身,返回 40921
  11. iParentId 不存在PUTiParentId=999999,返回 40411
  12. iParentId 是后代:祖父→父→子三层结构,PUT 祖父把 iParentId 设为子的 id,返回 40921
  13. sProcedureName 字段被忽略:客户端误传 sProcedureName="other",DB 中该字段保持原值。

接口契约一致性

  • 响应格式严格符合 {code, message, data, timestamp}(docs/05 § 全局约定)。
  • 错误码段位与 docs/05 一致:40010 / 40411 / 40421 / 40921 / 50000
  • 异常堆栈不出现在响应里。

测试覆盖

  • 单元测试 ModuleServiceImplTest(继续 mock ModuleMapper):

    • update_targetNotFound_throws40421
    • update_targetSoftDeleted_throws40421
    • update_parentSelfReference_throws40921
    • update_parentNotFound_throws40411
    • update_parentIsDescendant_throws40921
    • update_full_returnsVOWithUpdatedFields(断言传给 mapper.updateById 的 entity 字段值,包括 sProcedureName 保留)
    • update_partialNullFields_keepsOriginalValues
    • update_clearParent_setsParentToNull
  • 集成测试 ModuleControllerIT 追加(@Transactional 自动回滚;用 ModuleMapper 直接预置数据):

    • put_validUpdate_returns200
    • put_setParentToNull_clearsParent
    • put_targetNotFound_returns40421
    • put_parentNotFound_returns40411
    • put_parentSelfRef_returns40921
    • put_parentIsDescendant_returns40921
    • put_missingRequired_returns40010
    • put_ignoresProcedureNameField_doesNotChange

代码与文档

  • // REQ-MOD-002 注释贴在 Controller 方法、Service 方法、新增 ErrorCode 枚举常量上。
  • 提交按 feat(mod): <subject> REQ-MOD-002 规范,每 Task 一个 commit。
  • 不引入 docs/04 § 零 技术栈外的依赖。