req_id: REQ-USR-001 date: 2026-04-30
spec_ref: docs/superpowers/specs/2026-04-30-REQ-USR-001.md
REQ-USR-001 用户新增 Implementation Plan
Execution: Parent skill
feature-tddexecutes this plan task-by-task.
Goal: 在 MOD 模块已建工程基础上扩展 USR 模块树,实现 POST /api/usr/users:新增 tUser 行 + 默认 BCrypt 密码哈希 + tUserPermission 多对多关联,含 iStaffId / permissionCategoryIds 存在性校验。
Architecture: 新建 module/usr/ 平行模块树(entity/dto/mapper/service/controller)+ 在 common/security/SecurityConfig 加 /api/usr/** permitAll + 注册 BCryptPasswordEncoder bean。Staff / PermissionCategory 仅做存在性校验(最小化 mapper,不建 entity)。事务包"3 项校验 + INSERT user + INSERT user_permission × N"。
Tech Stack: 复用(Spring Boot 3.3.5 / MyBatis-Plus / Spring Security / JJWT);本 REQ 引入 BCryptPasswordEncoder(spring-security-crypto 已通过 starter-security 引入,无需新依赖)。
Schema 改动
无(tUser / tStaff / tPermissionCategory / tUserPermission 均在 V1 就位)。
文件变更清单
新增
backend/src/main/java/com/xly/erp/module/usr/entity/User.javabackend/src/main/java/com/xly/erp/module/usr/entity/UserPermission.javabackend/src/main/java/com/xly/erp/module/usr/dto/CreateUserDTO.javabackend/src/main/java/com/xly/erp/module/usr/mapper/UserMapper.javabackend/src/main/java/com/xly/erp/module/usr/mapper/UserPermissionMapper.javabackend/src/main/java/com/xly/erp/module/usr/mapper/StaffMapper.javabackend/src/main/java/com/xly/erp/module/usr/mapper/PermissionCategoryMapper.javabackend/src/main/java/com/xly/erp/module/usr/service/UserService.javabackend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.javabackend/src/main/java/com/xly/erp/module/usr/controller/UserController.javabackend/src/test/java/com/xly/erp/module/usr/mapper/UserMapperIT.javabackend/src/test/java/com/xly/erp/module/usr/mapper/StaffMapperIT.javabackend/src/test/java/com/xly/erp/module/usr/mapper/PermissionCategoryMapperIT.javabackend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.javabackend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java
修改
-
backend/src/main/java/com/xly/erp/common/security/SecurityConfig.java— 加/api/usr/**permitAll +@Bean BCryptPasswordEncoder
任务步骤
全局:每 commit
<type>(usr): <subject> REQ-USR-001;测试派发子会话;现有 67 用例全程绿。
Task 1: SecurityConfig 扩展 + BCryptPasswordEncoder bean
Files:
- Modify:
backend/src/main/java/com/xly/erp/common/security/SecurityConfig.java
API shape:
- 在
authorizeHttpRequests的现有requestMatchers("/api/mod/**").permitAll()后追加requestMatchers("/api/usr/**").permitAll() - 类内追加
@Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } stub 注释保持
// REQ-MOD-001 stub: see USR-004 follow-upStep 1: 修改 SecurityConfig
-
Step 2: 子会话验证 PASS
- 命令:
cd backend && mvn -B test - 期望:现有 67 用例全绿(路径扩范围不收紧)
- 命令:
-
Step 3: Commit
git commit -m "refactor(usr): widen permitAll to /api/usr/** + bcrypt bean REQ-USR-001"
Task 2: tUser entity + UserMapper + IT
Files:
- Create:
backend/src/main/java/com/xly/erp/module/usr/entity/User.java - Create:
backend/src/main/java/com/xly/erp/module/usr/mapper/UserMapper.java - Create:
backend/src/test/java/com/xly/erp/module/usr/mapper/UserMapperIT.java
API shape:
-
UserPO:@TableName("tUser")+ 17 字段 1:1 映射(参 docs/03 § tUser),全部@TableField显式列名(与 MODModule.java风格一致),iIncrement用@TableId(IdType.AUTO) UserMapper extends BaseMapper<User>:暂只用 BaseMapper 的 insert/selectById(唯一冲突走 DB 索引兜底,无需自定义 exists 方法)-
Step 1: 写失败测试
UserMapperIT(2 用例)-
insertAndSelectById_persistsAllStandardCols— 构造 User 实例(含 sPasswordHash="bcrypt-stub")插入 → selectById 比较;断言 sBrandsId="XLY"、bDeleted=false 等 -
uniqueUserNoConstraint_rejectsDuplicate— 插入两条同 sUserNo(不同 sUserName)→ 第二次抛DuplicateKeyException - 测试隔离:
@BeforeEach @AfterEach用DELETE FROM tUserPermission WHERE iUserId IN (SELECT iIncrement FROM tUser WHERE sUserNo LIKE 'sp_test_%')+DELETE FROM tUser WHERE sUserNo LIKE 'sp_test_%'(避免外键孤儿,但 USR-001 的 tUserPermission 与 tUser 在本 IT 不会被插入;此句保留为防御)
-
Step 2: 实现 entity + mapper
-
Step 3: 子会话验证 PASS
- 命令:
cd backend && mvn -B test -Dtest=UserMapperIT
- 命令:
-
Step 4: Commit
git commit -m "feat(usr): tUser entity + mapper REQ-USR-001"
Task 3: StaffMapper + PermissionCategoryMapper(最小存在性查询)
Files:
- Create:
backend/src/main/java/com/xly/erp/module/usr/mapper/StaffMapper.java - Create:
backend/src/main/java/com/xly/erp/module/usr/mapper/PermissionCategoryMapper.java - Create:
backend/src/test/java/com/xly/erp/module/usr/mapper/StaffMapperIT.java - Create:
backend/src/test/java/com/xly/erp/module/usr/mapper/PermissionCategoryMapperIT.java
API shape:
-
StaffMapper(不继承 BaseMapper,仅注解 SELECT;本 REQ 不建 Staff entity):@Select("SELECT 1 FROM tStaff WHERE iIncrement = #{id} AND bDeleted = 0 LIMIT 1") Integer findActiveStaffFlag(@Param("id") Integer id)default boolean existsActiveById(Integer id) { return findActiveStaffFlag(id) != null; }
-
PermissionCategoryMapper(同上):-
@Select("<script>SELECT COUNT(1) FROM tPermissionCategory WHERE bDeleted = 0 AND iIncrement IN <foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach></script>") int countActiveByIds(@Param("ids") List<Integer> ids)(mybatis 动态 SQL);空 list 调用方先短路,不调 mapper
-
-
Step 1: 写失败测试
-
StaffMapperIT#existsActiveById_handlesAliveDeletedMissing:JdbcTemplate 直插 alive staff + deleted staff;断言三种 id(alive/deleted/不存在)的返回值 -
PermissionCategoryMapperIT#countActiveByIds_returnsCorrectCount:JdbcTemplate 直插 cat1(alive) + cat2(alive) + cat3(deleted);查 [cat1,cat2,cat3] → count=2;查 [cat1, 99999] → count=1;查 [99999] → count=0
-
Step 2: 实现 mapper
-
Step 3: 子会话验证 PASS
- 命令:
cd backend && mvn -B test -Dtest='StaffMapperIT,PermissionCategoryMapperIT'
- 命令:
-
Step 4: Commit
git commit -m "feat(usr): staff + permission-category existence mappers REQ-USR-001"
Task 4: tUserPermission entity + UserPermissionMapper + IT
Files:
- Create:
backend/src/main/java/com/xly/erp/module/usr/entity/UserPermission.java - Create:
backend/src/main/java/com/xly/erp/module/usr/mapper/UserPermissionMapper.java - 复用
UserMapperIT(追加用例)或独立UserPermissionMapperIT,本 plan 选择追加到 UserMapperIT
API shape:
-
UserPermissionPO:@TableName("tUserPermission")+ 字段iIncrement(@TableId AUTO)/sId/sBrandsId/sSubsidiaryId/tCreateDate/iUserId/iCategoryId/sCreatedBy(参 docs/03 § tUserPermission) UserPermissionMapper extends BaseMapper<UserPermission>,仅用 BaseMapper.insert-
Step 1: 写失败测试(追加到
UserMapperIT)-
userPermissionInsert_persistsRowWithUserAndCategory— 先插一行 user,再插一行 userPermission(iUserId=user.id, iCategoryId=10);JdbcTemplate 验 row 存在
-
Step 2: 实现 entity + mapper
-
Step 3: 子会话验证 PASS
- 命令:
cd backend && mvn -B test -Dtest=UserMapperIT
- 命令:
-
Step 4: Commit
git commit -m "feat(usr): tUserPermission entity + mapper REQ-USR-001"
Task 5: CreateUserDTO + UserService.create 主流程(合法 + 标准列)
Files:
- Create:
backend/src/main/java/com/xly/erp/module/usr/dto/CreateUserDTO.java - 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 - Create:
backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java
API shape:
-
CreateUserDTO字段(带@JsonProperty+ Bean Validation):@NotBlank @Size(max=50) String sUserNo@NotBlank @Size(max=50) String sUserName-
Integer iStaffId(可空) @NotBlank String sUserType@NotBlank String sLanguage-
Boolean bCanModifyDocs(可空) -
List<Integer> permissionCategoryIds(可空,null/空均按"无权限组"处理)
-
UserService#create(CreateUserDTO dto) : Map<String, Object>(返回{iIncrement, sUserNo});本 plan 用Map<String, Object>而非新 VO,与 MOD 控制器响应风格保持一致 -
UserServiceImpl依赖:UserMapper/UserPermissionMapper/StaffMapper/PermissionCategoryMapper/TenantProperties/StubSecurityProperties/BCryptPasswordEncoder @Transactional(rollbackFor = Exception.class)-
流程主路径(仅本 task 实现合法 + 标准列):
- 构造
User entity:DTO 透传 + 标准列(sBrandsId/sSubsidiaryId/tCreateDate/sCreatedBy 同 MOD 模块策略) +sPasswordHash = encoder.encode("666666")+bDeleted=false userMapper.insert(entity)- 若
permissionCategoryIds非空:for-loop 插UserPermission行 - 返回
Map.of("iIncrement", entity.getIIncrement(), "sUserNo", entity.getSUserNo())
- 构造
-
Step 1: 写失败测试(2 用例)
-
createWithValidDto_persistsUser_andUserPermissions— Mock mappers + encoder.encode 返回 "$2a$10$stub";ArgumentCaptor 抓userMapper.insert+userPermissionMapper.insert;断言 user.sBrandsId="XLY"、sCreatedBy="STUB_ADMIN"、sPasswordHash 以 "$2a$" 开头;N 条权限关联含正确 iUserId / iCategoryId -
createWithoutPermissionCategoryIds_skipsUserPermissionInserts—permissionCategoryIds=null;userPermissionMapper.insert永不调用
-
-
Step 2: 实现 DTO + service 主流程
- 仅覆盖本 task 两用例所需逻辑(异常分支留 Task 6)
-
Step 3: 子会话验证 PASS
- 命令:
cd backend && mvn -B test -Dtest=UserServiceImplTest
- 命令:
-
Step 4: Commit
git commit -m "feat(usr): user create dto + service happy path REQ-USR-001"
Task 6: Service 异常分支补全
Files:
- Modify:
backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java - Modify:
backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java
API shape: 不变(仅在 service 头部补 4 类校验 + 异常翻译)
校验顺序(service 实现):
- 枚举校验
sUserType ∈ {普通用户, 超级管理员}+sLanguage ∈ {zh, en, zh-TW}→ 任一非法BizException(40001, "<字段>: 取值非法") -
iStaffId != null且!staffMapper.existsActiveById(iStaffId)→BizException(40022, "职员不存在或已删除") -
permissionCategoryIds非空:int n = permissionCategoryMapper.countActiveByIds(ids); if (n != ids.size()) throw new BizException(40023, "权限分类含无效 id") -
userMapper.insert(...)用 try/catch 捕获DuplicateKeyException→BizException(40020, "用户号或用户名已存在") -
sCreatedBy优先 SecurityContextHelper.currentUserNo(),回退 stub
-
Step 1: 在
UserServiceImplTest追加 6 用例createWithInvalidUserType_throws40001createWithInvalidLanguage_throws40001-
createWithStaffNotFound_throws40022— MockstaffMapper.existsActiveById(...) → false;userMapper.insert永不调用 -
createWithSomeInvalidPermissionIds_throws40023— MockpermissionCategoryMapper.countActiveByIds([1,2,3]) → 2;userMapper.insert永不调用 -
createWithDuplicateUserNo_throws40020— MockuserMapper.insert抛DuplicateKeyException -
createUsesAuthenticatedUserNoAsCreatedBy—SecurityContextHolder注 "ALICE";ArgumentCaptorsCreatedBy="ALICE"
Step 2: 在 ServiceImpl 补 4 类校验 + 异常翻译
-
Step 3: 子会话验证 PASS
- 命令:
cd backend && mvn -B test -Dtest=UserServiceImplTest - 期望:2 + 6 = 8 用例全绿
- 命令:
-
Step 4: Commit
git commit -m "feat(usr): user create error branches REQ-USR-001"
Task 7: UserController POST + IT(9 用例)+ 全量回归
Files:
- Create:
backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java - Create:
backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java
API shape:
@RestController @RequestMapping("/api/usr")@PostMapping("/users") public Result<Map<String, Object>> create(@Valid @RequestBody CreateUserDTO dto) { return Result.ok(userService.create(dto)); }-
Step 1: 写失败 IT(9 用例)
-
postValidBody_with_jwt_returns200_andPersists— 前置:JdbcTemplate 直插一行 staff + 两行 permission_category;POST 完整 body 带 JWT;code=0/data.iIncrement>0/data.sUserNo == 请求值;JdbcTemplate 验 tUser 行存在 + tUserPermission 行数 == permissionCategoryIds.size() postEmptyBody_returns40001-
postInvalidUserType_returns40001—sUserType="火星" -
postInvalidLanguage_returns40001—sLanguage="ja" -
postDuplicateUserNo_returns40020— 先 POST 一次成功,再 POST 同 sUserNo(不同 sUserName)→code=40020 -
postStaffNotFound_returns40022—iStaffId=99999990→code=40022 -
postPermissionCategoryNotFound_returns40023—permissionCategoryIds=[99999991]→code=40023 -
postWithoutJwt_permitAllStub_returns200_andCreatedBySTUBADMIN— DB 验 sCreatedBy="STUB_ADMIN" -
postTamperedJwt_returns20001— Authorization "Bearer not.a.real.jwt";DB 无新增行 - 测试隔离:
@BeforeEach @AfterEach清理tUserPermission+tUser(按 sUserNo LIKE 'sp_test_%')+ 清理本 IT 创建的 tStaff / tPermissionCategory(按 sStaffNo / sCategoryCode LIKE 'sp_test_%');按外键依赖顺序删(tUserPermission → tUser → tStaff / tPermissionCategory)
-
Step 2: 实现 controller
-
Step 3: 子会话跑全量回归
- 命令:
cd backend && mvn -B test - 期望:MOD 67 + USR-001 新增 2(UserMapperIT) + 1(UserMapperIT 追加) + 1(StaffMapperIT) + 1(PermissionCategoryMapperIT) + 8(UserServiceImplTest) + 9(UserControllerIT) = 89 用例全绿
- 命令:
-
Step 4: Commit
git commit -m "test(usr): user create integration coverage REQ-USR-001"
提交计划
| commit | 覆盖 |
|---|---|
refactor(usr): widen permitAll to /api/usr/** + bcrypt bean REQ-USR-001 |
Task 1 |
feat(usr): tUser entity + mapper REQ-USR-001 |
Task 2 |
feat(usr): staff + permission-category existence mappers REQ-USR-001 |
Task 3 |
feat(usr): tUserPermission entity + mapper REQ-USR-001 |
Task 4 |
feat(usr): user create dto + service happy path REQ-USR-001 |
Task 5 |
feat(usr): user create error branches REQ-USR-001 |
Task 6 |
test(usr): user create integration coverage REQ-USR-001 |
Task 7 |