REQ-USR-001 增加用户 Implementation Plan
Execution: Parent skill
feature-tddexecutes 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.javabackend/.../module/usr/mapper/PermissionGroupMapper.javabackend/.../module/usr/mapper/UserPermissionMapper.java-
backend/.../common/constants/UsrErrorCode.java— 40300/40901/40902 backend/.../module/usr/dto/UserCreateReqDTO.javabackend/.../module/usr/vo/UserCreateRespVO.javabackend/.../module/usr/vo/StaffVO.javabackend/.../module/usr/vo/PermissionGroupVO.javabackend/.../module/usr/service/UserService.javabackend/.../module/usr/service/impl/UserServiceImpl.javabackend/.../module/usr/controller/UserController.javabackend/.../module/usr/UserServiceTest.javabackend/.../module/usr/UserControllerTest.javafrontend/src/api/usr.tsfrontend/src/pages/usr/UserListPage.tsxfrontend/src/pages/usr/UserFormDrawer.tsxfrontend/src/test/UserListPage.test.tsx
修改:
-
backend/.../config/JwtAuthenticationFilter.java— 存 UserPrincipal 而非 String(CLAUDE.md S2,必要基础设施) -
frontend/src/App.tsx— 补 /usr/users 路由
合同级常量
UsrErrorCode:
PERMISSION_DENIED = 40300USERNAME_EXISTS = 40901USER_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.javarecord(见合同级常量) - 修改
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.javagit 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
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<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) : UserCreateRespVOUserService#getStaffs(String brandId) : List<StaffVO>UserService#getPermissionGroups(String brandId) : List<PermissionGroupVO>
createUser 逻辑序列:
-
!"超级管理员".equals(principal.userType())→throw new BizException(40300, "权限不足") -
userMapper.selectCount(LQ...eq(sUserCode, req.getUserCode())) > 0→throw new BizException(40902, "用户号已存在") -
userMapper.selectCount(LQ...eq(sUsername).eq(sBrandsId)) > 0→throw new BizException(40901, "用户名已存在") -
req.getEmployeeId() != null→staffMapper.selectOne(LQ...eq(sId).eq(sBrandsId)) == null→throw new BizException(40001, "员工不存在") - 构造
UsrUserEntity:sId=UUID,sBrandsId=principal.brandId(),sCreatorUsername=principal.username(),tCreateDate=now,sPasswordHash=passwordEncoder.encode("666666"),bIsDisabled=0,iLoginFailCount=0 userMapper.insert(user)-
permGroupIds非 null 且非空 → 循环userPermissionMapper.insert(new UserPermissionEntity(UUID, brandId, now, user.sId, groupId)) - 返回
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:
@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_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()))
- 按 API shape 创建
-
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(三个测试):
-
superAdmin_seesNewButton— store userType=超级管理员 → 「新增」按钮可见 -
normalUser_doesNotSeeNewButton— store userType=普通用户 → 「新增」按钮不可见 -
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"