2026-05-06-REQ-USR-001.md 10.3 KB

req_id: REQ-USR-001 date: 2026-05-06

module: module_usr

Spec: REQ-USR-001 — 用户新增

目标

实现后端 POST /api/users 接口:录入新用户基本信息 + 员工关联(可选)+ 权限组关联,密码默认 666666 经 BCrypt 哈希后落库;返回 iIncrement + 用户 VO(不含密码哈希)。

输入 / 触发

接口POST /api/users,Content-Type application/json

Request bodyUserCreateDTO)字段:

字段 类型 必填 校验 / 取值 落库列
sUserNo String 长度 1-50;bDeleted=0 范围内系统内唯一 tUser.sUserNo
sUserName String 长度 1-50;bDeleted=0 范围内系统内唯一(登录账号) tUser.sUserName
iStaffId Integer 必须指向存在且未软删除的 tStaff.iIncrement tUser.iStaffId
sUserType String 枚举:普通用户 / 超级管理员 tUser.sUserType
sLanguage String 枚举:zh / en / zh-TW tUser.sLanguage
bCanModifyDocs Boolean 默认 false tUser.bCanModifyDocs
permissionCategoryIds List 每个元素须指向存在且未软删除的 tPermissionCategory.iIncrement;可空数组(无授权) 写入 tUserPermission 关联表

鉴权:契约要求 Authorization: Bearer <accessToken> + USR:CREATE。沿用 module_mod 的 SecurityConfig permitAll;Controller Javadoc:REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('USR:CREATE')")

密码不在 DTO 里:默认 666666org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder 哈希后落库。BCrypt 已经在 spring-boot-starter-security 中包含,无需新增依赖。

输出 / 结果

HTTP 200,响应体

{
  "code": 200,
  "message": "操作成功",
  "data": {
    "iIncrement": 12,
    "sUserNo": "u001",
    "sUserName": "alice",
    "iStaffId": 7,
    "sUserType": "普通用户",
    "sLanguage": "zh",
    "bCanModifyDocs": false,
    "tCreateDate": "2026-05-06T10:30:00",
    "bDeleted": false,
    "permissionCategoryIds": [1, 2, 3]
  },
  "timestamp": 1746528600000
}

新建 VO UserVO:字段 iIncrement / sUserNo / sUserName / iStaffId / sUserType / sLanguage / bCanModifyDocs / tCreateDate / bDeleted / permissionCategoryIds(聚合自 tUserPermission)。

不返回sPasswordHash / sId / sBrandsId / sSubsidiaryId / sCreatedBy / tLastLoginDate / tDeletedDate / sDeletedBy

业务规则

  1. 唯一性sUserNosUserNamebDeleted=0 范围内系统内全局唯一。冲突 → BizException(USR_USER_NAME_OR_NO_DUP) (40921)。
  2. 职员校验:若 iStaffId 非空,必须 selectById(iStaffId) 存在且 bDeleted=0;不存在或已删 → BizException(STAFF_NOT_FOUND) (40421)。
  3. 权限分类校验:若 permissionCategoryIds 非空,每个 id 都要存在且未软删除(一次 selectBatchIds 一次性校验);任一不存在 → BizException(PERM_CATEGORY_NOT_FOUND) (40422)。
  4. 密码哈希:固定初始密码字符串 "666666"BCryptPasswordEncoder().encode("666666") → 落 tUser.sPasswordHashBCryptPasswordEncoder 注册为 Spring Bean(PasswordConfig)便于 REQ-USR-004 复用。
  5. 关联表写入tUserPermissionpermissionCategoryIds 逐条 insert(每条 iUserId=新用户 id / iCategoryId=对应分类 id / tCreateDate=now;docs/03 修订版无 bSelected 列,关联记录存在即「已选」)。
  6. 新建记录初始状态bDeleted=0tDeletedDate=NULLsDeletedBy=NULLtCreateDate=LocalDateTime.now()tLastLoginDate=NULLsCreatedBy=NULL(多租户/登录上下文未引入)、sBrandsId=NULLsSubsidiaryId=NULLsId=NULL
  7. 事务边界@Transactional(rollbackFor = Exception.class) 包住 用户校验 + 用户 insert + 权限关联批量 insert 三步;任一失败整体回滚。
  8. 并发兜底:DB 唯一索引 uk_user_no / uk_user_name 兜底唯一性;service 捕 DuplicateKeyException 映射为 USR_USER_NAME_OR_NO_DUP

边界与约束

鉴权策略

沿用 module_mod 的 SecurityConfig permitAll。注释 // REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('USR:CREATE')")

错误码映射

场景 错误码 ErrorCode 枚举
必填缺失 / 类型 / 长度 / 枚举非法 40010 PARAM_INVALID(已存在)
iStaffId 不存在 / 已删除 40421 STAFF_NOT_FOUND新增
permissionCategoryIds 任一不存在 / 已删除 40422 PERM_CATEGORY_NOT_FOUND新增
sUserNo / sUserName 唯一冲突 40921 USR_USER_NAME_OR_NO_DUP新增
服务端兜底 50000 INTERNAL_ERROR

docs/05 § REQ-USR-001 中列的错误码 40020 / 40921 / 40421 / 40422 —— 段位约定 40020 是 USR 模块的参数错(非 MOD 模块的 40010)。本 spec 统一沿用 40010 作为参数错(与 GlobalExceptionHandler 现有映射一致),避免在两套段位里折腾。docs/05 后续 sweep 时再统一对齐。

性能 / 并发

  • 单条用户 + 至多 N 条权限关联 insert,预期低并发。uk_user_no / uk_user_name 唯一约束兜底。
  • permissionCategoryIds 批量校验用 selectBatchIds 单次 SQL,O(1) round-trip。

