req_id: REQ-USR-002 date: 2026-05-08
spec_ref: docs/superpowers/specs/2026-05-08-REQ-USR-002.md
修改用户 Implementation Plan
Execution: Parent skill
feature-tddexecutes this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: 实现 PUT /api/usr/users/{userId},允许超级管理员修改用户类型、语言、权限等,并在前端用户列表增加"修改"按钮触发编辑抽屉。
Architecture: 后端遵循现有三层(Controller → Service → Mapper)模式,新增 UserUpdateReqDTO/UserUpdateRespVO,UserServiceImpl.updateUser 做权限、存在性、自身角色三重校验后执行字段更新 + 权限组先删后插;前端复用 UserFormDrawer(新增 userId/initialData props 区分 create/edit 模式),UserListPage 增加"操作"列。
Tech Stack: Spring Boot 3 / MyBatis-Plus LambdaUpdateWrapper / @Transactional / React 18 / Ant Design 5 / Vitest
Schema 改动
无(不需要新 migration)
文件变更清单
- Create:
backend/src/main/java/com/example/erp/module/usr/dto/UserUpdateReqDTO.java— 修改用户请求体 - Create:
backend/src/main/java/com/example/erp/module/usr/vo/UserUpdateRespVO.java— 修改用户响应 VO - Modify:
backend/src/main/java/com/example/erp/common/constants/UsrErrorCode.java— 追加 USER_NOT_FOUND=40400 / SELF_ADMIN_CHANGE=40301 - Modify:
backend/src/main/java/com/example/erp/module/usr/service/UserService.java— 追加 updateUser 接口方法 - Modify:
backend/src/main/java/com/example/erp/module/usr/service/impl/UserServiceImpl.java— 实现 updateUser - Modify:
backend/src/main/java/com/example/erp/module/usr/controller/UserController.java— 追加 PUT /users/{userId} - Modify:
backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java— 追加 updateUser 测试 - Modify:
backend/src/test/java/com/example/erp/module/usr/UserControllerTest.java— 追加 HTTP 测试 - Modify:
frontend/src/api/usr.ts— 追加 UserUpdateReq / UserUpdateResp / updateUser() - Modify:
frontend/src/pages/usr/UserFormDrawer.tsx— 支持 edit 模式(userId + initialData props) - Modify:
frontend/src/pages/usr/UserListPage.tsx— 追加操作列 + editingUser 状态 - Modify:
frontend/src/test/UserListPage.test.tsx— 追加编辑流程测试
任务步骤
Task 1: 错误码 + DTO/VO 数据结构
Files:
- Modify:
backend/src/main/java/com/example/erp/common/constants/UsrErrorCode.java - Create:
backend/src/main/java/com/example/erp/module/usr/dto/UserUpdateReqDTO.java - Create:
backend/src/main/java/com/example/erp/module/usr/vo/UserUpdateRespVO.java - Test:
backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java
API shape:
UserUpdateReqDTO {
String userType; // "普通用户" | "超级管理员"
String language; // "中文" | "英文" | "繁体"
boolean canEditDoc;
boolean isDisabled;
String employeeId; // nullable
List<String> permGroupIds;
}
UserUpdateRespVO {
String userId;
String username;
LocalDateTime updatedAt;
}
UsrErrorCode.USER_NOT_FOUND = 40400
UsrErrorCode.SELF_ADMIN_CHANGE = 40301
-
Step 1: 写失败测试
- 测试名:
UserServiceTest#updateUser_dtoDefaults - 意图:
new UserUpdateReqDTO()后各字段可 set/get;new UserUpdateRespVO()可 set/get userId/username/updatedAt;UsrErrorCode.USER_NOT_FOUND == 40400,UsrErrorCode.SELF_ADMIN_CHANGE == 40301 - 子会话确认 FAIL(类不存在)
- 测试名:
-
Step 2: 实现最小代码
- 在 UsrErrorCode 追加两个常量
- 创建 UserUpdateReqDTO(@Getter @Setter,6 个字段)
- 创建 UserUpdateRespVO(@Getter @Setter,3 个字段:userId/username/updatedAt: LocalDateTime)
Step 3: 子会话验证 PASS
-
Step 4: Commit
git add backend/src/main/java/com/example/erp/common/constants/UsrErrorCode.java backend/src/main/java/com/example/erp/module/usr/dto/UserUpdateReqDTO.java backend/src/main/java/com/example/erp/module/usr/vo/UserUpdateRespVO.java backend/src/test/java/com/example/erp/module/usr/UserServiceTest.javagit commit -m "feat(usr): 添加 UserUpdateReqDTO / UserUpdateRespVO / 错误码 REQ-USR-002"
Task 2: UserService.updateUser 实现(含权限校验与权限组 replace)
Files:
- Modify:
backend/src/main/java/com/example/erp/module/usr/service/UserService.java - Modify:
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#updateUser(String userId, UserUpdateReqDTO req, UserPrincipal principal) : UserUpdateRespVO
业务逻辑约束(按顺序执行):
-
!"超级管理员".equals(principal.userType())→BizException(PERMISSION_DENIED, "权限不足") -
userMapper.selectOne(sId=userId AND sBrandsId=brandId)== null →BizException(USER_NOT_FOUND, "用户不存在") -
principal.userId().equals(userId) && !"超级管理员".equals(req.getUserType())→BizException(SELF_ADMIN_CHANGE, "禁止修改自己的管理员角色") -
req.getEmployeeId() != null→ staffMapper 校验存在性,不存在 →BizException(EMPLOYEE_NOT_FOUND, "员工不存在") - 用
LambdaUpdateWrapper<UsrUserEntity>更新 sUserType/sLanguage/bCanEditDoc/bIsDisabled/sEmployeeId(按 sId=userId AND sBrandsId=brandId) -
userPermissionMapper.delete(sUserId=userId AND sBrandsId=brandId),再批量 insert 新 UserPermissionEntity 列表(permGroupIds 为空时只删不插) - 返回 UserUpdateRespVO(userId, username=entity.sUsername, updatedAt=LocalDateTime.now())
- Step 1: 写失败测试(共 6 个)
T1 updateUser_nonAdmin_throws40300
- 意图: principal.userType="普通用户" → BizException.code==40300
- principal: new UserPrincipal("u1","admin","普通用户","b1")
T2 updateUser_userNotFound_throws40400
- 意图: 超级管理员 principal,userMapper.selectOne 返回 null → BizException.code==40400
T3 updateUser_selfAdminChange_throws40301
- 意图: principal.userId=="u1",userId=="u1",req.userType="普通用户" → BizException.code==40301
- userMapper.selectOne 需 stub 返回一个存在的用户实体
T4 updateUser_invalidEmployee_throws40001
- 意图: req.employeeId="bad-emp",staffMapper.selectOne 返回 null → BizException.code==40001
- userMapper.selectOne stub 返回有效用户;principal.userId!="u-target" 确保不触发 40301
T5 updateUser_happyPath_updatesAndReturnsResp
- 意图: 一切校验通过 → userMapper 执行 update;permissionMapper 先 delete 再 insert;返回 vo.userId/username/updatedAt 非 null
- Stub: userMapper.selectOne 返回含 sUsername="alice" 的实体;staffMapper.selectOne 返回非 null(若 employeeId 非 null);userMapper.update 返回 1;delete/insert 正常
T6 updateUser_emptyPermGroupIds_onlyDeletes
- 意图: req.permGroupIds=[] → userPermissionMapper.delete 被调用;insert 不被调用
verify(userPermissionMapper, never()).insert(anyList())
-
子会话确认 6 个测试均 FAIL(方法不存在)
- Step 2: 在 UserService 接口追加方法声明,在 UserServiceImpl 实现 updateUser
使用 @Transactional
-
注意: UsrUserEntity 已有 sId / sUserType / sLanguage / bCanEditDoc / bIsDisabled / sEmployeeId 字段(来自现有代码)
- Step 3: 子会话验证全部 PASS
- Step 4: Commit
git add backend/src/main/java/com/example/erp/module/usr/service/ backend/src/test/java/com/example/erp/module/usr/UserServiceTest.javagit commit -m "feat(usr): UserService.updateUser 三重校验 + 权限组 replace REQ-USR-002"
Task 3: UserController PUT /api/usr/users/{userId}
Files:
- Modify:
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:
@PutMapping("/users/{userId}")
public Result<UserUpdateRespVO> updateUser(
@PathVariable String userId,
@Valid @RequestBody UserUpdateReqDTO req,
@AuthenticationPrincipal UserPrincipal principal)
// REQ-USR-002: 修改用户
- Step 1: 写失败测试(共 2 个)
T1 updateUser_withToken_returns200
- 意图: PUT /api/usr/users/u-target,含超级管理员 Token,body={userType,language,canEditDoc,isDisabled,permGroupIds}
- stub: userService.updateUser(any,any,any) 返回 UserUpdateRespVO{userId="u-target",username="alice",updatedAt=now}
- 断言: HTTP 200, $.code==200, $.data.userId=="u-target", $.data.username=="alice"
T2 updateUser_noAuth_returns401
意图: 无 Token → HTTP 401
-
子会话确认 FAIL(endpoint 不存在)
- Step 2: 在 UserController 追加 PUT /users/{userId} 方法,加 // REQ-USR-002: 修改用户 注释
- Step 3: 子会话验证 PASS
- Step 4: Commit
git add backend/src/main/java/com/example/erp/module/usr/controller/UserController.java backend/src/test/java/com/example/erp/module/usr/UserControllerTest.javagit commit -m "feat(usr): PUT /api/usr/users/{userId} REQ-USR-002"
Task 4: 前端 API + UserFormDrawer edit 模式
Files:
- Modify:
frontend/src/api/usr.ts - Modify:
frontend/src/pages/usr/UserFormDrawer.tsx - Test:
frontend/src/test/UserListPage.test.tsx
API shape (usr.ts):
export interface UserUpdateReq {
userType: string
language: string
canEditDoc: boolean
isDisabled: boolean
employeeId: string | null
permGroupIds: string[]
}
export interface UserUpdateResp {
userId: string
username: string
updatedAt: string
}
export function updateUser(userId: string, req: UserUpdateReq): Promise<UserUpdateResp>
// → request.put(`/usr/users/${userId}`, req)
UserFormDrawer props 扩展:
interface Props {
open: boolean
onClose: () => void
onSuccess: () => void
userId?: string // 非 null = edit 模式
initialData?: {
userType: string
language: string
canEditDoc: boolean
isDisabled: boolean
employeeId?: string | null
}
}
edit 模式行为:
- 抽屉 title="修改用户",隐藏 userCode/username Form.Item
- open 时用 initialData 初始化 form(form.setFieldsValue),selectedPermIds=[]
提交调 updateUser(userId, req) 而非 createUser;req.isDisabled 从 form.values.isDisabled 读取(Checkbox valuePropName="checked")
Step 1: 写失败测试
在 UserListPage.test.tsx vi.mock 里追加:
updateUser: vi.fn().mockResolvedValue({ userId: 'u1', username: 'alice', updatedAt: '2026-05-08T10:00:00' })
T1 editMode_drawerTitle_shows修改用户
- renderPage,设 drawerOpen=true + editingUser 存在(通过点击模拟行上的修改按钮)
- 断言: screen 中存在 "修改用户" 文本
T2 editMode_submit_callsUpdateUser
- 打开 edit 抽屉,点击确认
waitFor: vi.mocked(updateUser) 被调用 1 次
-
子会话确认 FAIL(updateUser 不存在于 mock 和 usr.ts)
- Step 2: 在 usr.ts 追加 UserUpdateReq / UserUpdateResp / updateUser;改造 UserFormDrawer 支持 edit props
- Step 3: 子会话验证 PASS
- Step 4: Commit
git add frontend/src/api/usr.ts frontend/src/pages/usr/UserFormDrawer.tsx frontend/src/test/UserListPage.test.tsxgit commit -m "feat(usr): updateUser API + UserFormDrawer edit 模式 REQ-USR-002"
Task 5: UserListPage 操作列 + editingUser 状态
Files:
- Modify:
frontend/src/pages/usr/UserListPage.tsx - Test:
frontend/src/test/UserListPage.test.tsx
行为:
- columns 末尾追加 "操作" 列,每行渲染
<PermButton permission="usr:edit" onClick={() => setEditingUser(row)}>修改</PermButton> - 新增状态:
const [editingUser, setEditingUser] = useState<UserListItemVO | null>(null) - UserFormDrawer 新增两个 props:
-
open={drawerOpen || editingUser !== null}(或用 editDrawerOpen 独立状态) userId={editingUser?.sId}initialData={editingUser ? { userType: editingUser.sUserType, language: editingUser.sLanguage, canEditDoc: false, isDisabled: editingUser.bIsDisabled === 1, employeeId: null } : undefined}- 注意:UserListItemVO 不含 bCanEditDoc,edit 模式下默认 false,用户可手动勾选
onSuccess={() => { setEditingUser(null); load(1) }}onClose={() => setEditingUser(null)}
-
注意:区分新增抽屉(drawerOpen)和编辑抽屉(editingUser !== null),两者可独立触发 UserFormDrawer,也可复用同一个实例——推荐独立实例避免状态冲突:新增用 drawerOpen/setDrawerOpen,编辑用 editingUser/setEditingUser。
- Step 1: 写失败测试
T1 editButton_openEditDrawer
- mock getUserList 返回 1 行(sId='u1',sUsername='alice',...)
- renderPage('超级管理员')
- waitFor: screen 中有 "alice" 渲染出来
- 点击 "修改" 按钮
waitFor: screen 中有 "修改用户" 文本(抽屉标题)
-
子会话确认 FAIL(无"修改"按钮)
- Step 2: 在 UserListPage 追加操作列和编辑抽屉逻辑;追加 // REQ-USR-002: 修改用户 注释
- Step 3: 子会话验证 PASS(全量测试:后端 45+ / 前端 11+)
- Step 4: Commit
git add frontend/src/pages/usr/UserListPage.tsx frontend/src/test/UserListPage.test.tsxgit commit -m "feat(usr): UserListPage 修改按钮 + 编辑抽屉 REQ-USR-002"
提交计划
-
feat(usr): 添加 UserUpdateReqDTO / UserUpdateRespVO / 错误码 REQ-USR-002(覆盖 Task 1) -
feat(usr): UserService.updateUser 三重校验 + 权限组 replace REQ-USR-002(覆盖 Task 2) -
feat(usr): PUT /api/usr/users/{userId} REQ-USR-002(覆盖 Task 3) -
feat(usr): updateUser API + UserFormDrawer edit 模式 REQ-USR-002(覆盖 Task 4) -
feat(usr): UserListPage 修改按钮 + 编辑抽屉 REQ-USR-002(覆盖 Task 5)