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

# REQ-USR-003 用户查询 Implementation Plan

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

**Goal:** 实现 `GET /api/usr/users` 单条件分页查询：tUser LEFT JOIN tStaff，按 field × match × value 动态过滤。

**Architecture:** 新增 `UserListVO` + `UserListQuery` 内部封装类（field/match/value/page）+ `UserMapper#pageWithFilter` 自定义动态 SQL + `UserService#list` + controller `@GetMapping`。

**Tech Stack:** 沿用；MyBatis 动态 SQL `<script>` + `<foreach>` / `<if>`。

---

## Schema 改动

无。

## 文件变更清单

### 新增

- `backend/src/main/java/com/xly/erp/module/usr/vo/UserListVO.java`
- `backend/src/main/java/com/xly/erp/module/usr/dto/UserListQuery.java`（内部封装规范化后的查询参数）

### 修改

- `backend/src/main/java/com/xly/erp/module/usr/mapper/UserMapper.java` — 追加 `pageWithFilter` + `countWithFilter`
- `backend/src/main/resources/mapper/usr/UserMapper.xml` — 新建 XML（动态 SQL 复杂，注解 @Select 不易读）
- `backend/src/main/java/com/xly/erp/module/usr/service/UserService.java` — 追加 `list(...)`
- `backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java` — 实现 list（字段映射 + 校验 + 调 mapper）
- `backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java` — 追加 GET 端点
- 测试：UserMapperIT(+1) / UserServiceImplTest(+10) / UserControllerIT(+8)

## 任务步骤

### Task 1: UserListVO + UserMapper#pageWithFilter / countWithFilter + IT

**Files:**
- Create: vo/UserListVO.java
- Modify: mapper/UserMapper.java + resources/mapper/usr/UserMapper.xml
- Modify: test/.../mapper/UserMapperIT.java

**API shape:**
- `UserListVO` 11 字段（参 spec），@JsonProperty 锁定 JSON 名
- `UserMapper.pageWithFilter`（XML）参数：`@Param("field") String physicalCol, @Param("match") String matchOp, @Param("value") Object value, @Param("offset") int offset, @Param("size") int size`；`physicalCol` 由 service 映射成 `u.sUserName` / `s.sStaffName` 等
- `UserMapper.countWithFilter` 同参数（除 offset/size）

**XML 关键结构**：
```xml
<select id="pageWithFilter" resultType="com.xly.erp.module.usr.vo.UserListVO">
  SELECT u.iIncrement, u.sUserName, s.sStaffName AS staffName, u.sUserNo,
         s.sDepartment AS department, u.sUserType, u.sLanguage,
         u.bDeleted, u.tLastLoginDate, u.sCreatedBy, u.tCreateDate
  FROM tUser u
  LEFT JOIN tStaff s ON s.iIncrement = u.iStaffId AND s.bDeleted = 0
  <where>
    <if test="value != null and value != ''">
      <choose>
        <when test="matchOp == 'contains'">${field} LIKE CONCAT('%', #{value}, '%')</when>
        <when test="matchOp == 'notContains'">${field} NOT LIKE CONCAT('%', #{value}, '%')</when>
        <when test="matchOp == 'equals'">
          <choose>
            <when test="field == 'DATE(u.tLastLoginDate)'">${field} = #{value}</when>
            <otherwise>${field} = #{value}</otherwise>
          </choose>
        </when>
      </choose>
    </if>
  </where>
  ORDER BY u.iIncrement DESC
  LIMIT #{offset}, #{size}
</select>
```
（XML 复杂度由 service 提供归一化的 `field`(物理列名 with prefix) / `matchOp`(英文常量) / `value` 后大幅简化）

- [ ] **Step 1: 写失败 IT `pageWithFilter_filtersAndJoins`**
- [ ] **Step 2: 实现 entity/VO + mapper + XML**
- [ ] **Step 3: 子会话验证 PASS**
- [ ] **Step 4: Commit**：`feat(usr): user list mapper + vo with join REQ-USR-003`

### Task 2: UserListQuery + UserService.list 主流程（合法 + 字段映射）

**Files:**
- Create: dto/UserListQuery.java（service 内部）
- Modify: service/UserService.java + impl/UserServiceImpl.java
- Modify: test/.../service/UserServiceImplTest.java

**API shape:**
- `UserService#list(String field, String match, String value, Integer pageNum, Integer pageSize) : Map<String, Object>`（返回 records/total/pageNum/pageSize）
- 内部：归一化（默认值/trim）→ field/match 校验 → value 解析（日期/布尔）→ 调 mapper.countWithFilter + pageWithFilter → 装结果 Map

**字段映射表**（service 静态常量）：
```
"用户名"   -> ("u.sUserName", STRING)
"员工名"   -> ("s.sStaffName", STRING)
"用户号"   -> ("u.sUserNo", STRING)
"部门"     -> ("s.sDepartment", STRING)
"用户类型" -> ("u.sUserType", STRING)
"作废"     -> ("u.bDeleted", BOOLEAN)
"登录日期" -> ("DATE(u.tLastLoginDate)", DATE)
"制单人"   -> ("u.sCreatedBy", STRING)
```
match 映射：包含→`contains`，不包含→`notContains`，等于→`equals`

**校验顺序**：pageSize 上限 → field 枚举 → match 枚举 → field/match 兼容（布尔/日期仅 equals）→ value 解析（日期）

- [ ] **Step 1: 写失败测试 4 条主流程用例**
  - listWithDefaults_invokesMapperWithUserNameContainsEmpty
  - listWithEmptyValue_skipsFilterCondition
  - listWithKeywordTrim
  - listReturnsEmptyRecords_whenMapperReturnsEmptyPage
- [ ] **Step 2: 实现 service**
- [ ] **Step 3: 子会话验证 PASS**
- [ ] **Step 4: Commit**：`feat(usr): user list service + field/match mapping REQ-USR-003`

### Task 3: Service 异常分支（field/match/兼容/pageSize/日期格式/布尔）

- [ ] **Step 1: 追加 6 用例**
  - listWithInvalidField_throws40001
  - listWithInvalidMatch_throws40001
  - listWithIncompatibleFieldMatch_throws40001
  - listWithPageSizeExceeds100_throws40002
  - listWithInvalidLoginDateFormat_throws40001
  - listWithBooleanFieldEqualsTrue_passesIntegerOne
- [ ] **Step 2: 实现校验分支**
- [ ] **Step 3: 子会话验证 PASS**（期望 18 + 4 + 6 = 28 用例）
- [ ] **Step 4: Commit**：`feat(usr): user list error branches REQ-USR-003`

### Task 4: Controller GET + 8 IT + 全量回归

- [ ] **Step 1: 追加 8 IT**（参 spec 验收清单）
- [ ] **Step 2: 实现 controller GET**
- [ ] **Step 3: 子会话跑全量回归**（期望 ≥ 129 用例）
- [ ] **Step 4: Commit**：`test(usr): user list integration coverage REQ-USR-003`

## 提交计划

| commit | 覆盖 |
|---|---|
| `feat(usr): user list mapper + vo with join REQ-USR-003` | Task 1 |
| `feat(usr): user list service + field/match mapping REQ-USR-003` | Task 2 |
| `feat(usr): user list error branches REQ-USR-003` | Task 3 |
| `test(usr): user list integration coverage REQ-USR-003` | Task 4 |
