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 透传。sCreatedBySecurityContextHelper.currentUserNo(),匿名状态回退 stubProps.stubUserNo(与 MOD 模块同策略)。

输出 / 结果

成功响应

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

持久化效果

事务内两步:

  1. INSERT tUser:DTO 字段 + 标准列 + sPasswordHash = bcrypt("666666") + tLastLoginDate=NULL + bDeleted=0
  2. permissionCategoryIds 中的每个 id:INSERT tUserPermission(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

业务规则

  1. 唯一性策略:DB uk_user_no + uk_user_name 包含已软删行;本期不支持"软删后用户号复用"——若 sUserNo 历史上有过删除记录,再次创建会被 DB 唯一索引拒绝。docs/03 § tUser 业务注记说"应用层保证(仅约束未删除部分)"是设计意向,本 REQ 实现保持与 V1 schema 一致:依赖 DB 唯一索引兜底,service 层捕获 DuplicateKeyExceptionBizException(40020,"用户号或用户名已存在"),不再做"先查后插"二次校验(避免竞态)。
  2. iStaffId 校验:非 null 时调 staffMapper.existsActiveById(iStaffId);false → BizException(40022,"职员不存在或已删除")
  3. permissionCategoryIds 校验:非空时一次性查 permissionCategoryMapper.countActiveByIds(ids),若返回数 != ids.size → BizException(40023,"权限分类含无效 id")避免 N+1(一条 IN 查询)。
  4. sUserType / sLanguage 枚举:service 入口处用 Set.contains 校验;非法 → BizException(40001, "<字段名>: 取值非法")
  5. 密码哈希:使用 Spring Security 的 BCryptPasswordEncoder(已通过 starter-security 引入),强度默认 10。@Bean BCryptPasswordEncoderSecurityConfig 注册(仅本 REQ 引入,避免循环依赖,参 § 实现范围抉择)。
  6. 事务:service 上 @Transactional(rollbackFor = Exception.class),包"校验 + INSERT user + INSERT user_permission * N"。任一步骤失败回滚,不留残行。
  7. 批量插入策略:本 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,避免哈希泄漏

实现范围与边界抉择

  1. 复用 MOD 模块工程:无新增 pom 依赖;backend/src/main/java/com/xly/erp/module/usr/ 全新模块树,与 module/mod/ 平行。
  2. SecurityConfig 路径扩展:在现有 /api/mod/** permitAll 同位加 /api/usr/** permitAll,stub 注释保持 // REQ-MOD-001 stub: see USR-004 follow-up(USR-004 时整段一次性收紧)。
  3. BCryptPasswordEncoder 注册位置:本 REQ 在 SecurityConfig@Bean BCryptPasswordEncoder,service 通过构造器注入。USR-004 登录接口同样依赖此 bean,无重复定义。
  4. Staff / PermissionCategory 仅做存在性校验:本 REQ 不建 Staff / PermissionCategory 完整 entity,仅建 StaffMapper / PermissionCategoryMapper 两个最小化接口(注解 SELECT 1 / SELECT COUNT)。后续 USR-002/003 真正用到完整字段时再补 entity。
  5. 唯一冲突处理走 DB 索引兜底:与 MOD-001 sProcedureName 风格一致;不做"先查后插"避免竞态。
  6. 批量插入 tUserPermission 暂用循环:本 REQ 数据量小,YAGNI;性能问题后续 REQ 出现时再优化。
  7. 测试用 stub JWT 注入:复用 TestJwtHelper,无需新建。

依赖的 schema 表 / 字段

写入:

  • tUser:14 个字段(除 tDeletedDate / sDeletedBy
  • tUserPermissioniUserId / iCategoryId / sCreatedBy / tCreateDate / sBrandsId / sSubsidiaryId

读取(仅校验存在性):

  • tStaffiIncrement + bDeleted
  • tPermissionCategoryiIncrement + 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_skipsUserPermissionInsertspermissionCategoryIds=null / 空 list → userPermissionMapper.insert 永不调用
  • createWithInvalidUserType_throws40001
  • createWithInvalidLanguage_throws40001
  • createWithStaffNotFound_throws40022staffMapper.existsActiveById(...)=false
  • createWithSomeInvalidPermissionIds_throws40023permissionCategoryMapper.countActiveByIds([1,2,3])=2,期望抛 40023;userMapper.insert 永不调用
  • createWithDuplicateUserNo_throws40020userMapper.insertDuplicateKeyException
  • 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 用例)
  • BCryptPasswordEncoder bean 在 SecurityConfig 注册,service 通过构造器注入
  • SecurityConfig 路径白名单含 /api/usr/**
  • // REQ-MOD-001 stub: see USR-004 follow-up 锚点保持
  • tUser.sPasswordHash 在响应中回显