Commit d7285a1cd82b17cb717430bd885dcdf50360168a

Authored by zichun
1 parent b5b16599

docs(spec:REQ-USR-003): 派生规格

docs/superpowers/specs/2026-06-01-REQ-USR-003.md 0 → 100644
  1 +# REQ-USR-003 查询用户 — 实现规格(后端)
  2 +
  3 +> 阶段:后端(backend)。作用域限定 controller / service / repository / DTO / 校验 / SQL migration / REST 契约。
  4 +> SSoT 引用:需求卡片 `docs/01-需求清单/USR-用户管理/REQ-USR-003.md`;DB 设计 `docs/03-数据库设计文档.md`;API 契约 `docs/05-API接口契约.md`;技术规范 `docs/04-技术规范.md`。
  5 +> 本规格只消费已锁定事实,忽略 UI 描述(控件类型/下拉数据源/列表布局/序号渲染),但查询条件、匹配规则、分页规则全部下沉到后端 DTO + Service。
  6 +
  7 +---
  8 +
  9 +## 1. Goal(目标)
  10 +
  11 +后台用户可按单一条件(用户名 / 员工名 / 用户号 / 部门 / 用户类型 / 作废 / 登录日期 / 制单人)配合匹配方式(包含 / 不包含 / 等于)筛选并分页浏览用户列表。查询为**只读**,不产生任何写副作用;密码与敏感字段绝不返回。对外仅提供一个端点:`GET /api/usr/users`。
  12 +
  13 +输出列含跨表字段「员工名」「部门」,来源 `usr_employee`,按 `usr_user.iEmployeeId` 左关联取得(用户未关联职员时该两列为 null)。
  14 +
  15 +---
  16 +
  17 +## 2. 输入 / 输出
  18 +
  19 +### 2.1 输入(请求)
  20 +
  21 +- **Method / Path**:`GET /api/usr/users`
  22 +- **Auth**:需要 Bearer JWT(任意已认证用户可调用,无管理员限制——查询为只读,docs/05 标注 Auth「需要(Bearer JWT)」未附加角色约束,见 § 8 D5)。无 / 失效 token → 401。
  23 +- **Query 参数 → `UserQueryDTO`**(全部可选,空条件返回全量分页):
  24 +
  25 +| DTO 字段 | 类型 | 必填 | 默认 | 校验 | 说明 |
  26 +|---|---|---|---|---|---|
  27 +| `queryField` | String | 否 | `用户名` | 取值 ∈ {`用户名`,`员工名`,`用户号`,`部门`,`用户类型`,`作废`,`登录日期`,`制单人`},越界 `40001` | 查询字段;为空时按默认 `用户名` 解释(仅当 `queryValue` 非空才生效) |
  28 +| `matchType` | String | 否 | `包含` | 取值 ∈ {`包含`,`不包含`,`等于`},越界 `40001` | 匹配方式;对枚举/外键/日期型字段仅 `等于` 有效(见 § 3 规则 5) |
  29 +| `queryValue` | String | 否 | — | `@Size(max=100)` | 查询值;为空 / null 表示不施加该条件(选择全部,见 § 3 规则 2) |
  30 +| `pageNum` | Integer | 否 | `1` | ≥ 1,否则 `42201` | 页码,从 1 起 |
  31 +| `pageSize` | Integer | 否 | `10` | 1 ≤ pageSize ≤ 100,否则 `42201` | 每页条数;上限 100(docs/04 § 3.2、docs/05 REQ-USR-003) |
  32 +
  33 +> 不接受 / 忽略的入参:任何写类字段、密码、租户列覆盖等本接口均不接收(GET 只读)。多条件组合不在本 REQ 范围(卡片输入表为单 `queryField` + 单 `matchType` + 单 `queryValue`,见 § 8 D2)。
  34 +
  35 +### 2.2 输出(响应)
  36 +
  37 +- 成功:`Result<PageResult<UserVO>>`,`code=0`。
  38 +- `PageResult<T>`(docs/04 § 1.4 / § 3.2):`{ records: UserVO[], total: number, pageNum: number, pageSize: number }`。
  39 +- `UserVO`(docs/05 REQ-USR-003 契约 + 卡片输出表 1,**不含 `sPassword` 及任何敏感字段**):
  40 +
  41 +| VO 字段 | 类型 | 来源 | 卡片输出列 | 说明 |
  42 +|---|---|---|---|---|
  43 +| `id` | Integer | `usr_user.iIncrement` | 序号映射主键 | 主键 ID;「序号」列由前端按行号渲染,后端只回 `id`(见 § 8 D4) |
  44 +| `sUserName` | String | `usr_user.sUserName` | 用户名 | 登录账号 |
  45 +| `employeeName` | String | `usr_employee.sEmployeeName` | 员工名 | 左关联,未关联职员为 null |
  46 +| `sUserNo` | String | `usr_user.sUserNo` | 用户号 | |
  47 +| `department` | String | `usr_employee.sDepartment` | 部门 | 左关联,未关联职员为 null |
  48 +| `sUserType` | String | `usr_user.sUserType` | 用户类型 | |
  49 +| `sLanguage` | String | `usr_user.sLanguage` | 语言 | |
  50 +| `iIsVoid` | Integer | `usr_user.iIsVoid` | 作废 | 0 正常 / 1 已作废(布尔语义,回传 0/1) |
  51 +| `tLastLoginDate` | DateTime | `usr_user.tLastLoginDate` | 登录日期 | 可为 null(从未登录) |
  52 +| `sCreator` | String | `usr_user.sCreator` | 制单人 | |
  53 +| `tCreateDate` | DateTime | `usr_user.tCreateDate` | 制单日期 | |
  54 +
  55 +- 失败:统一 `Result`(见 § 6 错误码),`message` 给可读中文提示,不抛栈。
  56 +- 响应**绝不返回** `sPassword` 或任何敏感字段(VO 不含密码列;不得 `SELECT *` 透传实体)。
  57 +
  58 +---
  59 +
  60 +## 3. 业务规则
  61 +
  62 +1. **只读无副作用**:本接口仅 `SELECT`,不写任何表、不更新 `tLastLoginDate`、不落审计;方法可标 `@Transactional(readOnly = true)`(可选)。
  63 +2. **空条件返回全量分页**:`queryValue` 为空 / null(trim 后为空串亦视为空)时不施加业务过滤条件,仅按分页返回全部用户;此时 `queryField` / `matchType` 不生效。
  64 +3. **单条件查询**:仅按 `queryField` 指定的单字段 + `matchType` 施加一个过滤条件(卡片输入表为单字段单匹配,非多条件 AND/OR 组合,见 § 8 D2)。
  65 +4. **字段 → 列映射**(`queryField` 中文 → 实际查询列):
  66 + - `用户名` → `usr_user.sUserName`(文本)
  67 + - `员工名` → `usr_employee.sEmployeeName`(文本,跨表)
  68 + - `用户号` → `usr_user.sUserNo`(文本)
  69 + - `部门` → `usr_employee.sDepartment`(文本,跨表)
  70 + - `用户类型` → `usr_user.sUserType`(枚举:{普通用户,超级管理员})
  71 + - `作废` → `usr_user.iIsVoid`(布尔 0/1)
  72 + - `登录日期` → `usr_user.tLastLoginDate`(日期时间)
  73 + - `制单人` → `usr_user.sCreator`(文本)
  74 +5. **匹配方式语义**(按 `matchType` 与字段类型组合,docs/04 § 3.2「文本条件模糊匹配,枚举/外键条件精确匹配」):
  75 + - **文本字段**(用户名 / 员工名 / 用户号 / 部门 / 制单人):`包含` → `LIKE '%v%'`;`不包含` → `NOT LIKE '%v%'`;`等于` → `= v`(精确)。`queryValue` 中的 LIKE 通配符 `%` `_` 须转义(见 § 8 D3)。
  76 + - **枚举字段**(用户类型):仅 `等于`(`= v`)有业务意义;`包含` 按 `LIKE '%v%'`、`不包含` 按 `NOT LIKE '%v%'` 也允许执行(容错,不报错),但推荐 `等于`。
  77 + - **布尔字段**(作废):`queryValue` 须可解析为 0/1(接受 `0`/`1`,亦可接受 `是`/`否`、`true`/`false` 归一化为 1/0,见 § 8 D6);按 `等于` 处理(`= 0/1`);`包含`/`不包含` 退化为 `=`/`<>`。无法解析为 0/1 → `40001`。
  78 + - **日期字段**(登录日期):`queryValue` 须为合法日期(`yyyy-MM-dd`)或日期时间(`yyyy-MM-dd HH:mm:ss`);`等于` → 命中该「整天」区间 `[day 00:00:00, day+1 00:00:00)`(仅给到日期时按当日匹配,见 § 8 D6);`包含`/`不包含` 在日期字段无模糊语义,按 `等于` 当日区间 / 取反处理。无法解析 → `40001`。
  79 +6. **跨表关联**:员工名 / 部门来自 `usr_employee`,按 `usr_user.iEmployeeId = usr_employee.iIncrement` **LEFT JOIN**(未关联职员的用户仍出现在结果,员工名 / 部门为 null)。当 `queryField` ∈ {员工名, 部门} 时,过滤施加在 join 后的 `usr_employee` 列上(未关联职员的行因该列为 null,`包含`/`等于` 自然不命中、`不包含` 的 null 处理见 § 8 D7)。
  80 +7. **分页规则**:
  81 + - `pageNum` < 1 或 `pageSize` < 1 或 `pageSize` > 100 → `42201`(参数非法,先于查询判定)。
  82 + - `pageNum` 超过实际总页数(数据层「越界」,如请求第 99 页但仅 3 页)→ **返回最后一页**数据(卡片验收「分页参数越界时返回最后一页」),`PageResult.pageNum` 回传被钳制后的实际页号;`total` 仍为真实总数(见 § 8 D1)。total 为 0 时返回空 `records`、`pageNum` 回传 1。
  83 + - 默认 `pageNum=1`、`pageSize=10`(docs/05 契约默认)。
  84 +8. **空结果**:无匹配记录时返回 `code=0` + `records=[]` + `total=0`,**不报错**(卡片验收「无匹配时返回空列表而非报错」)。
  85 +9. **密码与敏感字段不返回**:查询 SQL 不 SELECT `sPassword`;VO 不含密码;多租户列 `sBrandsId`/`sSubsidiaryId`/`sId` 不在输出列(输出列严格按 § 2.2 UserVO)。
  86 +
  87 +---
  88 +
  89 +## 4. 约束(技术 / 安全)
  90 +
  91 +- **分层**(docs/04 § 1.2):`UsrUserController`(仅 `@Valid` 校验 + 委派)→ `UsrUserService` / `UsrUserServiceImpl`(业务 + 分页装配)→ `UsrUserMapper`(MyBatis-Plus)。Controller 禁止直接操作 Mapper。本 REQ 在已存在的 `UsrUserService` 上新增 `queryUsers(UserQueryDTO dto)` 方法,复用 REQ-USR-001/002 已建的 controller / service / mapper / entity,不另起类。
  92 +- **包路径**:`com.xly.erp.modules.usr.{controller,service,service.impl,mapper,entity,dto,vo}`;新增查询入参 DTO `UserQueryDTO` 置于 `modules/usr/dto`,输出 `UserVO` 置于 `modules/usr/vo`。本 REQ 仅触及 `modules/usr/**`,不跨模块。
  93 +- **命名**(docs/04 § 1.3):方法 `queryUsers`;REST 路径 `GET /api/usr/users`;DTO `UserQueryDTO`、VO `UserVO`。
  94 +- **统一响应**(docs/04 § 1.4):返回 `Result<PageResult<UserVO>>`,`code=0` 成功;错误码集中在 `ResultCode` 枚举(含 `42201`、`40001`)。
  95 +- **异常处理**(docs/04 § 1.5):业务/参数错误抛 `BusinessException(ResultCode,msg)`,由 `GlobalExceptionHandler` 转 `Result`;`@Valid` 基础校验失败由全局处理器转 `40001`;分页越界(`pageNum<1`/`pageSize` 越界)转 `42201`(在 Controller/Service 入口判定或 `@Min/@Max` 校验 + 处理器映射,见 § 8 D8)。
  96 +- **分页实现**(docs/04 § 3.2、§ 3.4):用 MyBatis-Plus 分页插件(`IPage`/`Page`)或在 Mapper XML 内 `LIMIT/OFFSET` + `COUNT(*)` 装配 `PageResult`;跨表 LEFT JOIN(usr_user ⋈ usr_employee)+ 动态条件建议用 Mapper XML(docs/04 § 3.4「复杂 SQL 写 XML」)。禁止 Controller 直接操作 Mapper。
  97 +- **数据访问**(docs/04 § 3.4):只走 Mapper;文本条件模糊匹配、枚举/布尔/日期条件精确匹配(docs/04 § 3.2)。SQL 参数化(`#{}` 预编译占位)防注入;LIKE 通配符转义(§ 8 D3)。
  98 +- **安全**:受保护接口经 `JwtAuthenticationFilter`;查询结果不含密码 / 不进日志敏感字段。
  99 +- **配置**:JWT 密钥、DB 凭据只从 `config-vars.yaml` / `application.yml` 读取,不硬编码。
  100 +- **schema**:本 REQ 仅**读** `usr_user` / `usr_employee`,二表已由 `sql/migrations/V1__initial_schema.sql` 建好,结构与 docs/03 一致,**无需新增 migration**(见 § 8 D9)。
  101 +
  102 +---
  103 +
  104 +## 5. Schema 引用(docs/03 SSoT)
  105 +
  106 +- **读**:`usr_user`(主键 `iIncrement`;输出列 `sUserName`/`sUserNo`/`sUserType`/`sLanguage`/`iIsVoid`/`tLastLoginDate`/`sCreator`/`tCreateDate`;过滤列另含 `iEmployeeId` 用于 join;**不 SELECT** `sPassword`)。相关索引:`idx_usr_user_employee`(iEmployeeId)、`idx_usr_user_type`(sUserType)、`uk_usr_user_username`(sUserName) 可被查询命中。
  107 +- **读**:`usr_employee`(按 `usr_user.iEmployeeId = usr_employee.iIncrement` LEFT JOIN;取 `sEmployeeName`→员工名、`sDepartment`→部门;过滤员工名 / 部门时命中其列)。索引 `idx_usr_employee_name`(sEmployeeName) 可被命中。
  108 +- 关系:`usr_user N:1 usr_employee`(外键 `fk_usr_user_employee`,ON DELETE SET NULL)。
  109 +- 实体:`UsrUser` ↔ `usr_user`,`UsrEmployee` ↔ `usr_employee`(匈牙利前缀列名,实体字段与列名映射保持一致);查询结果装配为 `UserVO`(非实体直出)。
  110 +
  111 +---
  112 +
  113 +## 6. API 引用 / 错误码(docs/05 SSoT)
  114 +
  115 +- 端点契约见 `docs/05-API接口契约.md` § REQ-USR-003(`GET /api/usr/users`)。
  116 +- 错误码:
  117 + - `0` — 成功,返回 `PageResult<UserVO>`。
  118 + - `42201` — 分页参数非法(`pageNum < 1` 或 `pageSize < 1` 或 `pageSize > 100`)。
  119 + - `40001` — 查询参数校验失败(`queryField` / `matchType` 枚举越界 / `queryValue` 超长 / 布尔字段值无法解析为 0/1 / 日期字段值非法)。
  120 + - 401 — 未认证(无 / 失效 token,由安全过滤器返回,非业务错误码)。
  121 +
  122 +---
  123 +
  124 +## 7. 验收标准(Acceptance Criteria)
  125 +
  126 +1. **空条件全量分页**:不传任何 `queryValue`(或 `queryValue` 空)调用 `GET /api/usr/users?pageNum=1&pageSize=10` → `code=0`,`records` 为前 10 条用户,`total` = 用户总数,`pageNum=1`、`pageSize=10`;每条含 § 2.2 全部 VO 列,**不含 `sPassword`**。
  127 +2. **文本「包含」筛选**:`queryField=用户名&matchType=包含&queryValue=adm` → 仅返回 `sUserName` 含 `adm` 的用户(LIKE `%adm%`),结果集正确。
  128 +3. **文本「等于」精确**:`queryField=用户名&matchType=等于&queryValue=admin` → 仅返回 `sUserName` 严格等于 `admin` 的用户。
  129 +4. **文本「不包含」**:`queryField=制单人&matchType=不包含&queryValue=admin` → 返回 `sCreator` 不含 `admin` 的用户。
  130 +5. **枚举「等于」**:`queryField=用户类型&matchType=等于&queryValue=超级管理员` → 仅返回 `sUserType=超级管理员` 的用户。
  131 +6. **布尔(作废)筛选**:`queryField=作废&queryValue=1` → 仅返回 `iIsVoid=1` 的用户;`queryValue=0` → 仅 `iIsVoid=0`;不可解析为 0/1(如 `abc`)→ `code=40001`。
  132 +7. **日期(登录日期)筛选**:`queryField=登录日期&matchType=等于&queryValue=2026-06-01` → 返回 `tLastLoginDate` 落在 2026-06-01 当日的用户;非法日期值 → `code=40001`。
  133 +8. **跨表员工名 / 部门**:返回每条 VO 的 `employeeName` / `department` 来自该用户关联职员;未关联职员(`iEmployeeId` 为 null)的用户仍在结果中,两列为 null。`queryField=部门&matchType=包含&queryValue=财务` 仅返回所属部门含「财务」的用户。
  134 +9. **无匹配返回空**:筛选条件无命中 → `code=0`,`records=[]`,`total=0`(不报错)。
  135 +10. **分页越界返回最后一页**:总数据仅 3 页(每页 10),请求 `pageNum=99` → `code=0`,`records` 为最后一页数据,`PageResult.pageNum` 回传实际最后页号,`total` 为真实总数。
  136 +11. **分页参数非法**:`pageNum=0` 或 `pageSize=0` 或 `pageSize=500` → `code=42201`。
  137 +12. **未认证**:无 / 失效 token 调用 → 401,不返回任何用户数据。
  138 +13. **密码绝不泄露**:任意查询的响应体均不含 `sPassword` 字段、不含明文密码。
  139 +
  140 +---
  141 +
  142 +## 8. 自主决策记录(decisions)
  143 +
  144 +| # | 问题 | 选择 | 依据 | 置信度 |
  145 +|---|---|---|---|---|
  146 +| D1 | 「分页越界」与 docs/05 `42201` 的边界冲突:卡片验收「越界返回最后一页」vs 契约 `pageNum<1` 报 `42201` | 区分两类:**参数非法**(`pageNum<1` / `pageSize<1` / `pageSize>100`)→ `42201`;**数据越界**(`pageNum` 超总页数但本身合法 ≥1)→ 钳制到最后一页返回 `code=0`。`pageNum` 回传钳制后实际页号 | 卡片验收「越界返回最后一页」明确针对「请求页超出数据范围」;docs/05 `42201` 文义为「pageNum<1 或 pageSize 超上限」即非法入参——二者作用域不同,可并存不矛盾 | high |
  147 +| D2 | 查询是单条件还是多条件组合 | **单条件**:单 `queryField` + 单 `matchType` + 单 `queryValue` | 卡片输入表 1 为三行单值(查询字段/匹配方式/查询值),无多字段并列;docs/05 query 参数亦为单组,无条件数组 | high |
  148 +| D3 | LIKE 模糊查询中 `queryValue` 含 `%`/`_`/`\` 等通配符的处理 | 对 `%` `_` `\` 做转义(`ESCAPE '\\'`),按字面匹配,防止用户输入被当通配符 / 注入 | 安全与正确性默认实践;docs/04 § 3.2 文本模糊匹配未禁止转义,转义更稳妥 | high |
  149 +| D4 | 卡片输出列「序号」如何承载 | 后端 VO 回 `id`(主键),「序号」由前端按行号渲染;后端不生成连续行号 | 卡片标「序号—系统生成」属列表展示行号(前端职责);docs/05 `UserVO` 含 `id` 不含「序号」;后端阶段不做 UI 行号 | high |
  150 +| D5 | 是否限制仅管理员可查询 | **不限制**,任意已认证用户可调用 | docs/05 REQ-USR-003 Auth 仅标「需要(Bearer JWT)」,未附加「仅管理员」(对比 REQ-USR-001/002 明确「仅管理员/超级管理员」);只读查询无敏感写操作 | high |
  151 +| D6 | 布尔(作废)/ 日期(登录日期)字段的 `queryValue` 解析口径 | 作废:接受 `0`/`1`(兼容 `是`/`否`、`true`/`false` 归一化),否则 `40001`;登录日期:接受 `yyyy-MM-dd`(按当日区间)或 `yyyy-MM-dd HH:mm:ss`,否则 `40001` | 卡片「查询值=手工输入文本」需后端按目标列类型解析;docs/04 § 3.2「枚举/外键精确匹配」延伸到布尔/日期取精确区间;非法值显式 `40001` 比静默放行更可靠 | medium |
  152 +| D7 | 跨表字段(员工名/部门)「不包含」对 NULL(未关联职员)行的处理 | `不包含` 仅对该列**非 null** 的行做 `NOT LIKE`;未关联职员(列为 null)的行不纳入「不包含」结果(SQL `NOT LIKE` 对 NULL 返回 unknown 自然不命中) | 遵循标准 SQL 三值逻辑;卡片未对 null 行的「不包含」语义特别要求,取 SQL 默认行为,避免引入易错的 `OR col IS NULL` 隐式语义 | medium |
  153 +| D8 | `42201` 的实现层判定方式 | 在 Controller/Service 入口显式判定 `pageNum`/`pageSize` 范围并抛 `BusinessException(42201)`(或 `@Min/@Max` + 全局处理器映射到 `42201`),不依赖分页插件静默兜底 | docs/04 § 1.5 业务/参数错误统一走 `BusinessException` + 全局处理器;显式判定才能精确回 `42201` 而非 `40001` | medium |
  154 +| D9 | 是否新增 migration | 不新增,复用 `V1__initial_schema.sql` | 本 REQ 纯读 `usr_user` / `usr_employee`,二表已在 V1 建好,结构与 docs/03 一致,无 schema 变更 | high |
  155 +
  156 +> 注:docs/03 中 `sLanguage` / `usr_company.sVersion` / `usr_permission` 粒度的「需用户审阅」占位为 DB 文档遗留标记,不在本只读查询 REQ 作用域内消解;本规格沿用 USR 模块既有取值锁定(语言 ∈ {中文,英文,繁体}、用户类型 ∈ {普通用户,超级管理员}),不阻塞。
... ...