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

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-tdd executes 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_yieldsViolation
    • UserCreateDTOValidationTest#invalidLanguageEnum_yieldsViolation
    • UserCreateDTOValidationTest#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 锁定):
    1. 唯一性预检userMapper.selectCount(eq(sUserName, dto.sUserName).eq(bDeleted, false)) > 0 → BizException(USR_USER_NAME_OR_NO_DUP);同理 sUserNo。
    2. iStaffId 校验:dto.iStaffId 非空 → staffMapper.selectById(...);null 或 bDeleted=true → BizException(STAFF_NOT_FOUND)
    3. 权限分类校验:dto.permissionCategoryIds 非空 → permissionCategoryMapper.selectBatchIds(ids);返回的 list 长度 < ids 长度 OR 任一 bDeleted=true → BizException(PERM_CATEGORY_NOT_FOUND)
    4. 构造 UserEntity:复制 dto;bCanModifyDocs null → false;sPasswordHash = passwordEncoder.encode("666666")tCreateDate = nowbDeleted = false;其他字段 null。
    5. Insert useruserMapper.insert(user),捕 DuplicateKeyExceptionUSR_USER_NAME_OR_NO_DUP。MyBatis-Plus 回写 iIncrement 到 entity。
    6. 批量 insert UserPermission:dto.permissionCategoryIds 非空 → 循环逐条 userPermissionMapper.insert(...)(每条 iUserId = user.iIncrement / iCategoryId = id / tCreateDate = now;无 bSelected 列)。
    7. 返回 VOUserVO.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.sPasswordHash doesNotExist
    • 测试方式:@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)