2026-05-08-REQ-USR-001.md 17.5 KB

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/usr/users 新建用户账号,系统自动初始化密码、写入权限关联;前端提供「用户管理」页+新增 Drawer 表单。

Architecture: Spring Boot 后端新增 UserController(三个端点:createUser / getStaffs / getPermissionGroups)+ UserServiceImpl(业务逻辑+事务)+ 三个辅助 Entity/Mapper(StaffEntity、PermissionGroupEntity、UserPermissionEntity)。JwtAuthenticationFilter 升级为存储 UserPrincipal record,使 Controller 通过 @AuthenticationPrincipal 取到 brandId / username / userType。前端新增 api/usr.ts + UserListPage.tsx + UserFormDrawer.tsx,在 App.tsx 补充 /usr/users 路由。

Tech Stack: Spring Boot 3.3.5 + MyBatis-Plus 3.5.7 + Lombok + Spring Security + spring-security-test;React 18 + Ant Design 5 + Vitest + @testing-library/react


文件映射

新建

  • backend/.../config/UserPrincipal.java — JWT principal record(userId, username, userType, brandId)
  • backend/.../module/usr/entity/StaffEntity.java — 映射 tStaff
  • backend/.../module/usr/entity/PermissionGroupEntity.java — 映射 usr_permission_group
  • backend/.../module/usr/entity/UserPermissionEntity.java — 映射 usr_user_permission
  • backend/.../module/usr/mapper/StaffMapper.java
  • backend/.../module/usr/mapper/PermissionGroupMapper.java
  • backend/.../module/usr/mapper/UserPermissionMapper.java
  • backend/.../common/constants/UsrErrorCode.java — 40300/40901/40902
  • backend/.../module/usr/dto/UserCreateReqDTO.java
  • backend/.../module/usr/vo/UserCreateRespVO.java
  • backend/.../module/usr/vo/StaffVO.java
  • backend/.../module/usr/vo/PermissionGroupVO.java
  • backend/.../module/usr/service/UserService.java
  • backend/.../module/usr/service/impl/UserServiceImpl.java
  • backend/.../module/usr/controller/UserController.java
  • backend/.../module/usr/UserServiceTest.java
  • backend/.../module/usr/UserControllerTest.java
  • frontend/src/api/usr.ts
  • frontend/src/pages/usr/UserListPage.tsx
  • frontend/src/pages/usr/UserFormDrawer.tsx
  • frontend/src/test/UserListPage.test.tsx

修改

  • backend/.../config/JwtAuthenticationFilter.java — 存 UserPrincipal 而非 String(CLAUDE.md S2,必要基础设施)
  • frontend/src/App.tsx — 补 /usr/users 路由

合同级常量

UsrErrorCode

  • PERMISSION_DENIED = 40300
  • USERNAME_EXISTS = 40901
  • USER_CODE_EXISTS = 40902

UsrErrorCode 用法throw new BizException(UsrErrorCode.PERMISSION_DENIED, "权限不足")

UserPrincipal(Java record)

package com.example.erp.config;
public record UserPrincipal(String userId, String username, String userType, String brandId) {}

JWT claim 名(来自 JwtUtil.generateAccessToken):

  • subject → userId
  • "username" → username
  • "userType" → userType
  • "brandId" → brandId

Task 1: UserPrincipal + JwtAuthenticationFilter 升级(跨模块基础设施 S2)

Files:

  • Create: backend/src/main/java/com/example/erp/config/UserPrincipal.java
  • Modify: backend/src/main/java/com/example/erp/config/JwtAuthenticationFilter.java

API shape:

  • record UserPrincipal(String userId, String username, String userType, String brandId) {}
  • JwtAuthenticationFilter.doFilterInternal() — 解析 claims 后构造 UserPrincipal,存入 UsernamePasswordAuthenticationToken 的 principal

  • Step 1: 写失败测试(在 UserControllerTest 中)

    • 文件:backend/src/test/java/com/example/erp/module/usr/UserControllerTest.java
    • 仅写类骨架 + 一个空测试方法 createUser_noToken_returns401(),注解 @WebMvcTest(controllers = UserController.class)
    • 此时编译失败(UserController 不存在)→ 子会话确认 FAIL
  • Step 2: 实现 UserPrincipal + 修改 JwtAuthenticationFilter

    • 创建 UserPrincipal.java record(见合同级常量)
    • 修改 JwtAuthenticationFilter.doFilterInternal()java UserPrincipal principal = new UserPrincipal( claims.getSubject(), claims.get("username", String.class), claims.get("userType", String.class), claims.get("brandId", String.class) ); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList()); SecurityContextHolder.getContext().setAuthentication(auth);
  • Step 3: 子会话验证已有测试仍通过

    • 命令:JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home mvn test -pl backend -Dtest=AuthControllerTest,AuthServiceTest
    • 期待:全部 PASS
  • Step 4: Commit

    • git add backend/src/main/java/com/example/erp/config/UserPrincipal.java backend/src/main/java/com/example/erp/config/JwtAuthenticationFilter.java
    • git commit -m "feat(usr): UserPrincipal record + JwtFilter升级存principal REQ-USR-001"

