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 body(UserCreateDTO)字段:
| 字段 | 类型 | 必填 | 校验 / 取值 | 落库列 |
|---|---|---|---|---|
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 里:默认
666666经org.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。
业务规则
-
唯一性:
sUserNo与sUserName在bDeleted=0范围内系统内全局唯一。冲突 →BizException(USR_USER_NAME_OR_NO_DUP)(40921)。 -
职员校验:若
iStaffId非空,必须selectById(iStaffId)存在且bDeleted=0;不存在或已删 →BizException(STAFF_NOT_FOUND)(40421)。 -
权限分类校验:若
permissionCategoryIds非空,每个 id 都要存在且未软删除(一次selectBatchIds一次性校验);任一不存在 →BizException(PERM_CATEGORY_NOT_FOUND)(40422)。 -
密码哈希:固定初始密码字符串
"666666"→BCryptPasswordEncoder().encode("666666")→ 落tUser.sPasswordHash。BCryptPasswordEncoder注册为 Spring Bean(PasswordConfig)便于 REQ-USR-004 复用。 -
关联表写入:
tUserPermission按permissionCategoryIds逐条insert(每条iUserId=新用户 id/iCategoryId=对应分类 id/tCreateDate=now;docs/03 修订版无 bSelected 列,关联记录存在即「已选」)。 -
新建记录初始状态:
bDeleted=0、tDeletedDate=NULL、sDeletedBy=NULL、tCreateDate=LocalDateTime.now()、tLastLoginDate=NULL、sCreatedBy=NULL(多租户/登录上下文未引入)、sBrandsId=NULL、sSubsidiaryId=NULL、sId=NULL。 -
事务边界:
@Transactional(rollbackFor = Exception.class)包住 用户校验 + 用户 insert + 权限关联批量 insert 三步;任一失败整体回滚。 -
并发兜底: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 表 / 字段
写表:tUser、tUserPermission
读表: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,但不依赖本接口运行时 — 仅依赖本接口建立的数据。
验收标准
功能正确性
-
正向 — 最小字段(无 staff、无权限):传入 sUserNo / sUserName / sUserType / sLanguage 必填,返回 200 +
data.iIncrement+ 默认 bCanModifyDocs=false / permissionCategoryIds=[];DB 中 sPasswordHash 为 BCrypt 哈希(不等于 "666666" 明文)。 - 正向 — 含 staff + 权限:传入合法 iStaffId + 3 个 permissionCategoryIds,返回 200;DB tUser.iStaffId 等于入参;tUserPermission 中存在 3 条关联(iUserId=新用户)。
- 唯一性冲突 — sUserName:先建一个 sUserName=alice 的用户,再用同 sUserName 提交,返回 40921。
- 唯一性冲突 — sUserNo:同上,sUserNo 冲突。
- iStaffId 不存在:传入 iStaffId=999999,返回 40421。
- iStaffId 已软删除:先建 staff 后置 bDeleted=1,再 POST,返回 40421。
- permissionCategoryIds 任一不存在:传入 [1, 999999],返回 40422,且 DB 中 tUser 与 tUserPermission 都未写入(事务回滚)。
- 必填缺失 / 枚举非法 / 长度超限:返回 40010。
-
空 permissionCategoryIds:传
[]或不传该字段,正向通过(无关联记录)。 -
密码哈希不可逆:直接读 DB sPasswordHash,断言以
$2a$或$2b$开头(BCrypt 标准前缀),且不含 "666666" 明文。 -
响应不暴露 sPasswordHash:jsonPath
$.data.sPasswordHashdoesNotExist。
接口契约一致性
- 响应格式
{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 中)。