2026-05-08-REQ-USR-002.md 13.8 KB

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<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 == 40400UsrErrorCode.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<UsrUserEntity> 更新 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<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.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):

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.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 末尾追加 "操作" 列,每行渲染 <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.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)