--- 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 body**(`ModuleUpdateDTO`)字段——与 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 ` + 权限码 `MOD:UPDATE`。本 REQ 沿用 REQ-MOD-001 的 SecurityConfig permitAll 占位(REQ-USR-004 后回头收紧);Controller 写注释 `// REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('MOD:UPDATE')")`。 ## 输出 / 结果 **HTTP 200,响应体**(统一响应格式): ```json { "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`。 - 若 `iParentId` 在 `tModule` 中不存在或已软删除 → `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 不存在**:`PUT` 时 `iParentId=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): REQ-MOD-002` 规范,每 Task 一个 commit。 - 不引入 docs/04 § 零 技术栈外的依赖。