2026-04-30-REQ-USR-001.md 15.1 KB

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 <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-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<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 @AfterEachDELETE 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:

  • UserPermission PO:@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 实现合法 + 标准列):

    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_skipsUserPermissionInsertspermissionCategoryIds=nulluserPermissionMapper.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 捕获 DuplicateKeyExceptionBizException(40020, "用户号或用户名已存在")
  5. sCreatedBy 优先 SecurityContextHelper.currentUserNo(),回退 stub
  • Step 1: 在 UserServiceImplTest 追加 6 用例

    • createWithInvalidUserType_throws40001
    • createWithInvalidLanguage_throws40001
    • createWithStaffNotFound_throws40022 — Mock staffMapper.existsActiveById(...) → falseuserMapper.insert 永不调用
    • createWithSomeInvalidPermissionIds_throws40023 — Mock permissionCategoryMapper.countActiveByIds([1,2,3]) → 2userMapper.insert 永不调用
    • createWithDuplicateUserNo_throws40020 — Mock userMapper.insertDuplicateKeyException
    • createUsesAuthenticatedUserNoAsCreatedBySecurityContextHolder 注 "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<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_returns40001sUserType="火星"
    • postInvalidLanguage_returns40001sLanguage="ja"
    • postDuplicateUserNo_returns40020 — 先 POST 一次成功,再 POST 同 sUserNo(不同 sUserName)→ code=40020
    • postStaffNotFound_returns40022iStaffId=99999990code=40022
    • postPermissionCategoryNotFound_returns40023permissionCategoryIds=[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