---
req_id: REQ-USR-002
date: 2026-04-30
spec_ref: docs/superpowers/specs/2026-04-30-REQ-USR-002.md
---

# REQ-USR-002 用户修改 Implementation Plan

> **Execution:** Parent skill `feature-tdd` executes this plan task-by-task.

**Goal:** 在 USR-001 已建工程基础上增量实现 `PUT /api/usr/users/{id}`：更新可编辑字段 + 重建权限组（删旧 + 插新）；保留 `sPasswordHash` / `sCreatedBy` / 标准列。

**Architecture:** 复用 `UserService` / `UserServiceImpl` / `UserController` / 全部 mappers；新增 `UpdateUserDTO`、`UserService#update`、`UserPermissionMapper#deleteByUserId`。SecurityConfig 已对 `/api/usr/**` permitAll，无需改。

**Tech Stack:** 沿用（Spring Boot 3.3.5 / MyBatis-Plus / JJWT）。

---

## Schema 改动

无（仅 UPDATE / DELETE / INSERT）。

## 文件变更清单

### 新增

- `backend/src/main/java/com/xly/erp/module/usr/dto/UpdateUserDTO.java`

### 修改

- `backend/src/main/java/com/xly/erp/module/usr/mapper/UserPermissionMapper.java` — 追加 `deleteByUserId(Integer)`
- `backend/src/main/java/com/xly/erp/module/usr/service/UserService.java` — 追加 `Integer update(Integer id, UpdateUserDTO dto)`
- `backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java` — 实现 update（含 5 类校验 + UPDATE + DELETE + INSERT × N）
- `backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java` — 追加 PUT 端点
- `backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java` — 追加 10 单测
- `backend/src/test/java/com/xly/erp/module/usr/mapper/UserMapperIT.java` — 追加 1 用例（deleteByUserId）
- `backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java` — 追加 10 IT

## 任务步骤

### Task 1: UserPermissionMapper#deleteByUserId + IT

**Files:**
- Modify: `backend/src/main/java/com/xly/erp/module/usr/mapper/UserPermissionMapper.java`
- Modify: `backend/src/test/java/com/xly/erp/module/usr/mapper/UserMapperIT.java`

**API shape:**
- `@Delete("DELETE FROM tUserPermission WHERE iUserId = #{userId}")` `int deleteByUserId(@Param("userId") Integer userId)`

- [ ] **Step 1: 写失败测试 `userPermissionMapper#deleteByUserId_removesAllRowsForGivenUser`**
  - 准备：插 user1 + user2；user1 关联 cat1+cat2，user2 关联 cat1
  - 调 `deleteByUserId(user1.id)` → 返回 2，user1 行数 = 0；user2 行数 = 1（不受影响）
- [ ] **Step 2: 实现 mapper 方法**
- [ ] **Step 3: 子会话验证 PASS**：`mvn -B test -Dtest=UserMapperIT`
- [ ] **Step 4: Commit**：`feat(usr): mapper#deleteByUserId for permission rebuild REQ-USR-002`

### Task 2: UpdateUserDTO + UserService.update 主流程（合法 + 目标存在性 + bShowPerm null→false）

**Files:**
- Create: `backend/src/main/java/com/xly/erp/module/usr/dto/UpdateUserDTO.java`
- Modify: `backend/src/main/java/com/xly/erp/module/usr/service/UserService.java`
- Modify: `backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java`
- Modify: `backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java`

**API shape:**
- `UpdateUserDTO` 字段（与 CreateUserDTO 平行，去掉 `permissionCategoryIds` 之外字段语义不变；剔除 `sPasswordHash`，**仍包含** `permissionCategoryIds` 用于重建权限组）
- `UserService#update(Integer id, UpdateUserDTO dto) : Integer`
- 实现：
  1. `selectById(id)` → null 或 bDeleted=true → 40400
  2. 枚举校验 sUserType / sLanguage（同 create）→ 40001
  3. iStaffId 校验（同 create）→ 40022
  4. permissionCategoryIds 校验（仅当非空 list；null/空 list 跳过校验直接清空）→ 40023
  5. 构造 entity 仅 set iIncrement + 6 个可编辑字段（其余 null）；`bCanModifyDocs` null → false
  6. `userMapper.updateById(entity)` try/catch DuplicateKeyException → 40020
  7. `userPermissionMapper.deleteByUserId(id)`
  8. 若 permissionCategoryIds 非空：for-loop 插 UserPermission

- [ ] **Step 1: 追加 4 单测**
  - `updateWithValidDto_invokesUpdateById_andRebuildsPermissions`
  - `updateWithTargetNotFound_throws40400`
  - `updateWithTargetAlreadyDeleted_throws40400`
  - `updateWithBCanModifyDocsNull_setsFalseInEntity`
- [ ] **Step 2: 实现 DTO + service 主流程**
- [ ] **Step 3: 子会话验证 PASS**：`mvn -B test -Dtest=UserServiceImplTest`（期望 8+4=12）
- [ ] **Step 4: Commit**：`feat(usr): user update dto + service happy path REQ-USR-002`

### Task 3: Service 异常分支补全（枚举 / staff / permission / 唯一冲突 / 清空权限）

**Files:**
- Modify: `backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java`
- Modify: `backend/src/test/java/com/xly/erp/module/usr/service/UserServiceImplTest.java`

**API shape:** 不变

- [ ] **Step 1: 追加 6 单测**
  - `updateWithInvalidUserType_throws40001`
  - `updateWithInvalidLanguage_throws40001`
  - `updateWithStaffNotFound_throws40022`
  - `updateWithSomeInvalidPermissionIds_throws40023`
  - `updateWithDuplicateUserNo_throws40020`
  - `updateWithEmptyPermissionIds_clearsExisting` — permissionCategoryIds=null；deleteByUserId 调一次、insert 永不调
- [ ] **Step 2: 实现校验分支**
- [ ] **Step 3: 子会话验证 PASS**：`mvn -B test -Dtest=UserServiceImplTest`（期望 12+6=18）
- [ ] **Step 4: Commit**：`feat(usr): user update error branches REQ-USR-002`

### Task 4: Controller PUT + IT（10 用例）+ 全量回归

**Files:**
- Modify: `backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java`
- Modify: `backend/src/test/java/com/xly/erp/module/usr/controller/UserControllerIT.java`

**API shape:**
- `@PutMapping("/users/{id}") public Result<Map<String,Object>> update(@PathVariable Integer id, @Valid @RequestBody UpdateUserDTO dto)`
- 返回 `Result.ok(Map.of("iIncrement", userService.update(id, dto)))`

- [ ] **Step 1: 追加 10 IT**（参 spec 验收清单）
- [ ] **Step 2: 实现 controller PUT**
- [ ] **Step 3: 子会话跑全量回归**：`mvn -B test`（期望 89 + 1+10+10=20 = 109+ 用例全绿）
- [ ] **Step 4: Commit**：`test(usr): user update integration coverage REQ-USR-002`

## 提交计划

| commit | 覆盖 |
|---|---|
| `feat(usr): mapper#deleteByUserId for permission rebuild REQ-USR-002` | Task 1 |
| `feat(usr): user update dto + service happy path REQ-USR-002` | Task 2 |
| `feat(usr): user update error branches REQ-USR-002` | Task 3 |
| `test(usr): user update integration coverage REQ-USR-002` | Task 4 |
