--- 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-tdd` executes 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.java` - `backend/src/main/java/com/xly/erp/module/usr/entity/UserPermission.java` - `backend/src/main/java/com/xly/erp/module/usr/dto/CreateUserDTO.java` - `backend/src/main/java/com/xly/erp/module/usr/mapper/UserMapper.java` - `backend/src/main/java/com/xly/erp/module/usr/mapper/UserPermissionMapper.java` - `backend/src/main/java/com/xly/erp/module/usr/mapper/StaffMapper.java` - `backend/src/main/java/com/xly/erp/module/usr/mapper/PermissionCategoryMapper.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/module/usr/mapper/UserMapperIT.java` - `backend/src/test/java/com/xly/erp/module/usr/mapper/StaffMapperIT.java` - `backend/src/test/java/com/xly/erp/module/usr/mapper/PermissionCategoryMapperIT.java` - `backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java` - `backend/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 `(usr): 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-up` - [ ] **Step 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:** - `User` PO:`@TableName("tUser")` + 17 字段 1:1 映射(参 docs/03 § tUser),全部 `@TableField` 显式列名(与 MOD `Module.java` 风格一致),`iIncrement` 用 `@TableId(IdType.AUTO)` - `UserMapper extends BaseMapper`:暂只用 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("") int countActiveByIds(@Param("ids") List 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:** - `UserPermission` PO:`@TableName("tUserPermission")` + 字段 `iIncrement(@TableId AUTO)` / `sId` / `sBrandsId` / `sSubsidiaryId` / `tCreateDate` / `iUserId` / `iCategoryId` / `sCreatedBy`(参 docs/03 § tUserPermission) - `UserPermissionMapper extends BaseMapper`,仅用 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 permissionCategoryIds`(可空,null/空均按"无权限组"处理) - `UserService#create(CreateUserDTO dto) : Map`(返回 `{iIncrement, sUserNo}`);本 plan 用 `Map` 而非新 VO,与 MOD 控制器响应风格保持一致 - `UserServiceImpl` 依赖:`UserMapper` / `UserPermissionMapper` / `StaffMapper` / `PermissionCategoryMapper` / `TenantProperties` / `StubSecurityProperties` / `BCryptPasswordEncoder` - `@Transactional(rollbackFor = Exception.class)` - 流程主路径(仅本 task 实现合法 + 标准列): 1. 构造 `User entity`:DTO 透传 + 标准列(sBrandsId/sSubsidiaryId/tCreateDate/sCreatedBy 同 MOD 模块策略) + `sPasswordHash = encoder.encode("666666")` + `bDeleted=false` 2. `userMapper.insert(entity)` 3. 若 `permissionCategoryIds` 非空:for-loop 插 `UserPermission` 行 4. 返回 `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 实现):** 1. 枚举校验 `sUserType ∈ {普通用户, 超级管理员}` + `sLanguage ∈ {zh, en, zh-TW}` → 任一非法 `BizException(40001, "<字段>: 取值非法")` 2. `iStaffId != null` 且 `!staffMapper.existsActiveById(iStaffId)` → `BizException(40022, "职员不存在或已删除")` 3. `permissionCategoryIds` 非空:`int n = permissionCategoryMapper.countActiveByIds(ids); if (n != ids.size()) throw new BizException(40023, "权限分类含无效 id")` 4. `userMapper.insert(...)` 用 try/catch 捕获 `DuplicateKeyException` → `BizException(40020, "用户号或用户名已存在")` 5. `sCreatedBy` 优先 SecurityContextHelper.currentUserNo(),回退 stub - [ ] **Step 1: 在 `UserServiceImplTest` 追加 6 用例** - `createWithInvalidUserType_throws40001` - `createWithInvalidLanguage_throws40001` - `createWithStaffNotFound_throws40022` — Mock `staffMapper.existsActiveById(...) → false`;`userMapper.insert` 永不调用 - `createWithSomeInvalidPermissionIds_throws40023` — Mock `permissionCategoryMapper.countActiveByIds([1,2,3]) → 2`;`userMapper.insert` 永不调用 - `createWithDuplicateUserNo_throws40020` — Mock `userMapper.insert` 抛 `DuplicateKeyException` - `createUsesAuthenticatedUserNoAsCreatedBy` — `SecurityContextHolder` 注 "ALICE";ArgumentCaptor `sCreatedBy="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> 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 |