2026-06-01-REQ-USR-002.md 13.5 KB

REQ-USR-002 修改用户 — 实现规格(后端)

阶段:后端(backend)。作用域限定 controller / service / repository / DTO / 校验 / SQL migration / REST 契约。 SSoT 引用:需求卡片 docs/01-需求清单/USR-用户管理/REQ-USR-002.md;DB 设计 docs/03-数据库设计文档.md;API 契约 docs/05-API接口契约.md;技术规范 docs/04-技术规范.md。 本规格只消费已锁定事实,忽略 UI 描述(控件类型/按钮位置/布局/预加载/下拉数据源),但校验规则与业务规则全部下沉到后端 DTO + Service。


1. Goal(目标)

后台管理员修改已有用户的基本信息:用户号、关联职员、用户类型、语言、单据修改权限、作废(禁用)标志、权限组授权;保存后变更立即生效(角色/状态实时反映在用户列表,禁用账号无法登录)。对外仅提供一个端点:PUT /api/usr/users/{id}

sUserName(登录账号,全局唯一标识)不可修改;密码不在本接口修改(见 § 8 D1)。


2. 输入 / 输出

2.1 输入(请求)

  • Method / PathPUT /api/usr/users/{id}
  • 路径参数idInteger,目标用户主键 usr_user.iIncrement);必须为正整数,对应记录须存在,否则 40401
  • Auth:需要 Bearer JWT,且调用方必须为管理员 / 超级管理员(sUserType = 超级管理员)。普通用户调用返回 40301(先于业务校验判定)。
  • 请求体(JSON)→ UpdateUserDTO
