2026-04-30-REQ-USR-002.md 8.86 KB

req_id: REQ-USR-002 date: 2026-04-30

module: module_usr

Spec: REQ-USR-002 — 用户修改

目标

在不破坏唯一性的前提下,更新已有用户的可编辑字段(sUserNo / sUserName / iStaffId / sUserType / sLanguage / bCanModifyDocs)+ 全量重建用户的权限组关联(tUserPermission)。sPasswordHashtCreateDatesCreatedBysBrandsId / sSubsidiaryId、软删除字段一律保留原值。

输入 / 触发

HTTP 接口(docs/05 § REQ-USR-002)

  • Method / Path: PUT /api/usr/users/{id}(path {id} = tUser.iIncrement
  • Auth: 必需(沿用 USR-001 stub:路径已在 SecurityConfig /api/usr/** permitAll,USR-004 闭环时统一改为 hasAuthority('SUPER_ADMIN')

请求 DTO UpdateUserDTO

JSON 字段 Java 类型 必填 校验 业务校验
sUserNo String @NotBlank @Size(max=50) 系统内唯一(依赖 tUser.uk_user_no);冲突 → 40020
sUserName String @NotBlank @Size(max=50) 系统内唯一(依赖 tUser.uk_user_name);冲突 → 40020
iStaffId Integer 非 null 时必须命中存在且 bDeleted=0 的记录;不存在 / 已软删 → 40022(沿用 USR-001 错误码语义;docs/05 § USR-002 未单列 40022,本实现复用 USR-001 已建立的语义)
sUserType String @NotBlank 必须在枚举 [普通用户, 超级管理员] 内;非法 → 40001
sLanguage String @NotBlank 必须在枚举 [zh, en, zh-TW] 内;非法 → 40001
bCanModifyDocs Boolean 缺省 false
permissionCategoryIds List<Integer> 非空时所有 id 必须在 tPermissionCategory 中存在 + bDeleted=0;任一不合法 → 40023(同 USR-001)。null / 空 list → 清空该用户权限组(删全部 tUserPermission)

sPasswordHash 显式从 DTO 中剔除——API 契约声明该字段不可改;密码修改走独立接口(未来 REQ)。

鉴权与上下文

JWT Filter 解析 token 写 principal=sUserNo;伪造 → code=20001;缺失 → permitAll 透传。sCreatedBy 在更新时不修改,无论是否携带 token。

输出 / 结果

成功响应

{ "code": 0, "msg": "ok", "data": { "iIncrement": 456 } }

持久化效果

事务内三步:

  1. UPDATE tUser SET <可编辑列> WHERE iIncrement = {id}sUserNo / sUserName / iStaffId / sUserType / sLanguage / bCanModifyDocs
  2. DELETE FROM tUserPermission WHERE iUserId = {id}(清空旧关联)
  3. permissionCategoryIds 中的每个 id:INSERT tUserPermission(iUserId={id}, iCategoryId=cid, sCreatedBy=同上, tCreateDate=NOW(), sBrandsId/sSubsidiaryId=配置)
tUser 字段 更新策略
sUserNo / sUserName / iStaffId / sUserType / sLanguage / bCanModifyDocs DTO 透传
其他字段(sPasswordHash / sCreatedBy / tCreateDate / sBrandsId / sSubsidiaryId / tLastLoginDate / bDeleted / tDeletedDate / sDeletedBy / sId 不更新(entity 上对应字段保持 null,依赖 MyBatis-Plus 默认 FieldStrategy.NOT_NULL 跳过 null 字段)

复用 MOD-002 / USR-001 已建立的"NOT_NULL 跳过 null 不动其他字段"策略;bCanModifyDocs DTO null 时 service 先展开为 false 再赋值。

业务规则

  1. 目标存在性SELECT * FROM tUser WHERE iIncrement = {id};行不存在 bDeleted=trueBizException(40400, "用户不存在或已删除")
  2. 枚举校验sUserType / sLanguage 复用 USR-001 的 Set.contains 校验;非法 → BizException(40001, "<字段>: 取值非法")
  3. iStaffId 校验(非 null 时):staffMapper.existsActiveById(iStaffId) == falseBizException(40022, "职员不存在或已删除")
  4. permissionCategoryIds 校验(非空时):permissionCategoryMapper.countActiveByIds(ids) != ids.size()BizException(40023, "权限分类含无效 id")
  5. 唯一冲突:依赖 DB 唯一索引兜底;userMapper.updateByIdDuplicateKeyExceptionBizException(40020, "用户号或用户名已存在")
  6. 重建权限组:先 userPermissionMapper.deleteByUserId(id) 全量删除,再 for-loop 插入新关联(即使 permissionCategoryIds 为空 / null 也执行删除,等价"清空")。
  7. 事务@Transactional(rollbackFor = Exception.class) 包"5 类校验 + UPDATE user + DELETE permission + INSERT permission × N",任一步失败回滚。

边界与约束

  • 必填项缺失40001
  • sUserType / sLanguage 非枚举40001
  • sUserNo / sUserName 唯一冲突40020
  • 目标 id 不存在 / 已软删40400
  • iStaffId 不存在 / 已软删40022
  • permissionCategoryIds 含无效 id40023
  • JWT 伪造20001
  • JWT 缺失 → permitAll stub
  • sPasswordHash 不被覆盖:DTO 不暴露字段;entity 上 sPasswordHash=null 由 NOT_NULL 跳过

实现范围与边界抉择

  1. 复用 USR-001 工程:所有 mapper / entity / dto 已就位;本 REQ 仅在 UserService / UserServiceImpl / UserController 上做增量 + UserPermissionMapper 追加 deleteByUserId
  2. 错误码 40022 语义复用:docs/05 § USR-002 错误码列表只列 40001/40020/40400,未单列 40022。本 spec 选择复用 USR-001 已建立的语义,与"同字段两个接口错误码一致"原则相符(同 MOD-002 复用 MOD-001 40010 的处理)。
  3. 重建权限组策略:选"先全删再插入"而非"diff 增量更新"——典型模块权限数 < 50,diff 实现复杂度收益不匹配。
  4. iStaffId 不强校验外键 SET NULL:DB 已 ON DELETE SET NULL,service 提前校验存在性给更友好错误码。

依赖的 schema 表 / 字段

写入:

  • tUser:6 个可编辑字段(其余依赖 NOT_NULL 跳过)
  • tUserPermissioniIncrement 自增 + 6 字段(先全删后批量插入)

读取(仅校验存在性):

  • tUser(selectById 校验目标)
  • tStaff / tPermissionCategory(同 USR-001)

依赖外键:fk_user_staff: iStaffId → tStaff.iIncrement (ON DELETE SET NULL) / tUserPermissiontUsertPermissionCategory 的外键。

依赖的接口

无(仅本 REQ 内部使用 USR-001 已实现的 mapper + 新增 userPermissionMapper.deleteByUserId)。

验收标准

单元测试(追加到 UserServiceImplTest

  • updateWithValidDto_invokesUpdateById_andRebuildsPermissions — Mock selectById(10)=aliveexistsActiveByIdcountActiveByIds;ArgumentCaptor 抓 userMapper.updateById + userPermissionMapper.deleteByUserId(10) 调用一次 + userPermissionMapper.insert × N;断言传入 entity 的 iIncrement=10 / 可编辑字段被透传 / sPasswordHash 等不可改字段为 null
  • updateWithTargetNotFound_throws40400
  • updateWithTargetAlreadyDeleted_throws40400
  • updateWithInvalidUserType_throws40001
  • updateWithInvalidLanguage_throws40001
  • updateWithStaffNotFound_throws40022
  • updateWithSomeInvalidPermissionIds_throws40023
  • updateWithDuplicateUserNo_throws40020 — Mock userMapper.updateByIdDuplicateKeyException
  • updateWithEmptyPermissionIds_clearsExistingpermissionCategoryIds=nulldeleteByUserId 调用一次,insert 永不调用
  • updateWithBCanModifyDocsNull_setsFalseInEntity

Mapper IT(追加到 UserMapperIT

  • userPermissionMapper#deleteByUserId_removesAllRowsForGivenUser — 直插 user + 2 行 permission;调 deleteByUserId → tUserPermission 中该 user 的行 == 0;其他 user 不受影响

集成测试(UserControllerIT,追加 8 用例)

  • putValidBody_with_jwt_returns200_andUpdates — 直插 user + 2 行 permission;PUT 改 sUserName 同时改 permissionCategoryIds;DB 验:可编辑字段更新;sPasswordHash / sCreatedBy 保留原值;tUserPermission 行数 == 新 ids.size()
  • putNonExistentId_returns40400
  • putAlreadyDeletedId_returns40400
  • putInvalidUserType_returns40001
  • putDuplicateUserNo_returns40020 — 先存在 user1(sUserNo=A) + user2(sUserNo=B);PUT user2 改 sUserNo=A → code=40020
  • putStaffNotFound_returns40022
  • putPermissionCategoryNotFound_returns40023
  • putWithEmptyPermissionIds_clearsAssociations — 直插 user + 2 行 permission;PUT 不带 permissionCategoryIds;DB 查该 user 的 tUserPermission == 0;sPasswordHash 保留原值
  • putWithoutJwt_permitAllStub_returns200_andDoesNotChangeCreatedBy
  • putTamperedJwt_returns20001

工程验收

  • cd backend && mvn -B test 全绿(89 + 新增 ≥ 19 = ≥ 108 用例)
  • DB 中 sPasswordHash / sCreatedBy / tCreateDate 在 PUT 前后字面相同
  • tUserPermission 行集与请求 ids 完全等价(删旧 + 插新)