Task 2: 实体 + Mapper + 错误码 + DTO/VO 骨架

Files:

  • Create: backend/src/main/java/com/example/erp/module/usr/entity/StaffEntity.java
  • Create: backend/src/main/java/com/example/erp/module/usr/entity/PermissionGroupEntity.java
  • Create: backend/src/main/java/com/example/erp/module/usr/entity/UserPermissionEntity.java
  • Create: backend/src/main/java/com/example/erp/module/usr/mapper/StaffMapper.java
  • Create: backend/src/main/java/com/example/erp/module/usr/mapper/PermissionGroupMapper.java
  • Create: backend/src/main/java/com/example/erp/module/usr/mapper/UserPermissionMapper.java
  • Create: backend/src/main/java/com/example/erp/common/constants/UsrErrorCode.java
  • Create: backend/src/main/java/com/example/erp/module/usr/dto/UserCreateReqDTO.java
  • Create: backend/src/main/java/com/example/erp/module/usr/vo/UserCreateRespVO.java
  • Create: backend/src/main/java/com/example/erp/module/usr/vo/StaffVO.java
  • Create: backend/src/main/java/com/example/erp/module/usr/vo/PermissionGroupVO.java

StaffEntity 字段(来自 docs/03 tStaff 表):iIncrement(PK), sId, sBrandsId, sSubsidiaryId, tCreateDate, sStaffNo, sStaffName, sDepartment, sCreatedBy, bDeleted(Bit/Integer), tDeletedDate, sDeletedBy@TableName("tStaff")

PermissionGroupEntity 字段iIncrement(PK), sId, sBrandsId, sSubsidiaryId, tCreateDate, sGroupCode, sGroupName, sCategory@TableName("usr_permission_group")

UserPermissionEntity 字段iIncrement(PK), sId, sBrandsId, sSubsidiaryId, tCreateDate, sUserId, sPermGroupId@TableName("usr_user_permission")

UserCreateReqDTO 字段@Valid 注解):

@NotBlank String userCode;
@NotBlank String username;
@NotBlank @Pattern(regexp = "普通用户|超级管理员") String userType;
@NotBlank @Pattern(regexp = "中文|英文|繁体") String language;
boolean canEditDoc = false;
String employeeId;           // nullable
List<String> permGroupIds;   // nullable

UserCreateRespVOString userId, userCode, username StaffVOString sId, sStaffName PermissionGroupVOString sId, sGroupCode, sGroupName, sCategory

  • Step 1: 写失败测试

    • UserServiceTest.java 中写类骨架 + 一个空测试 createUser_normalUser_throws40300(),引用 UserService(未创建)
    • 子会话确认编译 FAIL
  • Step 2: 创建所有文件

    • 按上方规格创建所有 entity / mapper / errorcode / dto / vo 文件
    • 每个 mapper 继承 BaseMapper<T>
    • 每个 entity 使用 Lombok @Getter @Setter
  • Step 3: 子会话验证编译通过

    • 命令:JAVA_HOME=... mvn compile -pl backend
  • Step 4: Commit

    • git add backend/src/main/java/
    • git commit -m "feat(usr): 实体/Mapper/DTO/VO骨架 + UsrErrorCode REQ-USR-001"

Task 3: UserServiceImpl — createUser 业务逻辑

Files:

  • Create: backend/src/main/java/com/example/erp/module/usr/service/UserService.java
  • Create: backend/src/main/java/com/example/erp/module/usr/service/impl/UserServiceImpl.java
  • Test: backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java

API shape:

  • UserService#createUser(UserCreateReqDTO req, UserPrincipal principal) : UserCreateRespVO
  • UserService#getStaffs(String brandId) : List<StaffVO>
  • UserService#getPermissionGroups(String brandId) : List<PermissionGroupVO>

