--- 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-tdd` executes 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 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.java` - `git 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 ``` 业务逻辑约束(按顺序执行): 1. `!"超级管理员".equals(principal.userType())` → `BizException(PERMISSION_DENIED, "权限不足")` 2. `userMapper.selectOne(sId=userId AND sBrandsId=brandId)` == null → `BizException(USER_NOT_FOUND, "用户不存在")` 3. `principal.userId().equals(userId) && !"超级管理员".equals(req.getUserType())` → `BizException(SELF_ADMIN_CHANGE, "禁止修改自己的管理员角色")` 4. `req.getEmployeeId() != null` → staffMapper 校验存在性,不存在 → `BizException(EMPLOYEE_NOT_FOUND, "员工不存在")` 5. 用 `LambdaUpdateWrapper` 更新 sUserType/sLanguage/bCanEditDoc/bIsDisabled/sEmployeeId(按 sId=userId AND sBrandsId=brandId) 6. `userPermissionMapper.delete(sUserId=userId AND sBrandsId=brandId)`,再批量 insert 新 UserPermissionEntity 列表(permGroupIds 为空时只删不插) 7. 返回 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.java` - `git 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 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.java` - `git 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):** ```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 // → request.put(`/usr/users/${userId}`, req) ``` **UserFormDrawer props 扩展:** ```ts 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 里追加: ```ts 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.tsx` - `git 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 末尾追加 "操作" 列,每行渲染 ` setEditingUser(row)}>修改` - 新增状态: `const [editingUser, setEditingUser] = useState(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.tsx` - `git 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)