req_id: REQ-USR-001 date: 2026-05-06
spec_ref: docs/superpowers/specs/2026-05-06-REQ-USR-001.md
REQ-USR-001 用户新增 Implementation Plan
Execution: Parent skill
feature-tddexecutes this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: 实现 POST /api/users:录入用户基本信息 + 可选员工关联 + 权限组关联,密码 666666 经 BCrypt 哈希落库;返回 UserVO(不含哈希)。
Architecture: 复用 module_mod 已建立的 common / config / 异常 / Jackson / Security 体系。新建 4 个 entity + 4 个 mapper(tUser / tStaff / tPermissionCategory / tUserPermission),UserService 协调跨表写入并用 @Transactional 包裹整体一致性。BCryptPasswordEncoder 注册为 Spring bean 供 REQ-USR-004 复用。
Tech Stack: Spring Boot 3.2.5 + Spring Security 6(BCryptPasswordEncoder)+ MyBatis-Plus 3.5.7 + JUnit 5 + Mockito。
Schema 改动
无(V1 已建 5 张表 + FK + UNIQUE 索引)。
文件变更清单
- 修改:
backend/src/main/java/com/xly/erp/common/response/ErrorCode.java— 追加 3 个常量 - 创建:
backend/src/main/java/com/xly/erp/config/PasswordConfig.java— BCryptPasswordEncoder bean - 创建:
backend/src/main/java/com/xly/erp/module/usr/entity/UserEntity.java - 创建:
backend/src/main/java/com/xly/erp/module/usr/entity/StaffEntity.java - 创建:
backend/src/main/java/com/xly/erp/module/usr/entity/PermissionCategoryEntity.java - 创建:
backend/src/main/java/com/xly/erp/module/usr/entity/UserPermissionEntity.java - 创建:
backend/src/main/java/com/xly/erp/module/usr/mapper/{UserMapper,StaffMapper,PermissionCategoryMapper,UserPermissionMapper}.java - 创建:
backend/src/main/java/com/xly/erp/module/usr/dto/UserCreateDTO.java - 创建:
backend/src/main/java/com/xly/erp/module/usr/vo/UserVO.java - 创建:
backend/src/main/java/com/xly/erp/module/usr/service/UserService.java - 创建:
backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java - 创建:
backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java - 修改:
backend/src/test/java/com/xly/erp/common/response/ApiResponseTest.java— 追加 3 个新错误码断言 - 创建:
backend/src/test/java/com/xly/erp/module/usr/dto/UserCreateDTOValidationTest.java - 创建:
backend/src/test/java/com/xly/erp/module/usr/mapper/UsrMappersIT.java— 4 张表 insert/select smoke test - 创建:
backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java— 9 个 mock 单测 - 创建:
backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java— 7 个 MockMvc 集成测试
任务步骤
Task 1: 错误码 + PasswordConfig
Files:
- Modify:
backend/src/main/java/com/xly/erp/common/response/ErrorCode.java - Create:
backend/src/main/java/com/xly/erp/config/PasswordConfig.java - Modify:
backend/src/test/java/com/xly/erp/common/response/ApiResponseTest.java
API shape:
- 新增 ErrorCode 常量(注意:与 MOD 段位共享但枚举名不同):
STAFF_NOT_FOUND(40421, "职员不存在或已删除")PERM_CATEGORY_NOT_FOUND(40422, "权限分类不存在或已删除")USR_USER_NAME_OR_NO_DUP(40921, "用户名或用户号已存在")
注:
MOD_NOT_FOUND(40421)与STAFF_NOT_FOUND(40421)code 相同但枚举名不同——code 段位由 docs/05 全局错误码表定义,message 文案区分语义;此设计与 docs/04 § 1.3 错误码段位划分一致。
PasswordConfig提供@Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
-
Step 1.1 写失败断言
- 在
ApiResponseTest#errorCode_constantsMatchDocs05Spec末尾追加 3 行:assertThat(ErrorCode.STAFF_NOT_FOUND.getCode()).isEqualTo(40421); assertThat(ErrorCode.PERM_CATEGORY_NOT_FOUND.getCode()).isEqualTo(40422); assertThat(ErrorCode.USR_USER_NAME_OR_NO_DUP.getCode()).isEqualTo(40921); - 子会话: FAIL
- 在
-
Step 1.2 实现 ErrorCode + PasswordConfig
- 子会话: PASS(ApiResponseTest 5/5;上下文重启 PasswordConfig bean 注入由后续 IT 验证)
-
Step 1.3 提交
git commit -m "feat(common): error codes + PasswordConfig REQ-USR-001"
Task 2: 4 张表 entity + mapper + Mapper smoke IT
Files:
- Create:
backend/src/main/java/com/xly/erp/module/usr/entity/{UserEntity,StaffEntity,PermissionCategoryEntity,UserPermissionEntity}.java - Create:
backend/src/main/java/com/xly/erp/module/usr/mapper/{UserMapper,StaffMapper,PermissionCategoryMapper,UserPermissionMapper}.java - Test:
backend/src/test/java/com/xly/erp/module/usr/mapper/UsrMappersIT.java
API shape(每个 entity 严格按 docs/03 字段;@TableField 显式声明列名;保留匈牙利前缀;主键 iIncrement IdType.AUTO;与 ModuleEntity 同范式):
| Entity | 表 | 关键字段(不含 5 个标准列) |
|---|---|---|
UserEntity |
tUser | sUserNo / sUserName / iStaffId / sUserType / sLanguage / bCanModifyDocs / sPasswordHash / tLastLoginDate / sCreatedBy / bDeleted / tDeletedDate / sDeletedBy |
StaffEntity |
tStaff | sStaffNo / sStaffName / sDepartment / sCreatedBy / bDeleted / tDeletedDate / sDeletedBy |
PermissionCategoryEntity |
tPermissionCategory | sCategoryCode / sCategoryName / iParentId / iSortOrder / sCreatedBy / bDeleted / tDeletedDate / sDeletedBy |
UserPermissionEntity |
tUserPermission | iUserId / iCategoryId / sCreatedBy(docs/03 修订版无 bSelected 列) |
每个 mapper extends BaseMapper<XxxEntity>,无自定义 SQL。
注意:spec § Service 实现依赖
tUser.iStaffId设置为 NULL 时不应被 IGNORED 策略影响(参考 REQ-MOD-002 经验)。本期 UserEntity 字段 不给 iStaffId 加FieldStrategy.IGNORED——因为 user create 走 insert(默认 NOT_NULL 策略对 insert 没问题:null 字段被跳过,DB 列默认 NULL),不会触发 update path 上的副作用。如未来 REQ-USR-002 需要 update 中清空 iStaffId,再按 REQ-MOD-003 经验用LambdaUpdateWrapper.set(...)处理。
-
Step 2.1 写失败 IT
-
UsrMappersIT#allFourMappers_insertAndSelect_smoke:用 4 个 mapper 各 insert 一条最小字段记录(构造 entity → insert → selectById 断言字段往返) @SpringBootTest @ActiveProfiles("test") @Transactional @Rollback- 子会话: FAIL(entity / mapper 不存在)
-
-
Step 2.2 实现 4 entity + 4 mapper
- 子会话: PASS
-
Step 2.3 提交
git commit -m "feat(usr): user/staff/permission/userPermission entities + mappers REQ-USR-001"
Task 3: UserCreateDTO + UserVO + Validation
Files:
- Create:
backend/src/main/java/com/xly/erp/module/usr/dto/UserCreateDTO.java - Create:
backend/src/main/java/com/xly/erp/module/usr/vo/UserVO.java - Test:
backend/src/test/java/com/xly/erp/module/usr/dto/UserCreateDTOValidationTest.java
API shape:
UserCreateDTO:
@NotBlank @Size(max=50) String sUserNo@NotBlank @Size(max=50) String sUserName-
Integer iStaffId(可空) @NotBlank @Pattern(regexp="^(普通用户|超级管理员)$") String sUserType@NotBlank @Pattern(regexp="^(zh|en|zh-TW)$") String sLanguage-
Boolean bCanModifyDocs(可空,service 层 default false) -
List<Integer> permissionCategoryIds(可空 / 空数组)
UserVO 字段 10 个(spec § 输出列表;不含 sPasswordHash),含静态工厂 from(UserEntity entity, List<Integer> permissionCategoryIds)。
-
Step 3.1 写失败测试(5 个)
UserCreateDTOValidationTest#allValidFields_yieldsNoViolations-
UserCreateDTOValidationTest#blankRequiredFields_yieldsViolations(4 个 @NotBlank) UserCreateDTOValidationTest#invalidUserTypeEnum_yieldsViolationUserCreateDTOValidationTest#invalidLanguageEnum_yieldsViolationUserCreateDTOValidationTest#overSizedFields_yieldsViolations- 子会话: FAIL
-
Step 3.2 实现 DTO + VO
- 子会话: PASS
-
Step 3.3 提交
git commit -m "feat(usr): user create DTO and VO REQ-USR-001"
Task 4: UserService.create + Mockito 单元测试
Files:
- Create:
backend/src/main/java/com/xly/erp/module/usr/service/UserService.java - Create:
backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java - Test:
backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java
API shape:
interface UserService { UserVO create(UserCreateDTO dto); }-
@Service @RequiredArgsConstructor class UserServiceImpl,依赖UserMapper/StaffMapper/PermissionCategoryMapper/UserPermissionMapper/PasswordEncoder。 -
create(dto)步骤(plan 锁定):-
唯一性预检:
userMapper.selectCount(eq(sUserName, dto.sUserName).eq(bDeleted, false))> 0 →BizException(USR_USER_NAME_OR_NO_DUP);同理 sUserNo。 -
iStaffId 校验:dto.iStaffId 非空 →
staffMapper.selectById(...);null 或 bDeleted=true →BizException(STAFF_NOT_FOUND)。 -
权限分类校验:dto.permissionCategoryIds 非空 →
permissionCategoryMapper.selectBatchIds(ids);返回的 list 长度 < ids 长度 OR 任一 bDeleted=true →BizException(PERM_CATEGORY_NOT_FOUND)。 -
构造 UserEntity:复制 dto;
bCanModifyDocsnull → false;sPasswordHash = passwordEncoder.encode("666666");tCreateDate = now;bDeleted = false;其他字段 null。 -
Insert user:
userMapper.insert(user),捕DuplicateKeyException→USR_USER_NAME_OR_NO_DUP。MyBatis-Plus 回写iIncrement到 entity。 -
批量 insert UserPermission:dto.permissionCategoryIds 非空 → 循环逐条
userPermissionMapper.insert(...)(每条iUserId = user.iIncrement/iCategoryId = id/tCreateDate = now;无 bSelected 列)。 -
返回 VO:
UserVO.from(user, dto.permissionCategoryIds 或 [])。
-
唯一性预检:
- 标
@Transactional(rollbackFor = Exception.class)
初始密码常量(写在 plan 锁定):
private static final String INITIAL_PASSWORD = "666666";
后续若策略变化,service 单点修改。
-
Step 4.1 写失败测试(9 个)
-
create_minimalFields_returnsVOWithBCryptHash:mock 全部 selectCount=0 / passwordEncoder.encode → "$2a$bcrypt";insert 设 iIncrement;断言 VO + 断言传给 userMapper.insert 的 entity.sPasswordHash 等于 mock 返回值 -
create_withStaffAndPermissions_writesAssociation:mock staff / batch ids 校验通过;断言 userPermissionMapper.insert 被调 N 次 + 每次 entity 字段 -
create_duplicateUserName_throws40921:selectCount(sUserName)>0 -
create_duplicateUserNo_throws40921:selectCount(sUserNo)>0 -
create_staffNotFound_throws40421:staffMapper.selectById → null -
create_staffSoftDeleted_throws40421:staff.bDeleted=true -
create_permissionCategoryNotFound_throws40422:selectBatchIds 返回比 ids 短 -
create_emptyPermissionCategoryIds_doesNotInsertAssociation:permissionCategoryIds=[],断言 userPermissionMapper.insert 从未被调 -
create_concurrentDuplicate_dupKeyException_mappedTo40921:mock userMapper.insert 抛 DuplicateKeyException - 测试方式:
@ExtendWith(MockitoExtension.class)+ArgumentCaptor<UserEntity>/ArgumentCaptor<UserPermissionEntity> - 子会话: FAIL
-
-
Step 4.2 实现 UserService + Impl
- 子会话: PASS
-
Step 4.3 提交
git commit -m "feat(usr): create user service REQ-USR-001"
Task 5: UserController + 端到端 IT
Files:
- Create:
backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java - Test:
backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java
API shape:
@RestController @RequestMapping("/api/users") @RequiredArgsConstructor class UserController@PostMapping ApiResponse<UserVO> create(@Valid @RequestBody UserCreateDTO dto)Javadoc:
REQ-MOD-001 用户新增 — REQ-USR-004 完成后追加 @PreAuthorize("hasAuthority('USR:CREATE')")-
Step 5.1 写失败测试(7 个)
-
post_minimalFields_returns200:仅必填字段;断言 200 + data.sUserName + data.bCanModifyDocs=false + data.permissionCategoryIds=[] -
post_withStaffAndPermissions_returns200_andDbAssociated:先 mapper.insert 一条 staff + 3 条 permissionCategory,POST 用户附 permissionCategoryIds;断言 DB tUserPermission 有 3 条匹配 -
post_duplicateUserName_returns40921:先 POST 一次,再 POST 同 sUserName -
post_staffNotFound_returns40421:iStaffId=999999 -
post_permissionCategoryNotFound_returns40422:permissionCategoryIds=[999999] -
post_passwordHashedInDb_notPlaintext:POST 后 selectById;断言 sPasswordHash 以 "$2a$" 或 "$2b$" 开头,且不含明文 "666666" -
post_responseExcludesSPasswordHash:jsonPath$.data.sPasswordHashdoesNotExist - 测试方式:
@SpringBootTest @AutoConfigureMockMvc @Transactional @Rollback+@Autowired UserMapper / StaffMapper / PermissionCategoryMapper / UserPermissionMapper - 子会话: FAIL(端点不存在)
-
-
Step 5.2 实现 UserController
- 子会话: PASS
-
Step 5.3 跑全量 backend 测试
cd backend && mvn -B test- 期望累计 76(module_mod 现有)+ 1(ApiResponse 错误码扩展) + 4(MapperIT) + 5(DTO Valid) + 9(service unit) + 7(controller IT) = 102 测试,全绿。
-
Step 5.4 提交
git commit -m "feat(usr): POST /api/users controller REQ-USR-001"
提交计划
-
feat(common): error codes + PasswordConfig REQ-USR-001(Task 1) -
feat(usr): user/staff/permission/userPermission entities + mappers REQ-USR-001(Task 2) -
feat(usr): user create DTO and VO REQ-USR-001(Task 3) -
feat(usr): create user service REQ-USR-001(Task 4) -
feat(usr): POST /api/users controller REQ-USR-001(Task 5)