createUser 逻辑序列

  1. !"超级管理员".equals(principal.userType())throw new BizException(40300, "权限不足")
  2. userMapper.selectCount(LQ...eq(sUserCode, req.getUserCode())) > 0throw new BizException(40902, "用户号已存在")
  3. userMapper.selectCount(LQ...eq(sUsername).eq(sBrandsId)) > 0throw new BizException(40901, "用户名已存在")
  4. req.getEmployeeId() != nullstaffMapper.selectOne(LQ...eq(sId).eq(sBrandsId)) == nullthrow new BizException(40001, "员工不存在")
  5. 构造 UsrUserEntity:sId=UUID,sBrandsId=principal.brandId(),sCreatorUsername=principal.username(),tCreateDate=now,sPasswordHash=passwordEncoder.encode("666666"),bIsDisabled=0,iLoginFailCount=0
  6. userMapper.insert(user)
  7. permGroupIds 非 null 且非空 → 循环 userPermissionMapper.insert(new UserPermissionEntity(UUID, brandId, now, user.sId, groupId))
  8. 返回 UserCreateRespVO(user.sId, user.sUserCode, user.sUsername)

UserServiceImpl 依赖@RequiredArgsConstructor;注入 UsrUserMapperStaffMapperPermissionGroupMapperUserPermissionMapperBCryptPasswordEncoder

  • Step 1: 写失败测试

    • UserServiceTest.java 完整写入下列测试(全部引用 UserService 接口),子会话确认失败(UserService 接口不存在):
    • createUser_normalUser_throws40300 — principal.userType=普通用户 → BizException(40300)
    • createUser_success_insertsUserAndReturnsVO — all mocks pass → 验证 userMapper.insert 被调用,返回 VO 有 userId
    • createUser_duplicateUserCode_throws40902 — selectCount 第1次返回 1L → BizException(40902)
    • createUser_duplicateUsername_throws40901 — selectCount 第1次返回 0L,第2次返回 1L → BizException(40901)
    • createUser_withPermGroups_insertsPermissions — permGroupIds=["g1","g2"] → verify userPermissionMapper.insert 被调用 2 次
    • createUser_invalidEmployeeId_throws40001 — staffMapper.selectOne 返回 null → BizException(40001)
  • Step 2: 实现 UserService + UserServiceImpl

    • 创建 UserService.java 接口(三个方法签名)
    • 创建 UserServiceImpl.java@Service @RequiredArgsConstructor,按逻辑序列实现 createUser()
    • getStaffs(brandId): staffMapper.selectList(LQ...eq(sBrandsId, brandId).eq(bDeleted, 0).select("sId","sStaffName")) → 映射 StaffVO
    • getPermissionGroups(brandId): permGroupMapper.selectList(LQ...eq(sBrandsId, brandId)) → 映射 PermissionGroupVO(如 brandId 空则 selectList(null))
  • Step 3: 子会话验证 UserServiceTest 全部通过

    • 命令:JAVA_HOME=... mvn test -pl backend -Dtest=UserServiceTest
  • Step 4: Commit

    • git add backend/src/
    • git commit -m "feat(usr): UserServiceImpl.createUser + getStaffs + getPermissionGroups REQ-USR-001"

Task 4: UserController — 三个端点

Files:

  • Create: backend/src/main/java/com/example/erp/module/usr/controller/UserController.java
  • Test: backend/src/test/java/com/example/erp/module/usr/UserControllerTest.java

API shape:

@RestController
@RequestMapping("/api/usr")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @PostMapping("/users")
    public Result<UserCreateRespVO> createUser(
            @Valid @RequestBody UserCreateReqDTO req,
            @AuthenticationPrincipal UserPrincipal principal) { ... }

    @GetMapping("/users/staffs")
    public Result<List<StaffVO>> getStaffs(@AuthenticationPrincipal UserPrincipal principal) { ... }

    @GetMapping("/users/permission-groups")
    public Result<List<PermissionGroupVO>> getPermissionGroups(@AuthenticationPrincipal UserPrincipal principal) { ... }
}

测试助手SecurityMockMvcRequestPostProcessors.authentication(...) 注入 UserPrincipal:

