# 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)**: ```java 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` 注解): ```java @NotBlank String userCode; @NotBlank String username; @NotBlank @Pattern(regexp = "普通用户|超级管理员") String userType; @NotBlank @Pattern(regexp = "中文|英文|繁体") String language; boolean canEditDoc = false; String employeeId; // nullable List permGroupIds; // nullable ``` **UserCreateRespVO**:`String userId, userCode, username` **StaffVO**:`String sId, sStaffName` **PermissionGroupVO**:`String sId, sGroupCode, sGroupName, sCategory` - [ ] **Step 1: 写失败测试** - 在 `UserServiceTest.java` 中写类骨架 + 一个空测试 `createUser_normalUser_throws40300()`,引用 `UserService`(未创建) - 子会话确认编译 FAIL - [ ] **Step 2: 创建所有文件** - 按上方规格创建所有 entity / mapper / errorcode / dto / vo 文件 - 每个 mapper 继承 `BaseMapper` - 每个 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` - `UserService#getPermissionGroups(String brandId) : List` **createUser 逻辑序列**: 1. `!"超级管理员".equals(principal.userType())` → `throw new BizException(40300, "权限不足")` 2. `userMapper.selectCount(LQ...eq(sUserCode, req.getUserCode())) > 0` → `throw new BizException(40902, "用户号已存在")` 3. `userMapper.selectCount(LQ...eq(sUsername).eq(sBrandsId)) > 0` → `throw new BizException(40901, "用户名已存在")` 4. `req.getEmployeeId() != null` → `staffMapper.selectOne(LQ...eq(sId).eq(sBrandsId)) == null` → `throw 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`;注入 `UsrUserMapper`、`StaffMapper`、`PermissionGroupMapper`、`UserPermissionMapper`、`BCryptPasswordEncoder` - [ ] **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:** ```java @RestController @RequestMapping("/api/usr") @RequiredArgsConstructor public class UserController { private final UserService userService; @PostMapping("/users") public Result createUser( @Valid @RequestBody UserCreateReqDTO req, @AuthenticationPrincipal UserPrincipal principal) { ... } @GetMapping("/users/staffs") public Result> getStaffs(@AuthenticationPrincipal UserPrincipal principal) { ... } @GetMapping("/users/permission-groups") public Result> getPermissionGroups(@AuthenticationPrincipal UserPrincipal principal) { ... } } ``` **测试助手** — `SecurityMockMvcRequestPostProcessors.authentication(...)` 注入 UserPrincipal: ```java 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_returns200` — `superAdmin()` + mock service返回VO → 验证 code=200, data.userId 非空 - `createUser_missingUserCode_returns40001` — 请求体 userCode 为空 → 验证 code=40001 - `getStaffs_returns200` — `superAdmin()` + mock returns list → 验证 code=200 - `getPermissionGroups_returns200` — `superAdmin()` + 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 接口**: ```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 export function getPermissionGroups(): Promise export function createUser(req: UserCreateReq): Promise ``` (`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**: - 顶部「新增」按钮(``,由 authSlice 中 userType 控制显示) - 点击按钮 → `setDrawerOpen(true)` - ` setDrawerOpen(false)} onSuccess={() => setDrawerOpen(false)} />` - 表格区域为 stub(empty Table,REQ-USR-003 补充) **PermButton**(如不存在则在本任务创建 `frontend/src/components/PermButton.tsx`): ```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 } ``` **App.tsx 新增路由**: ```tsx } /> ``` **测试 `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` 三个测试(引用 `UserListPage`、`PermButton`) - 子会话确认 FAIL(文件不存在) - [ ] **Step 2: 实现** - 按上方规格创建 `api/usr.ts`、`components/PermButton.tsx`、`UserFormDrawer.tsx`、`UserListPage.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"`