2026-05-15-REQ-USR-002.md
8.62 KB
req_id: REQ-USR-002 date: 2026-05-15
module: module_usr
Spec: REQ-USR-002 — 新增用户
目标
超级管理员通过 POST /api/v1/users 新建用户账号。账号立即生效;初始密码由系统统一设为 666666(哈希后存入);可选关联职员;同时按勾选写入权限分类授权关系。
输入 / 触发
HTTP 入口 POST /api/v1/users。要求请求头 Authorization: Bearer <accessToken>。
请求体 CreateUserReq(JSON):
| 字段 | 类型 | 必填 | 校验规则 |
|---|---|---|---|
username |
string | 是 | 非空;3-20 位字母数字下划线(正则 ^[A-Za-z0-9_]{3,20}$);系统内唯一(命中 sys_user.sUsername 返 40901) |
userCode |
string | 是 | 非空;最大 50;系统内唯一(命中 sys_user.sUserCode 返 40902) |
userType |
string | 是 | 枚举 NORMAL / SUPER_ADMIN
|
language |
string | 是 | 枚举 zh-CN / en-US / zh-TW
|
canEditDocument |
boolean | 是 | true / false |
employeeId |
int | 否 | 若不为 null,必须命中 sys_employee.iIncrement AND iIsDeleted=0,否则返 40004 |
permissionCategoryIds |
int[] | 否 | 可为空数组或省略;每个元素必须命中 sys_permission_category.iIncrement AND iIsDeleted=0,否则返 40004 |
本 REQ 不接受
password字段:用户提交的任何 password 字段被忽略(或返 40001)。docs/05 应同步删除password与40002—— 本 REQ 落地时由实现方在 docs/05 一并修订(spec 自审范畴)。
输出 / 结果
成功 201 Created:Result<CreateUserVo>
{ "code": 200, "message": "操作成功", "data": { "userId": 42, "username": "alice", "userCode": "U001" }, "timestamp": ... }
HTTP 201 体现新资源创建;body 仍走统一
Result包装(docs/04 § 1.3)。
副作用(同一事务):
-
sys_user插入一条新记录:-
sPasswordHash=BCryptPasswordEncoder.encode("666666") -
iFailedLoginCount= 0、tLockUntil= NULL、tLastLoginDate= NULL -
iIsDeleted= 0 -
sCreatedBy= 当前登录用户 username(来自 JWT claim)
-
-
sys_user_permission_category按permissionCategoryIds批量插入授权记录,sGrantedBy= 当前登录用户 username
失败:
| HTTP | code | 含义 | 触发条件 |
|---|---|---|---|
| 400 | 40001 | 必填字段缺失或格式错误 | jakarta 校验失败(@NotBlank / @Pattern / 枚举不合法);请求包含 password 字段(本 REQ 不允许) |
| 400 | 40004 | 员工或权限分类不存在 |
employeeId 或 permissionCategoryIds 中任一不命中 |
| 401 | 40101 | 未携带或无效 Token | Authorization 缺失 / 解析失败 / 用户已作废 / 用户已锁定 |
| 403 | 40301 | 非超级管理员调用 | JWT claim userType != SUPER_ADMIN
|
| 409 | 40901 | 用户名已存在 | sUsername 唯一冲突(DB 唯一键 uk_sys_user_username 兜底;service 层 select 预检以返友好错误) |
| 409 | 40902 | 用户号已存在 | sUserCode 唯一冲突 |
业务规则
-
鉴权:手写
JwtHandlerInterceptor实现HandlerInterceptor,注册到WebMvcConfigurer,匹配/api/v1/**但排除/api/v1/auth/login:- 读
Authorization: Bearer <token>头;缺失 → 抛BizException(40101, "未携带 token") -
JwtUtil.parse(token)抛 BizException 时透传(JwtUtil 内部已返 40101) - 解析 claims → 查
sys_user.iIncrement = sub;若不存在 /iIsDeleted=1/tLockUntil > NOW()→ 抛BizException(40101, "token 关联用户不可用") - 把 username / userType / userId / companyCode 放入
LoginContext(ThreadLocal 工具)供后续使用 - 业务方法结束时(
afterCompletion)清理 ThreadLocal
- 读
-
角色守卫:用自定义注解
@RequireSuperAdmin(标注在 controller 方法上)+ 同一 interceptor 检查handler instanceof HandlerMethod && method.isAnnotationPresent(...)→ 校验LoginContext.userType == SUPER_ADMIN;不匹配 → 抛BizException(40301, "权限不足,仅超级管理员可调用") -
唯一性校验:在 service 写入前用
selectByUsername/selectByUserCode查重(友好返 40901 / 40902);DB 唯一索引兜底(捕获DataIntegrityViolationException转 40901 或 40902,避免堆栈泄漏) - 外键校验:employeeId / permissionCategoryIds 在写入前一次性 select 校验(SELECT iIncrement FROM ... WHERE iIncrement IN(...) AND iIsDeleted=0),数量不齐 → 40004
-
初始密码:固定字符串
"666666",通过BCryptPasswordEncoder.encode哈希;常量定义在LoginServiceImpl.INITIAL_PASSWORD(已存在)或新建UserCreateServiceImpl.INITIAL_PASSWORD -
事务:
@Transactional,sys_user 插入 + sys_user_permission_category 批量插入在同一事务;唯一冲突或 FK 不存在均回滚 -
本 REQ 不接受 password 字段:DTO 不定义 password 属性;若客户端误传,Jackson 默认配置(
FAIL_ON_UNKNOWN_PROPERTIES=false)会忽略——但为防御性,建议application.yml设spring.jackson.deserialization.fail-on-unknown-properties: true,确保多余字段返 40001
边界与约束
- HTTPS / 鉴权 / 统一响应 / 异常:复用 REQ-USR-001 已建的全部基础设施
-
HandlerInterceptor 位置:放在
backend/src/main/java/com/xly/erp/common/security/,与JwtUtil同包 -
ThreadLocal 工具:
LoginContext单例,提供current() / set() / clear();测试场景可手工 set 以模拟登录态 -
@RequireSuperAdmin 注解:放在
backend/src/main/java/com/xly/erp/common/security/ -
不实现:
- 修改密码 / 重置密码(推迟到后续 REQ)
- 用户软删除接口(推迟到 REQ-USR-003 的"作废"路径)
- 权限分类的新增 / 编辑(运营模块)
- 员工的 CRUD(HR 模块)
- JWT 黑名单 / refresh token / 多端互踢(推迟)
-
docs/05 同步修订:本 REQ 实现时需同步修订 docs/05 § REQ-USR-002,去掉
password字段与40002错误码(与 REQ 卡片对齐为"系统生成初始密码")
依赖的 schema 表 / 字段
写 sys_user(V1 已建):
- 写入:
sUsername,sUserCode,sPasswordHash,iEmployeeId,sUserType,sLanguage,iCanEditDocument,iIsDeleted=0,iFailedLoginCount=0,sCreatedBy
写 sys_user_permission_category(V1 已建):
- 写入:
iUserId,iPermissionCategoryId,sGrantedBy
只读 sys_employee(V1 已建):
- 读:
iIncrement,iIsDeleted(校验 employeeId 存在)
只读 sys_permission_category(V1 已建):
- 读:
iIncrement,iIsDeleted(校验 permissionCategoryIds 全部存在)
本 REQ 不需要新增 migration。
依赖的接口
- 本 REQ 提供:
POST /api/v1/users - 鉴权前置依赖:JWT 由 REQ-USR-001 签发
- 下游会消费但本 REQ 不实现的接口:
-
GET /api/v1/employees(员工下拉,后续 HR 模块) -
GET /api/v1/permission-categories(权限分类下拉,后续运营模块)
-
验收标准
后端集成测试:
-
正常路径(最小字段):管理员 token + 合法 username / userCode / userType / language / canEditDocument,不带 employeeId / permissionCategoryIds → 201;DB 出现新用户记录,
sPasswordHash非空且可被BCryptPasswordEncoder.matches("666666", ...)验证;iIsDeleted=0,iFailedLoginCount=0,sCreatedBy= 管理员 username -
正常路径(完整字段):带 employeeId + permissionCategoryIds(2 条)→ 201;DB sys_user 一行 + sys_user_permission_category 两行;
sGrantedBy正确 -
新用户立刻可登录:调用
POST /api/v1/auth/login用初始密码666666+ 任意有效公司 → 200 + token - 缺 Authorization 头 → 401 / 40101
- Token 无效(篡改) → 401 / 40101
- Token 关联用户已作废 → 401 / 40101
- NORMAL 用户 token 调用 → 403 / 40301
- username 重复 → 409 / 40901;DB 没有新用户被插入
- userCode 重复 → 409 / 40902;DB 没有新用户被插入
- employeeId 不存在 / 已软删 → 400 / 40004
- permissionCategoryIds 含不存在 ID → 400 / 40004;事务回滚(sys_user 也未插入)
- 请求体缺 username / username 含非法字符 → 400 / 40001
- 请求体携带 password 字段 → 400 / 40001(Jackson 严格反序列化)
- userType / language 非枚举值 → 400 / 40001
- 唯一索引兜底:模拟绕过 service 层预检的并发场景(DataIntegrityViolationException 路径),断言返 40901 而非 50000
测试基础:复用 LoginTestSeeder + 扩展 UserCreateTestSeeder(管理员 token 注入 + 权限分类 fixture)。