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 / Path:
PUT /api/usr/users/{id} -
路径参数:
id(Integer,目标用户主键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=0,data.id= 被修改用户主键usr_user.iIncrement(= 路径id)。 - 失败:统一
Result(见 § 6 错误码),message给可读中文提示,不抛栈。 - 响应不返回密码 / 任何敏感字段(仅含
data.id)。
3. 业务规则
-
目标用户必须存在:按路径
id查usr_user;查不到(或已物理不存在)抛BusinessException(40401),不做任何写入。 -
sUserName不可修改:本接口不接收 / 不更新sUserName;即便请求体携带也忽略。登录账号唯一性不在本接口被破坏。 -
密码不在本接口修改:
sPassword列保持原值不动;本接口不读取、不重置、不返回密码(密码重置/初始化由其他流程负责,REQ 卡片表内"密码自动初始化"属前端/独立流程描述,见 § 8 D1)。 -
用户类型约束:
sUserType必填,取值仅限 {普通用户,超级管理员},越界40001。角色变更需调用方具备管理员权限(已由 § 3 规则 9 的前置权限校验保证)。 -
语言取值约束:
sLanguage必填,取值仅限 {中文,英文,繁体},越界40001。 -
关联职员可选且需存在:传非 null
iEmployeeId时校验usr_employee存在;不存在40001。显式传 null 表示解除关联(置空iEmployeeId)的处理见 § 8 D3。 -
作废 / 禁用实时生效:
iIsVoid=1落库后该账号即不可登录(登录由 REQ-USR-004 校验iIsVoid,命中返回40302),无需额外流程;iIsVoid=0恢复正常。取值仅限 {0,1},越界40001。 -
权限组授权(多对多,全量覆盖):
permissionIds非 null 时,先校验每个 id 在usr_permission存在(不存在40001),去重后以该集合全量覆盖该用户在usr_user_permission的授权——删除不在集合内的旧授权、插入新增授权(实现可"先删后插"或差量更新,结果须等于目标集合);空数组[]表示清空全部授权;permissionIds为 null 表示本次不改动授权。唯一索引uk_usr_user_permission防重复。 -
权限校验前置:仅
超级管理员/ 管理员可调用;非管理员40301(在 Spring Security 或 Service 入口判定,先于业务校验)。 -
审计字段只读:
sCreator/tCreateDate保持原值,本接口不更新;不引入"最后修改人/时间"列(docs/03usr_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};新增 DTOUpdateUserDTO置于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),由GlobalExceptionHandler转Result;@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_permissionon(iUserId,iPermissionId);外键fk_usr_up_user/fk_usr_up_permission均 CASCADE)——按iUserId=id全量覆盖授权。 -
读:
usr_employee(校验iEmployeeId存在,外键fk_usr_user_employeeON DELETE SET NULL);usr_permission(校验permissionIds存在)。 - 实体:
UsrUser↔usr_user,UsrUserPermission↔usr_user_permission,UsrEmployee↔usr_employee,UsrPermission↔usr_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)
-
正常修改基本信息:管理员对已存在用户
id携带合法 body(sUserType/sLanguage合法)调用PUT /api/usr/users/{id}→code=0,data.id= 该id;usr_user对应行的sUserType/sLanguage/iCanModifyBill/sUserNo/iEmployeeId按传入值更新,sUserName/sPassword/sCreator/tCreateDate保持原值不变。 -
用户不存在:以不存在的
id调用 →code=40401,无任何写入。 -
参数非法:
sUserType或sLanguage越界 /iEmployeeId引用不存在职员 /permissionIds含不存在权限 id /iIsVoid或iCanModifyBill非 0/1 →code=40001,无副作用(事务回滚)。 -
禁用实时生效:传
iIsVoid=1修改成功后,该账号经 REQ-USR-004 登录返回40302(账号已禁用);改回iIsVoid=0后可正常登录。 -
角色变更实时生效:修改
sUserType成功后,REQ-USR-003 查询该用户返回的sUserType即为新值。 -
权限组全量覆盖:传
permissionIds=[a,b](均存在,原授权为[a,c])→ 覆盖后usr_user_permission中该用户授权恰为{a,b}(删除 c、保留/插入 a、新增 b);传permissionIds=[]→ 清空该用户全部授权;不传permissionIds(null)→ 现有授权保持不变;重复 id 去重后仅写一次。 -
越权访问:普通用户或无 / 失效 token 调用 →
code=40301(无权限)/ 401(未认证),不发生任何修改。 -
密码不变 & 响应不含密码:调用前后
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 的取值锁定(语言 ∈ {中文,英文,繁体})继续,不阻塞。