字符集 / 长度

  • utf8mb4,允许中文姓名 / 用户名(虽然多数业务侧用英文)。
  • 长度超 schema 上限 → 视为参数错 40010。

与 docs/04 § 1.4 / 3.5 一致性

  • 异常走 GlobalExceptionHandler。
  • BCryptPasswordEncoder bean 不硬编码 strength(默认 10),从 application.yml 读取 strength(暂留默认)。

已知技术债

  • sCreatedBy=NULL:REQ-USR-004 引入登录上下文后回填。
  • 多租户字段 NULL:与 module_mod 一致,REQ-USR-004 后由拦截器注入。

依赖的 schema 表 / 字段

写表tUsertUserPermission

读表tStaff(关联校验 / 后续 REQ-USR-003 列表 join)、tPermissionCategory(关联校验 / 列表只读字典)

tUser 字段 落库逻辑
iIncrement DB AUTO_INCREMENT
sUserNo / sUserName 入参(必填,唯一)
iStaffId 入参(可选;FK 校验通过的 tStaff.iIncrement
sUserType / sLanguage 入参(必填,枚举)
bCanModifyDocs 入参(可选,默认 false)
sPasswordHash BCrypt("666666")
tCreateDate LocalDateTime.now()
tLastLoginDate / sCreatedBy / 多租户 / sId / bDeleted 三件套 见 § 业务规则 6
tUserPermission 字段 落库逻辑
iIncrement DB AUTO_INCREMENT
iUserId 新用户 iIncrement
iCategoryId dto.permissionCategoryIds[i]
tCreateDate LocalDateTime.now()
sCreatedBy NULL(REQ-USR-004 后回填)
多租户 / sId NULL

索引利用

  • uk_user_no / uk_user_name(UNIQUE):用户唯一性预检 + 兜底
  • uk_user_perm (UNIQUE iUserId+iCategoryId):防重复授权(应用层不会触发,DB 兜底)

外键

  • fk_user_staff(tUser.iStaffId → tStaff.iIncrement):应用层先查再 insert,避免直接抛 SQL 完整性异常
  • fk_up_user(tUserPermission.iUserId → tUser.iIncrement):CASCADE,本接口无需关心
  • fk_up_category(tUserPermission.iCategoryId → tPermissionCategory.iIncrement):应用层先 selectBatchIds 再 insert

依赖的接口

无(本接口独立工作)。

REQ-USR-002 / 003 / 004 都会读 tUser,但不依赖本接口运行时 — 仅依赖本接口建立的数据。

验收标准

功能正确性

  1. 正向 — 最小字段(无 staff、无权限):传入 sUserNo / sUserName / sUserType / sLanguage 必填,返回 200 + data.iIncrement + 默认 bCanModifyDocs=false / permissionCategoryIds=[];DB 中 sPasswordHash 为 BCrypt 哈希(不等于 "666666" 明文)。
  2. 正向 — 含 staff + 权限:传入合法 iStaffId + 3 个 permissionCategoryIds,返回 200;DB tUser.iStaffId 等于入参;tUserPermission 中存在 3 条关联(iUserId=新用户)。
  3. 唯一性冲突 — sUserName:先建一个 sUserName=alice 的用户,再用同 sUserName 提交,返回 40921。
  4. 唯一性冲突 — sUserNo:同上,sUserNo 冲突。
  5. iStaffId 不存在:传入 iStaffId=999999,返回 40421。
  6. iStaffId 已软删除:先建 staff 后置 bDeleted=1,再 POST,返回 40421。
  7. permissionCategoryIds 任一不存在:传入 [1, 999999],返回 40422,且 DB 中 tUser 与 tUserPermission 都未写入(事务回滚)。
  8. 必填缺失 / 枚举非法 / 长度超限:返回 40010。
  9. 空 permissionCategoryIds:传 [] 或不传该字段,正向通过(无关联记录)。
  10. 密码哈希不可逆:直接读 DB sPasswordHash,断言以 $2a$$2b$ 开头(BCrypt 标准前缀),且不含 "666666" 明文。
  11. 响应不暴露 sPasswordHash:jsonPath $.data.sPasswordHash doesNotExist。

接口契约一致性

  • 响应格式 {code, message, data, timestamp}
  • 不回显堆栈。

测试覆盖

  • 单元测试 UserServiceImplTest:mock UserMapper / StaffMapper / PermissionCategoryMapper / UserPermissionMapper / BCryptPasswordEncoder
    • create_minimalFields_returnsVOWithBCryptHash
    • create_withStaffAndPermissions_writesAssociation
    • create_duplicateUserName_throws40921
    • create_duplicateUserNo_throws40921
    • create_staffNotFound_throws40421
    • create_staffSoftDeleted_throws40421
    • create_permissionCategoryNotFound_throws40422
    • create_emptyPermissionCategoryIds_doesNotInsertAssociation
    • create_concurrentDuplicate_dupKeyException_mappedTo40921
  • 集成测试 UserControllerIT
    • post_minimalFields_returns200
    • post_withStaffAndPermissions_returns200_andDbAssociated
    • post_duplicateUserName_returns40921
    • post_staffNotFound_returns40421
    • post_permissionCategoryNotFound_returns40422
    • post_passwordHashedInDb_notPlaintext
    • post_responseExcludesSPasswordHash

代码与文档

  • // REQ-USR-001 注释贴在 Controller / Service / 新增 ErrorCode / DTO / VO。
  • 提交按 feat(usr): <subject> REQ-USR-001 规范。
  • 不引入 docs/04 § 零 技术栈外的依赖(BCryptPasswordEncoder 已在 spring-boot-starter-security 中)。