txw/docs/superpowers/specs/2026-07-27-user-list-by-qy-design.md

232 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 按企业查询用户接口设计
- 日期2026-07-27
- 模块:`txw-mhzc`(门户主站)
- 状态:待用户审阅
## 1. 背景与目标
### 1.1 需求
新增一个按企业名称(及登录账号)查询用户的接口:
- 入参:单个 `key` 关键字,对 `dlzh`(登录账号)或 `qymc`(企业名称)做 **OR 模糊匹配**`key` 为空时返回空结果
- 出参:符合条件的 `dlzh` + `qymc` 列表,分页返回
- 一个用户可关联多个企业,匹配结果**每个企业一条记录**dlzh 可重复)
### 1.2 设计目标
- 与现有代码风格一致XML 处理复杂 SQL、default 方法处理简单 wrapper
- 不污染现有 `getAllUser` 接口,独立新增
- 单条 SQL 完成三表 JOIN分页由 MyBatis-Plus 插件自动包装
## 2. 数据模型
| 表 | 关键字段 |
|---|---|
| `txw_mhzc_yhxxb`(用户表) | `yh_uuid`、`dlzh`(登录账号)、`yxbz`(有效标志 Y/N、`sdbz`(锁定标志 Y/N |
| `txw_mhzc_qyxxb`(企业表) | `qyuuid`、`qymc`(企业名称) |
| `txw_mhzc_yhqygxb`(用户-企业关系表) | `yh_uuid``qyuuid`**一对多** |
用户过滤硬性条件:`yxbz = 'Y' AND sdbz = 'N'`(与 `getYhxxByDlzh` 等现有查询保持一致)。
## 3. 接口契约
### 3.1 请求
```
POST /user/listUserByQy
Content-Type: application/json
```
请求体 `UserQyReqVO`
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `key` | String | 是 | 模糊匹配关键字,对 `dlzh``qymc` 任一命中即返回 |
| `pageNo` | Integer | 是 | 页码(继承 `PageParam` |
| `pageSize` | Integer | 是 | 每页大小(继承 `PageParam` |
`key` 为空字符串或 null → 返回空结果;`key` 非空 → 对 `dlzh``qymc` 做 OR 模糊匹配。
### 3.2 响应
```json
{
"code": 0,
"msg": "ok",
"data": {
"total": 42,
"current": 1,
"size": 10,
"pages": 5,
"records": [
{ "dlzh": "zhangsan@qy", "qymc": "某某科技有限公司" },
{ "dlzh": "zhangsan@qy", "qymc": "某某集团" }
]
}
}
```
`UserQyVO` 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `dlzh` | String | 登录账号 |
| `qymc` | String | 企业名称 |
## 4. 组件设计
### 4.1 新增文件
| 文件 | 作用 |
|---|---|
| `pojo/vo/UserQyReqVO.java` | 入参 VO`dlzh`、`qymc`,继承 `PageParam` |
| `pojo/vo/UserQyVO.java` | 出参 VO`dlzh` + `qymc` |
| `mapper/TxwMhzcYhxxbMapper.java` | 新增 `listUserByQy` 方法 |
| `mapper/TxwMhzcYhxxbMapper.xml` | 新增对应 `<select>` |
| `service/TxwMhzcYhxxbService.java` | 新增接口方法 |
| `service/impl/TxwMhzcYhxxbServiceImpl.java` | 新增实现 |
| `controller/UserController.java` | 新增 controller 方法 |
### 4.2 调用链
```
Controller#listUserByQy
→ Service#listUserByQy
→ Mapper#listUserByQyXML <select>
→ DB: 三表 JOIN + LIKE + 分页
→ Page<UserQyVO>
```
## 5. SQL 设计
```xml
<select id="listUserByQy"
resultType="com.css.txw.mhzc.pojo.vo.UserQyVO">
SELECT y.dlzh AS dlzh, q.qymc AS qymc
FROM txw_mhzc_yhxxb y
JOIN txw_mhzc_yhqygxb g ON g.yh_uuid = y.yh_uuid
JOIN txw_mhzc_qyxxb q ON q.qyuuid = g.qyuuid
WHERE y.yxbz = 'Y' AND y.sdbz = 'N'
<choose>
<when test="reqVO.key != null and reqVO.key != ''">
AND (y.dlzh LIKE CONCAT('%', #{reqVO.key}, '%')
OR q.qymc LIKE CONCAT('%', #{reqVO.key}, '%'))
</when>
<otherwise>
AND 1 = 0
</otherwise>
</choose>
ORDER BY y.lrrq DESC
</select>
```
- `LIKE` 参数通过 MyBatis `#{}` 占位符绑定,`%` 由 `CONCAT` 在 SQL 内拼接,不存在 SQL 注入风险
- `<choose>` 分支保证 `key` 为空时 SQL 必定返回空(`AND 1=0` 短路),不依赖调用方
- 分页由 MyBatis-Plus 的 `PaginationInnerInterceptor` 自动包装(项目已有配置)
## 6. 代码骨架
### 6.1 `UserQyReqVO.java`
```java
package com.css.txw.mhzc.pojo.vo;
import com.css.ggzc.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "按企业查询用户请求")
@Data
public class UserQyReqVO extends PageParam {
@Schema(description = "关键字,对登录账号或企业名称做模糊匹配(必填,为空时返回空结果)")
private String key;
}
```
### 6.2 `UserQyVO.java`
```java
package com.css.txw.mhzc.pojo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "用户-企业匹配结果")
@Data
public class UserQyVO {
@Schema(description = "登录账号")
private String dlzh;
@Schema(description = "企业名称")
private String qymc;
}
```
### 6.3 `TxwMhzcYhxxbMapper.java` 新增方法
```java
Page<UserQyVO> listUserByQy(IPage<UserQyVO> page, @Param("reqVO") UserQyReqVO reqVO);
```
> **备注**:项目内 MyBatis-Plus 习惯 `default` 方法直接用 `QueryWrapper`,复杂 SQL 才走 XML。本接口需三表 JOIN + LIKE + 分页,**直接走 XML**。Mapper 接口只声明方法,由 `TxwMhzcYhxxbMapper.xml` 中 `<select id="listUserByQy">` 提供实现;由于 MP 的 `PaginationInnerInterceptor` 仅在 mapper 方法签名出现 `IPage` 参数时才会自动注入 `SELECT COUNT(*)` 与 `LIMIT`,必须把 `Page` 显式作为参数传入。
### 6.4 `TxwMhzcYhxxbService.java` 新增方法
```java
Page<UserQyVO> listUserByQy(UserQyReqVO reqVO);
```
### 6.5 `TxwMhzcYhxxbServiceImpl.java` 新增实现
```java
@Override
public Page<UserQyVO> listUserByQy(UserQyReqVO reqVO) {
Page<UserQyVO> page = new Page<>(reqVO.getPageNo(), reqVO.getPageSize());
return yhxxbMapper.listUserByQy(page, reqVO);
}
```
### 6.6 `UserController.java` 新增方法
```java
@PostMapping("/listUserByQy")
@Operation(summary = "按企业查询用户",
description = "按登录账号与企业名称模糊查询用户-企业匹配项")
public CommonResult<Page<UserQyVO>> listUserByQy(@RequestBody UserQyReqVO reqVO) {
return CommonResult.success(yhxxbService.listUserByQy(reqVO));
}
```
## 7. 错误处理
| 场景 | 行为 |
|---|---|
| `key` 为空字符串或 null | 返回空结果(`Page{records=[], total=0}`HTTP 200由 SQL `<otherwise> AND 1=0 </otherwise>` 短路保证) |
| `key` 非空但无匹配 | `Page{records=[], total=0}`HTTP 200不抛业务异常 |
| SQL 异常 | 由全局 `@RestControllerAdvice` 兜底返回 `CommonResult.error` |
| 输入含 SQL 通配符(`%`、`_` | `#{}` 通过预编译参数阻断 SQL 注入(输入中的 `'` 无法闭合 SQL 字符串),因此整体上是**安全的**。但需注意:`#{}` 不会转义 MySQL LIKE 模式元字符(`%`、`_`),输入若含这些字符仍会按 LIKE 通配符匹配。例如输入 `100%`,末位 `%` 会被 MySQL 识别为通配符,等价于 `LIKE '%100<任意内容>%'`。对本接口使用场景(按名称/账号搜索)影响极小,但若将来需要严格字面量匹配,可在 SQL 内对参数做 `REPLACE(..., '%', '\\%')` + 显式 `ESCAPE '\\'`,或调用方自行过滤通配符。 |
## 8. 测试
与现有项目风格一致,以手动验证为主:
1. **`key` 为空**`{key:"",pageNo:1,pageSize:10}` → 返回空结果(`records=[]`、`total=0`
2. **`key` 命中 dlzh**:仅 `dlzh` 含子串的项返回
3. **`key` 命中 qymc**:仅 `qymc` 含子串的项返回
4. **`key` 同时命中 dlzh 与 qymc**OR 关系,命中任一即返回
5. **多企业用户**:同一 dlzh 出现多条,每条对应一个 qymc
6. **过滤被锁定用户**`sdbz='Y'` 的用户不出现在结果中
7. **过滤无效用户**`yxbz='N'` 的用户不出现在结果中
8. **特殊字符**:输入 `'%' OR '1'='1`(或类似注入尝试串)应被 `#{}` 预编译中性化,返回 HTTP 200 不报 500其中注入部分`' OR '1'='1`)因 `#{}` 不允许 `'` 闭合字符串而被作为字面量处理。需要特别注意的是:`#{}` 不会转义 LIKE 元字符,因此输入中若含 `%``_` 仍会按 MySQL 通配符匹配——例如输入 `100%` 会被解读为 `LIKE '%100<任意内容>%'`。对本按名称/账号搜索的使用场景无实际风险,但若未来需严格字面量匹配,应在 SQL 内通过 `REPLACE(...)` + 显式 `ESCAPE '\\'` 转义。
9. **分页边界**`pageSize=10`、`pageNo` 超过总页数 → 返回空 recordstotal 仍正确
## 9. 不在范围内
- 不修改现有 `getAllUser` 接口及 `UserVO`
- 不新增权限校验(与 `getAllUser` 一致,无显式权限控制,依赖网关层)
- 不缓存查询结果(用户与企业关联可能变化,首次上线不强加缓存策略)
- 不支持按其他字段(手机号、身份证号、纳税人识别号)筛选