2026-04-30-REQ-USR-001.md
9.71 KB
req_id: REQ-USR-001 date: 2026-04-30
module: module_usr
Spec: REQ-USR-001 — 用户新增
目标
录入新用户基本信息 + 初始化默认密码(666666 的 BCrypt 哈希)+ 同步建立用户与权限分类的多对多关联,返回新用户 iIncrement + sUserNo。
输入 / 触发
HTTP 接口(docs/05 § REQ-USR-001)
- Method / Path:
POST /api/usr/users - Auth: 必需(沿用 MOD 模块 stub:本 REQ 在 SecurityConfig 加
/api/usr/**permitAll,USR-004 闭环时统一改为hasAuthority('SUPER_ADMIN')) - Permission: 仅超级管理员(stub 期不强制)
请求 DTO CreateUserDTO
| 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 时必须命中 tStaff 中存在且 bDeleted=0 的记录;不存在 / 已软删 → 40022
|
sUserType |
String |
是 | @NotBlank |
必须在枚举 [普通用户, 超级管理员] 内;非法 → 40001(沿用 docs/05 错误码列表) |
sLanguage |
String |
是 | @NotBlank |
必须在枚举 [zh, en, zh-TW](代码值,前端做 i18n 标签映射)内;非法 → 40001
|
bCanModifyDocs |
Boolean |
否 | — | 缺省 false
|
permissionCategoryIds |
List<Integer> |
否 | — | 非空时所有 id 必须在 tPermissionCategory 中存在 + bDeleted=0;任一不合法 → 40023。空 list / null → 不建关联 |
鉴权与上下文
JWT Filter 解析 token 写 principal=sUserNo;伪造 token → code=20001;缺失 token → permitAll 透传。sCreatedBy 取 SecurityContextHelper.currentUserNo(),匿名状态回退 stubProps.stubUserNo(与 MOD 模块同策略)。
输出 / 结果
成功响应
{ "code": 0, "msg": "ok", "data": { "iIncrement": 456, "sUserNo": "u001" } }
持久化效果
事务内两步:
- INSERT
tUser:DTO 字段 + 标准列 +sPasswordHash = bcrypt("666666")+tLastLoginDate=NULL+bDeleted=0 - 对
permissionCategoryIds中的每个 id:INSERTtUserPermission(iUserId=新用户.iIncrement, iCategoryId=id, sCreatedBy=同上, tCreateDate=NOW(), sBrandsId/sSubsidiaryId=配置默认)
tUser 字段 |
来源 |
|---|---|
iIncrement |
DB 自增 |
sId |
NULL |
sBrandsId / sSubsidiaryId
|
TenantProperties(XLY/XLY) |
tCreateDate |
LocalDateTime.now() |
sUserNo / sUserName / iStaffId / sUserType / sLanguage / bCanModifyDocs
|
DTO 透传 |
sPasswordHash |
BCryptPasswordEncoder.encode("666666")(每次 hash salt 不同) |
tLastLoginDate |
NULL(USR-004 登录时更新) |
sCreatedBy |
JWT principal 或 stub |
bDeleted |
0 |
tDeletedDate / sDeletedBy
|
NULL |
业务规则
-
唯一性策略:DB
uk_user_no+uk_user_name包含已软删行;本期不支持"软删后用户号复用"——若 sUserNo 历史上有过删除记录,再次创建会被 DB 唯一索引拒绝。docs/03 § tUser 业务注记说"应用层保证(仅约束未删除部分)"是设计意向,本 REQ 实现保持与 V1 schema 一致:依赖 DB 唯一索引兜底,service 层捕获DuplicateKeyException→BizException(40020,"用户号或用户名已存在"),不再做"先查后插"二次校验(避免竞态)。 -
iStaffId 校验:非 null 时调
staffMapper.existsActiveById(iStaffId);false →BizException(40022,"职员不存在或已删除")。 -
permissionCategoryIds 校验:非空时一次性查
permissionCategoryMapper.countActiveByIds(ids),若返回数 != ids.size →BizException(40023,"权限分类含无效 id")。避免 N+1(一条 IN 查询)。 -
sUserType / sLanguage 枚举:service 入口处用
Set.contains校验;非法 →BizException(40001, "<字段名>: 取值非法")。 -
密码哈希:使用 Spring Security 的
BCryptPasswordEncoder(已通过 starter-security 引入),强度默认 10。@Bean BCryptPasswordEncoder在SecurityConfig注册(仅本 REQ 引入,避免循环依赖,参 § 实现范围抉择)。 -
事务:service 上
@Transactional(rollbackFor = Exception.class),包"校验 + INSERT user + INSERT user_permission * N"。任一步骤失败回滚,不留残行。 -
批量插入策略:本 REQ 用
for (Integer id : ids) userPermissionMapper.insert(rec)简单循环。permissionCategoryIds 典型 < 50,N+1 影响可接受。后续若性能瓶颈再改 batch INSERT。
边界与约束
-
必填项缺失 →
40001 -
sUserType/sLanguage非枚举 →40001 -
sUserNo/sUserName唯一冲突 →40020 -
iStaffId不存在 / 已软删 →40022 -
permissionCategoryIds含无效 id / 已软删 →40023 -
JWT 伪造 →
20001 - JWT 缺失 → permitAll stub(USR-004 后改 401)
-
不返回
sPasswordHash:响应 data 仅含iIncrement+sUserNo,避免哈希泄漏
实现范围与边界抉择
-
复用 MOD 模块工程:无新增 pom 依赖;
backend/src/main/java/com/xly/erp/module/usr/全新模块树,与module/mod/平行。 -
SecurityConfig 路径扩展:在现有
/api/mod/**permitAll 同位加/api/usr/**permitAll,stub 注释保持// REQ-MOD-001 stub: see USR-004 follow-up(USR-004 时整段一次性收紧)。 -
BCryptPasswordEncoder注册位置:本 REQ 在SecurityConfig加@Bean BCryptPasswordEncoder,service 通过构造器注入。USR-004 登录接口同样依赖此 bean,无重复定义。 -
Staff / PermissionCategory 仅做存在性校验:本 REQ 不建
Staff/PermissionCategory完整 entity,仅建StaffMapper/PermissionCategoryMapper两个最小化接口(注解 SELECT 1 / SELECT COUNT)。后续 USR-002/003 真正用到完整字段时再补 entity。 -
唯一冲突处理走 DB 索引兜底:与 MOD-001
sProcedureName风格一致;不做"先查后插"避免竞态。 -
批量插入
tUserPermission暂用循环:本 REQ 数据量小,YAGNI;性能问题后续 REQ 出现时再优化。 -
测试用 stub JWT 注入:复用
TestJwtHelper,无需新建。
依赖的 schema 表 / 字段
写入:
-
tUser:14 个字段(除tDeletedDate/sDeletedBy) -
tUserPermission:iUserId/iCategoryId/sCreatedBy/tCreateDate/sBrandsId/sSubsidiaryId
读取(仅校验存在性):
-
tStaff:iIncrement+bDeleted -
tPermissionCategory:iIncrement+bDeleted
依赖索引:tUser.uk_user_no / uk_user_name 兜底唯一冲突;tStaff.iIncrement PK;tPermissionCategory.iIncrement PK。
外键:fk_user_staff: iStaffId → tStaff.iIncrement (ON DELETE SET NULL) 在 INSERT 时由 DB 兜底(service 提前校验给更友好错误码)。
依赖的接口
无(USR-001 是用户域 CRUD 起点)。
验收标准
单元测试(UserServiceImplTest,Mockito)
-
createWithValidDto_persistsUser_andUserPermissions— Mock mappers,ArgumentCaptor 抓userMapper.insert+userPermissionMapper.insert × N;断言:sBrandsId="XLY"/sCreatedBy="STUB_ADMIN"/tCreateDate != null/sPasswordHash是 BCrypt 格式(以$2a$或$2b$开头)/ N 条权限关联含正确iUserId/iCategoryId -
createWithoutPermissionCategoryIds_skipsUserPermissionInserts—permissionCategoryIds=null/ 空 list →userPermissionMapper.insert永不调用 -
createWithInvalidUserType_throws40001 -
createWithInvalidLanguage_throws40001 -
createWithStaffNotFound_throws40022—staffMapper.existsActiveById(...)=false -
createWithSomeInvalidPermissionIds_throws40023—permissionCategoryMapper.countActiveByIds([1,2,3])=2,期望抛 40023;userMapper.insert永不调用 -
createWithDuplicateUserNo_throws40020—userMapper.insert抛DuplicateKeyException -
createUsesAuthenticatedUserNoAsCreatedBy— SecurityContextHolder 注 "ALICE",断言sCreatedBy="ALICE"
Mapper IT(UserMapperIT,真实 DB)
-
insertAndSelectById_persistsAllStandardCols— 构造 User 实例插入,selectById比较;sPasswordHash非空且 BCrypt 格式 -
uniqueUserNoConstraint_rejectsDuplicate— 插入两条同 sUserNo(不同 sUserName)→ 第二次DuplicateKeyException
Mapper IT(StaffMapperIT + PermissionCategoryMapperIT,最小化)
-
staffMapper#existsActiveById_handlesAliveDeletedMissing -
permissionCategoryMapper#countActiveByIds_returnsCorrectCount
集成测试(UserControllerIT)
-
postValidBody_with_jwt_returns200_andPersists— 直插一条职员 + 两条权限分类作为前置数据;POST 完整 body 带 JWT;code=0/data.iIncrement>0/data.sUserNo等于请求;JdbcTemplate 验证tUser+tUserPermission行 -
postEmptyBody_returns40001 -
postInvalidUserType_returns40001 -
postInvalidLanguage_returns40001 -
postDuplicateUserNo_returns40020 -
postStaffNotFound_returns40022 -
postPermissionCategoryNotFound_returns40023 -
postWithoutJwt_permitAllStub_returns200_andCreatedBySTUBADMIN -
postTamperedJwt_returns20001
工程验收
-
cd backend && mvn -B test全绿(67 + USR-001 新增 8(svc) + 4(mapperIT) + 9(controllerIT) = 88 用例) -
BCryptPasswordEncoderbean 在SecurityConfig注册,service 通过构造器注入 - SecurityConfig 路径白名单含
/api/usr/** -
// REQ-MOD-001 stub: see USR-004 follow-up锚点保持 -
tUser.sPasswordHash在响应中不回显