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 <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 个字段)。
业务规则
-
目标模块必须存在且未软删除:
SELECT ... WHERE iIncrement = {id} AND bDeleted = 0。不存在或已删 →40421。 -
sProcedureName不可改:DTO 不接受该字段;后端读取目标记录后保留原sProcedureName不变。 -
iParentId自引用校验:- 若
iParentId等于路径参数{id}(自引用)→40921。 - 若
iParentId在tModule中不存在或已软删除 →40411。 - 若
iParentId是{id}的后代(沿iParentId链向下走,深度上限 5 层与 docs/03 § tModule 业务注记一致)→40921。
- 若
-
保留字段:
iIncrement/sId/sBrandsId/sSubsidiaryId/tCreateDate/sCreatedBy/bDeleted/tDeletedDate/sDeletedBy在本接口不被修改。 -
bShowPermission/iSortOrder部分更新:DTO 中为null→ 保持原值;显式传值 → 覆盖。 - 审计:本 REQ 暂不维护"最近修改时间"和"修改人"列(schema 未规划相关字段,docs/03 也未要求)。后续若需,按 V_n migration 加列同步更新 docs/03。
-
多租户字段不写入:与 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)。
验收标准
功能正确性
-
正向 — 全量更新非父字段:传入合法的 7 个字段(不含
iParentId自引用),返回 200 + 最新 VO;DB 中查询新值与入参一致;sProcedureName/tCreateDate与原值相同。 -
正向 — 设置父模块:先建 root + child,再
PUT /api/modules/{child_id}把iParentId改到另一个 sibling;返回 200,DB 中iParentId更新成功。 -
正向 — 清空父模块(设为根):
PUT时显式传"iParentId": null,DB 中iParentId变 NULL。 -
正向 — 部分字段保留原值:DTO 中
bShowPermission/iSortOrder传 null,DB 中保留原值。 -
目标不存在:
PUT /api/modules/999999,返回 200 +code=40421。 -
目标已软删除:先把模块
bDeleted置 1(直接 DB UPDATE 模拟),再PUT,返回40421。 -
必填缺失:DTO 缺
sModuleNameZh,返回40010。 -
枚举非法:
sDisplayType="X",返回40010。 -
长度超限:
sModuleType= 51 字符,返回40010。 -
iParentId 自引用:
PUT /api/modules/{id}把iParentId设为{id}本身,返回40921。 -
iParentId 不存在:
PUT时iParentId=999999,返回40411。 -
iParentId 是后代:祖父→父→子三层结构,
PUT祖父把iParentId设为子的 id,返回40921。 -
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 § 零 技术栈外的依赖。