DTO 字段 类型 必填 校验 落库列(usr_user 说明
sUserNo String @Size(max=50) sUserNo 用户号;传入即覆盖,为 null 不更新该列(见 § 8 D3 部分更新语义)
iEmployeeId Integer iEmployeeId 关联职员 ID;非 null 时须为 usr_employee 中存在的记录,否则 40001
sUserType String @NotBlank + 取值 ∈ {普通用户,超级管理员} sUserType 用户类型;越界 40001
sLanguage String @NotBlank + 取值 ∈ {中文,英文,繁体} sLanguage 界面语言;越界 40001
iCanModifyBill Integer 取值 ∈ {0,1} iCanModifyBill 单据修改权限;为 null 不更新该列
iIsVoid Integer 取值 ∈ {0,1} iIsVoid 作废 / 禁用标志:0 正常 / 1 禁用;为 null 不更新该列(见 § 8 D2)
permissionIds List<Integer> 元素须为 usr_permission 中存在的 id(去重) 重写 usr_user_permission 权限组勾选的全量集合(见 § 8 D4 全量覆盖语义);为 null 表示不改动现有授权

不接受前端传入 / 后端忽略的字段:sUserName(唯一标识不可改)、sPassword(密码不在本接口修改)、tCreateDate / sCreator(创建审计只读,保持原值)、tLastLoginDate(由登录流程维护)、iIncrement / sId / sBrandsId / sSubsidiaryId(主键与租户列不在本接口变更)。若请求体携带这些字段,一律忽略不落库。

2.2 输出(响应)

  • 成功:Result<{ id: number }>code=0data.id = 被修改用户主键 usr_user.iIncrement(= 路径 id)。
  • 失败:统一 Result(见 § 6 错误码),message 给可读中文提示,不抛栈。
  • 响应不返回密码 / 任何敏感字段(仅含 data.id)。

3. 业务规则

  1. 目标用户必须存在:按路径 idusr_user;查不到(或已物理不存在)抛 BusinessException(40401),不做任何写入。
  2. sUserName 不可修改:本接口不接收 / 不更新 sUserName;即便请求体携带也忽略。登录账号唯一性不在本接口被破坏。
  3. 密码不在本接口修改sPassword 列保持原值不动;本接口不读取、不重置、不返回密码(密码重置/初始化由其他流程负责,REQ 卡片表内"密码自动初始化"属前端/独立流程描述,见 § 8 D1)。
  4. 用户类型约束sUserType 必填,取值仅限 {普通用户,超级管理员},越界 40001。角色变更需调用方具备管理员权限(已由 § 3 规则 9 的前置权限校验保证)。
  5. 语言取值约束sLanguage 必填,取值仅限 {中文,英文,繁体},越界 40001
  6. 关联职员可选且需存在:传非 null iEmployeeId 时校验 usr_employee 存在;不存在 40001。显式传 null 表示解除关联(置空 iEmployeeId)的处理见 § 8 D3。
  7. 作废 / 禁用实时生效iIsVoid=1 落库后该账号即不可登录(登录由 REQ-USR-004 校验 iIsVoid,命中返回 40302),无需额外流程;iIsVoid=0 恢复正常。取值仅限 {0,1},越界 40001
  8. 权限组授权(多对多,全量覆盖)permissionIds 非 null 时,先校验每个 id 在 usr_permission 存在(不存在 40001),去重后以该集合全量覆盖该用户在 usr_user_permission 的授权——删除不在集合内的旧授权、插入新增授权(实现可"先删后插"或差量更新,结果须等于目标集合);空数组 [] 表示清空全部授权;permissionIds 为 null 表示本次不改动授权。唯一索引 uk_usr_user_permission 防重复。
  9. 权限校验前置:仅 超级管理员 / 管理员可调用;非管理员 40301(在 Spring Security 或 Service 入口判定,先于业务校验)。
  10. 审计字段只读sCreator / tCreateDate 保持原值,本接口不更新;不引入"最后修改人/时间"列(docs/03 usr_user 无此列,不新增 migration)。

4. 约束(技术 / 安全)

  • 分层(docs/04 § 1.2):UsrUserController(仅 @Valid 校验 + 委派)→ UsrUserService / UsrUserServiceImpl(业务)→ UsrUserMapper / UsrUserPermissionMapper(MyBatis-Plus)。Controller 禁止直接操作 Mapper。本 REQ 在已存在的 UsrUserService 上新增 updateUser(Integer id, UpdateUserDTO dto) 方法,复用 REQ-USR-001 已建的 controller / service / mapper / entity,不另起类。
  • 包路径com.xly.erp.modules.usr.{controller,service,service.impl,mapper,entity,dto,vo};新增 DTO UpdateUserDTO 置于 modules/usr/dto。本 REQ 仅触及 modules/usr/**,不跨模块。
  • 命名(docs/04 § 1.3):方法 updateUser;REST 路径 PUT /api/usr/users/{id}
  • 统一响应(docs/04 § 1.4):返回 Result<T>code=0 成功;错误码集中在 ResultCode 枚举。
  • 异常处理(docs/04 § 1.5):业务错误抛 BusinessException(ResultCode,msg),由 GlobalExceptionHandlerResult@Valid 校验失败由全局处理器转 40001
  • 事务(docs/04 § 1.6):updateUser 涉及 usr_user + usr_user_permission 多表写,方法上加 @Transactional(rollbackFor = Exception.class),任一步失败整体回滚(权限覆盖与主记录更新同事务)。
  • 认证 / 密码(docs/04 § 1.7):受保护接口经 JwtAuthenticationFilter;本接口不触碰密码,更不得将密码写入日志 / 响应。
  • 数据访问(docs/04 § 3.4):只走 Mapper;查询 / 校验 / 删授权用 LambdaQueryWrapper 或 MP 内置;禁止 Controller 直接操作 Mapper。
  • 配置:JWT 密钥、DB 凭据只从 config-vars.yaml / application.yml 读取,不硬编码。
  • schema:本 REQ 所需表(usr_user 写 / usr_employee 读 / usr_permission 读 / usr_user_permission 写)已由 sql/migrations/V1__initial_schema.sql 建好,结构与 docs/03 一致,无需新增 migration(见 § 8 D5)。

5. Schema 引用(docs/03 SSoT)

  • usr_user(主键 iIncrement;更新 sUserNo / iEmployeeId / sUserType / sLanguage / iCanModifyBill / iIsVoid;不更新 sUserName / sPassword / 审计列 / 租户列)。
  • usr_user_permission(关联表;唯一索引 uk_usr_user_permission on (iUserId,iPermissionId);外键 fk_usr_up_user / fk_usr_up_permission 均 CASCADE)——按 iUserId=id 全量覆盖授权。
  • usr_employee(校验 iEmployeeId 存在,外键 fk_usr_user_employee ON DELETE SET NULL);usr_permission(校验 permissionIds 存在)。
  • 实体:UsrUserusr_userUsrUserPermissionusr_user_permissionUsrEmployeeusr_employeeUsrPermissionusr_permission(匈牙利前缀列名,实体字段与列名映射保持一致)。

6. API 引用 / 错误码(docs/05 SSoT)

  • 端点契约见 docs/05-API接口契约.md § REQ-USR-002(PUT /api/usr/users/{id})。
  • 错误码:
    • 40001 — 参数校验失败(字段格式 / 必填 / 枚举越界 / 关联 id(iEmployeeId / permissionIds 元素)不存在 / id 非正整数)。
    • 40401 — 用户不存在(路径 id 无对应 usr_user 记录)。
    • 40301 — 无权限(非管理员调用)。
    • 0 — 成功,返回 data.id

7. 验收标准(Acceptance Criteria)

  1. 正常修改基本信息:管理员对已存在用户 id 携带合法 body(sUserType / sLanguage 合法)调用 PUT /api/usr/users/{id}code=0data.id = 该 idusr_user 对应行的 sUserType / sLanguage / iCanModifyBill / sUserNo / iEmployeeId 按传入值更新,sUserName / sPassword / sCreator / tCreateDate 保持原值不变。
  2. 用户不存在:以不存在的 id 调用 → code=40401,无任何写入。
  3. 参数非法sUserTypesLanguage 越界 / iEmployeeId 引用不存在职员 / permissionIds 含不存在权限 id / iIsVoidiCanModifyBill 非 0/1 → code=40001,无副作用(事务回滚)。
  4. 禁用实时生效:传 iIsVoid=1 修改成功后,该账号经 REQ-USR-004 登录返回 40302(账号已禁用);改回 iIsVoid=0 后可正常登录。
  5. 角色变更实时生效:修改 sUserType 成功后,REQ-USR-003 查询该用户返回的 sUserType 即为新值。
  6. 权限组全量覆盖:传 permissionIds=[a,b](均存在,原授权为 [a,c])→ 覆盖后 usr_user_permission 中该用户授权恰为 {a,b}(删除 c、保留/插入 a、新增 b);传 permissionIds=[] → 清空该用户全部授权;不传 permissionIds(null)→ 现有授权保持不变;重复 id 去重后仅写一次。
  7. 越权访问:普通用户或无 / 失效 token 调用 → code=40301(无权限)/ 401(未认证),不发生任何修改。
  8. 密码不变 & 响应不含密码:调用前后 sPassword 列字节级不变(用原密码仍可登录);成功响应体仅含 data.id,绝不含 sPassword 或明文密码。

8. 自主决策记录(decisions)

# 问题 选择 依据 置信度
D1 REQ 卡片输入表把「密码」标为"保存后自动设为初始化",与 docs/05 契约"密码不在本接口修改"冲突 后端不在本接口修改/重置密码sPassword 保持原值;body 不含密码字段 docs/05 契约(SSoT,明确"密码不在本接口修改")+ REQ 卡片「跨字段规则: 密码不在该接口修改」明文一致,权重高于输入表内"系统生成"备注(该备注属新增场景遗留/前端描述) high
D2 「状态/禁用」字段:REQ 目标与验收提到"状态""被禁用账号无法登录",但 REQ 输入表 1 未显式列出状态字段 usr_user.iIsVoid(0 正常/1 禁用)承载"状态",作为可选入参 iIsVoid docs/03 usr_user.iIsVoid 即"作废/禁用标志,禁用后不可登录";docs/05 REQ-USR-002 请求体已显式包含 iIsVoid;与验收"禁用账号无法登录"语义吻合 high
D3 部分更新语义:可选字段(sUserNo/iEmployeeId/iCanModifyBill/iIsVoid)传 null 时是"置空列"还是"不更新" 采用不更新(保持原值)语义:null 表示本次不改动该列;不提供显式"清空"区分(JSON null 与缺省不区分),与权限 permissionIds=null 不改动一致 REST PUT 在本契约场景按"提交需修改的字段"实践更安全,避免误把未填字段清空原有数据;契约未要求区分"置空 vs 不传",取保守不更新可避免数据丢失。前端编辑表单预加载原值后整体回填,行为等价 medium
D4 permissionIds 更新语义(增量追加 vs 全量覆盖) 全量覆盖:以传入集合替换该用户全部授权;[] 清空;null 不改动 REQ 卡片表 2「权限组」复选框预加载原值、提交即当前勾选全集,语义为"这一组就是最终授权";全量覆盖与编辑界面"勾选即最终态"一致,避免无法取消授权 high
D5 是否新增 migration 复用 V1__initial_schema.sql 已建表,不新增 usr_user / usr_user_permission、读 usr_employee / usr_permission 均已在 V1 建好,结构与 docs/03 一致,本 REQ 无 schema 变更(也不新增"最后修改人/时间"列,docs/03 未定义) high
D6 管理员判定口径 与 REQ-USR-001 一致:以 sUserType=超级管理员 视为有调用权限;普通用户禁止 docs/05 标注"仅管理员/超级管理员可调用";本库用户类型枚举仅 {普通用户,超级管理员},无独立角色表;保持模块内一致 medium

注:docs/03 中 sLanguage / usr_company.sVersion / usr_permission 粒度的「需用户审阅」占位为 DB 文档遗留标记,不在本后端 REQ 作用域内消解;本规格沿用 REQ-USR-001 的取值锁定(语言 ∈ {中文,英文,繁体})继续,不阻塞。