static RequestPostProcessor superAdmin() {
    return authentication(new UsernamePasswordAuthenticationToken(
        new UserPrincipal("u1","admin","超级管理员","b1"), null, emptyList()));
}
static RequestPostProcessor normalUser() {
    return authentication(new UsernamePasswordAuthenticationToken(
        new UserPrincipal("u2","user","普通用户","b1"), null, emptyList()));
}
  • Step 1: 写失败测试

    • UserControllerTest.java 完整(@WebMvcTest(UserController.class) + @Import(SecurityConfig, JwtAuthenticationFilter, BeanConfig, JwtUtil, JwtProperties) + @MockBean UserService):
    • createUser_noAuth_returns401 — 无 auth header → Spring Security 返回 401
    • createUser_validRequest_returns200superAdmin() + mock service返回VO → 验证 code=200, data.userId 非空
    • createUser_missingUserCode_returns40001 — 请求体 userCode 为空 → 验证 code=40001
    • getStaffs_returns200superAdmin() + mock returns list → 验证 code=200
    • getPermissionGroups_returns200superAdmin() + mock returns list → 验证 code=200
    • 子会话确认 FAIL(UserController 不存在)
  • Step 2: 实现 UserController

    • 按 API shape 创建 UserController.java
    • createUser: Result.ok(userService.createUser(req, principal))
    • getStaffs: Result.ok(userService.getStaffs(principal.brandId()))
    • getPermissionGroups: Result.ok(userService.getPermissionGroups(principal.brandId()))
  • Step 3: 子会话验证 UserControllerTest 全部通过

    • 命令:JAVA_HOME=... mvn test -pl backend -Dtest=UserControllerTest,UserServiceTest,AuthControllerTest,AuthServiceTest
  • Step 4: Commit

    • git add backend/src/
    • git commit -m "feat(usr): UserController POST/users GET/staffs GET/permission-groups REQ-USR-001"

Task 5: 前端 api/usr.ts + UserFormDrawer + UserListPage + App.tsx 路由

Files:

  • Create: frontend/src/api/usr.ts
  • Create: frontend/src/pages/usr/UserListPage.tsx
  • Create: frontend/src/pages/usr/UserFormDrawer.tsx
  • Create: frontend/src/test/UserListPage.test.tsx
  • Modify: frontend/src/App.tsx

api/usr.ts 接口

export interface StaffVO { sId: string; sStaffName: string }
export interface PermissionGroupVO { sId: string; sGroupCode: string; sGroupName: string; sCategory: string | null }
export interface UserCreateReq {
  userCode: string; username: string; userType: '普通用户' | '超级管理员';
  language: '中文' | '英文' | '繁体'; canEditDoc?: boolean;
  employeeId?: string | null; permGroupIds?: string[]
}
export interface UserCreateResp { userId: string; userCode: string; username: string }

export function getStaffs(): Promise<StaffVO[]>
export function getPermissionGroups(): Promise<PermissionGroupVO[]>
export function createUser(req: UserCreateReq): Promise<UserCreateResp>

request.get('/usr/users/staffs') 等)

UserFormDrawer.tsx props: open: boolean, onClose: () => void, onSuccess: () => void

  • useEffect 拉取 staffs + permissionGroups
  • Form 字段:userCode(Input), username(Input), userType(Select), language(Select), canEditDoc(Checkbox), employeeId(Select, options=staffs), permissions(Table with checkbox column)
  • 提交:调 createUser(),成功 → message.success('新增用户成功') + onSuccess();失败 → message.error(e.message)

UserListPage.tsx:

  • 顶部「新增」按钮(<PermButton permission="usr:create">,由 authSlice 中 userType 控制显示)
  • 点击按钮 → setDrawerOpen(true)
  • <UserFormDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} onSuccess={() => setDrawerOpen(false)} />
  • 表格区域为 stub(empty Table,REQ-USR-003 补充)

PermButton(如不存在则在本任务创建 frontend/src/components/PermButton.tsx):

// REQ-USR-001: 权限按钮,根据 userType 控制显示
export function PermButton({ permission, children, ...props }: { permission: string } & ButtonProps) {
  const userType = useAppSelector(s => s.auth.userInfo?.userType)
  // usr:create / usr:edit 仅超级管理员可见
  if (userType !== '超级管理员') return null
  return <Button {...props}>{children}</Button>
}

App.tsx 新增路由

<Route path="/usr/users" element={<PrivateRoute><UserListPage /></PrivateRoute>} />

测试 UserListPage.test.tsx(三个测试):

  1. superAdmin_seesNewButton — store userType=超级管理员 → 「新增」按钮可见
  2. normalUser_doesNotSeeNewButton — store userType=普通用户 → 「新增」按钮不可见
  3. clickNewButton_opensDrawer — 超级管理员点击新增 → UserFormDrawer 出现(mock API calls with vi.mock)
  • Step 1: 写失败测试

    • UserListPage.test.tsx 三个测试(引用 UserListPagePermButton
    • 子会话确认 FAIL(文件不存在)
  • Step 2: 实现

    • 按上方规格创建 api/usr.tscomponents/PermButton.tsxUserFormDrawer.tsxUserListPage.tsx
    • 修改 App.tsx 添加路由
  • Step 3: 子会话验证前端测试通过

    • 命令:cd frontend && npm run test -- --run
  • Step 4: Commit

    • git add frontend/src/
    • git commit -m "feat(usr): 前端 UserListPage + UserFormDrawer + usr.ts + PermButton REQ-USR-001"