feat: 查询接口改为key

This commit is contained in:
liulujian 2026-07-29 15:51:20 +08:00
parent 3007b73d77
commit 8552deddda
7 changed files with 165 additions and 115 deletions

Binary file not shown.

View File

@ -0,0 +1,94 @@
# `POST /mhzc/user/listUserByQy` 接口说明
按企业名称与登录账号模糊查询用户-企业匹配项。
## 基本信息
| 项 | 说明 |
| ---------------------- | ----------------------------------- |
| **请求方法** | `POST` |
| **路径** | `/mhzc/user/listUserByQy` |
| **Content-Type** | `application/json; charset=UTF-8` |
| **鉴权** | 需要携带登录信息 |
## 入参 `UserQyReqVO`
| 字段 | 类型 | 必填 | 说明 |
| ------------ | ------- | ---- | ------------------------------------------ |
| `key` | String | 是 | 模糊匹配关键字,对登录账号或企业名称命中任一即返回;为空时返回空结果 |
| `pageNo` | Integer | 是 | 页码(从 1 开始) |
| `pageSize` | Integer | 是 | 每页大小建议传10 |
**示例请求体:**
```json
{
"key": "上海链坤",
"pageNo": 1,
"pageSize": 10
}
```
**入参规则:**
- `key` 为空字符串或 null → 返回空结果(`records=[]``total=0`
- `key` 非空 → 对 `dlzh`(登录账号)和 `qymc`(企业名称)做 `OR` 模糊匹配,命中任一即返回
## 返回结果
外层结构 `CommonResult`
| 字段 | 类型 | 说明 |
| -------- | ---------------- | ---------------------- |
| `code` | Integer | 业务码,`0` 表示成功 |
| `msg` | String | 提示信息 |
| `data` | Page\<UserQyVO\> | 分页结果 |
分页 `Page<UserQyVO>`
| 字段 | 类型 | 说明 |
| ----------- | ---------------- | -------- |
| `total` | Long | 总记录数 |
| `current` | Long | 当前页码 |
| `size` | Long | 每页大小 |
| `pages` | Long | 总页数 |
| `records` | List\<UserQyVO\> | 数据列表 |
每条记录 `UserQyVO`
| 字段 | 类型 | 说明 |
| -------- | ------ | -------- |
| `dlzh` | String | 登录账号 |
| `qymc` | String | 企业名称 |
**示例响应体:**
```json
{
"code": 0,
"msg": "ok",
"data": {
"total": 42,
"current": 1,
"size": 10,
"pages": 5,
"records": [
{ "dlzh": "user001@qy", "qymc": "上海链坤数字科技有限公司" },
{ "dlzh": "user001@qy", "qymc": "上海链坤集团" }
]
}
}
```
## 行为说明
- **一个用户可关联多个企业**:匹配结果中 `dlzh` 可能重复,每个企业占一行
- **空结果**:返回 HTTP 200`records=[]``total=0`,不抛业务异常
## 错误码
| 场景 | HTTP | code | msg |
| ------------- | ---- | ---- | --------------- |
| 成功 | 200 | 0 | ok |
| 请求体非 JSON | 400 | — | Spring 默认错误 |
| 系统异常 | 200 | 500 | "系统异常:..." |

View File

@ -4,7 +4,7 @@
**Goal:** 在 `txw-mhzc` 模块新增 `POST /user/listUserByQy` 接口,按 `dlzh`(登录账号)与 `qymc`(企业名称)模糊查询用户-企业匹配项并分页返回。
**Architecture:** 单条三表 JOIN SQL`yhxxb` ⨝ `yhqygxb``qyxxb`),写在 `TxwMhzcYhxxbMapper.xml`Mapper 接口显式接收 `IPage` 参数以触发 MyBatis-Plus 分页拦截器注入 LIMIT 与 COUNT(*)。Controller → Service → Mapper 三层调用,与现有 `getAllUser` 完全同构。
**Architecture:** 单条三表 JOIN SQL`yhxxb` ⨝ `yhqygxb``qyxxb`),写在 `TxwMhzcYhxxbMapper.xml`Mapper 接口显式接收 `IPage` 参数以触发 MyBatis-Plus 分页拦截器注入 LIMIT 与 COUNT(*)。Controller → Service → Mapper 三层调用,与现有 `getAllUser` 完全同构。SQL 用 `<choose>` 实现"OR 模糊匹配 + 空 key 返回空"的语义。
**Tech Stack:**
- Spring Boot + MyBatis-Plus 3.x项目内 BaseMapperX + IService
@ -19,10 +19,11 @@
- 模块归属:`txw-mhzc`,所有改动只动该模块
- 包路径:`com.css.txw.mhzc`
- 入参 `UserQyReqVO` 继承 `com.css.ggzc.framework.common.pojo.PageParam`
- 入参字段:单个 `String key`,对 `y.dlzh``q.qymc`**OR 模糊匹配**`key` 为空字符串或 null 时返回空结果SQL `<otherwise> AND 1=0 </otherwise>` 短路保证)
- 出参 `Page<UserQyVO>``com.baomidou.mybatisplus.extension.plugins.pagination.Page` 承载
- 接口路径:`POST /user/listUserByQy`(与 `UserController` 同前缀)
- 过滤硬性条件:`y.yxbz = 'Y' AND y.sdbz = 'N'`(与 `getYhxxByDlzh` 等现有查询一致)
- 模糊匹配:`LIKE CONCAT('%', #{reqVO.xxx}, '%')``#{...}` 走预编译SQL 注入安全
- 模糊匹配:`LIKE CONCAT('%', #{reqVO.key}, '%')``#{...}` 走预编译SQL 注入安全
- 排序:`ORDER BY y.lrrq DESC`
- 一个用户关联多个企业 → 结果中 dlzh 重复出现,每个企业占一行
- 项目无单测基建,按规格 §8 的 9 项手动验证清单作为验收标准
@ -51,7 +52,7 @@
**Interfaces:**
- Consumes: 无(最底层数据载体)
- Produces:
- `UserQyReqVO extends PageParam { String dlzh; String qymc; }`
- `UserQyReqVO extends PageParam { String key; }`
- `UserQyVO { String dlzh; String qymc; }`
- [ ] **Step 1: 创建 `UserQyReqVO.java`**
@ -69,11 +70,8 @@ import lombok.Data;
@Data
public class UserQyReqVO extends PageParam {
@Schema(description = "登录账号(模糊匹配,可空)")
private String dlzh;
@Schema(description = "企业名称(模糊匹配,可空)")
private String qymc;
@Schema(description = "关键字,对登录账号或企业名称做模糊匹配(必填,为空时返回空结果)")
private String key;
}
```
@ -161,18 +159,22 @@ Page<UserQyVO> listUserByQy(IPage<UserQyVO> page, @Param("reqVO") UserQyReqVO re
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'
<if test="reqVO.dlzh != null and reqVO.dlzh != ''">
AND y.dlzh LIKE CONCAT('%', #{reqVO.dlzh}, '%')
</if>
<if test="reqVO.qymc != null and reqVO.qymc != ''">
AND q.qymc LIKE CONCAT('%', #{reqVO.qymc}, '%')
</if>
<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>
```
> **关键说明**
> - 因为 mapper 参数加了 `@Param("reqVO")`XML 内 `<if test>``#{...}` 都必须用 `reqVO.dlzh` / `reqVO.qymc` 限定,不能直接写 `dlzh` / `qymc`
> - 因为 mapper 参数加了 `@Param("reqVO")`XML 内 `<if test>``#{...}` 都必须用 `reqVO.key` 限定
> - `<choose>` 分支保证 `key` 为空时 SQL 必定返回空(`AND 1=0` 短路),不依赖调用方传非空
> - `resultType` 必须写 VO 类的完全限定名,否则 MyBatis 无法实例化结果
> - `yxbz='Y' AND sdbz='N'` 是与现有 `getYhxxByDlzh`、`getYhxxByYhuuid` 等查询一致的硬性过滤条件
@ -308,113 +310,67 @@ git commit -m "feat(mhzc): 暴露 POST /user/listUserByQy 端点"
- 端口、网关、Swagger UI 路径以项目 `application.yaml` / `bootstrap-local.yml` 为准
- 数据库存在测试数据:至少有 1 个有效用户(`yxbz='Y' AND sdbz='N'`)关联 2 个企业1 个被锁定用户1 个无效用户
- [ ] **Step 1: 验证空入参**
- [ ] **Step 1: 验证 `key` 为空返回空结果**
请求:
```bash
curl -X POST http://localhost:<port>/user/listUserByQy \
-H "Content-Type: application/json" \
-d '{"dlzh":"","qymc":"","pageNo":1,"pageSize":10}'
-H "Content-Type: application/json; charset=UTF-8" \
--data-binary @verify-listUserByQy-empty.json
```
预期HTTP 200`code=0``data.total` ≥ 1`data.records` 中所有 `dlzh` 都属于有效用户。
文件内容:`{"key":"","pageNo":1,"pageSize":10}`
- [ ] **Step 2: 验证只按 `dlzh` 模糊匹配**
预期HTTP 200`code=0``data.records=[]``data.total=0`(由 SQL `AND 1=0` 短路保证)。
请求:
- [ ] **Step 2: 验证 `key` 命中 `dlzh`OR**
```bash
curl -X POST http://localhost:<port>/user/listUserByQy \
-H "Content-Type: application/json" \
-d '{"dlzh":"<某个已知账号前缀>","qymc":"","pageNo":1,"pageSize":10}'
```
请求(用文件 `verify-listUserByQy-dlzh.json`,内容 `{"key":"<已知账号前缀>","pageNo":1,"pageSize":10}`
预期:`data.records[*].dlzh` 都包含该前缀;`qymc` 不影响结果。
预期:`data.records[*].dlzh` 都包含该前缀;`qymc` 是否命中不影响结果。
- [ ] **Step 3: 验证只按 `qymc` 模糊匹配**
- [ ] **Step 3: 验证 `key` 命中 `qymc`OR**
请求:
```bash
curl -X POST http://localhost:<port>/user/listUserByQy \
-H "Content-Type: application/json" \
-d '{"dlzh":"","qymc":"<某个已知企业名称子串>","pageNo":1,"pageSize":10}'
```
请求(用文件 `verify-listUserByQy-qymc.json`,内容 `{"key":"<已知企业名称子串>","pageNo":1,"pageSize":10}`
预期:`data.records[*].qymc` 都包含该子串。
- [ ] **Step 4: 验证 `dlzh` 与 `qymc` 同时生效AND**
- [ ] **Step 4: 验证 `key` 同时命中 `dlzh``qymc`OR**
请求:
请求(用文件 `verify-listUserByQy-both.json`,内容 `{"key":"<账号或企业名中存在的子串>","pageNo":1,"pageSize":10}`
```bash
curl -X POST http://localhost:<port>/user/listUserByQy \
-H "Content-Type: application/json" \
-d '{"dlzh":"<某个已知账号前缀>","qymc":"<某个已知企业名称子串>","pageNo":1,"pageSize":10}'
```
预期:`data.records` 同时满足两个条件。
预期:`data.records` 包含命中任一字段的所有项。
- [ ] **Step 5: 验证多企业用户返回多行**
请求:
```bash
curl -X POST http://localhost:<port>/user/listUserByQy \
-H "Content-Type: application/json" \
-d '{"dlzh":"<关联 2 个企业的那个账号>","qymc":"","pageNo":1,"pageSize":10}'
```
请求(用文件 `verify-listUserByQy-multi.json`,内容 `{"key":"<关联 2 个企业的那个账号>","pageNo":1,"pageSize":10}`
预期:`data.records` 中该 `dlzh` 出现 2 次,分别对应 2 个不同的 `qymc`
- [ ] **Step 6: 验证锁定用户被过滤**
请求:
```bash
curl -X POST http://localhost:<port>/user/listUserByQy \
-H "Content-Type: application/json" \
-d '{"dlzh":"<被锁定用户账号>","qymc":"","pageNo":1,"pageSize":10}'
```
请求(用文件 `verify-listUserByQy-locked.json`,内容 `{"key":"<被锁定用户账号>","pageNo":1,"pageSize":10}`
预期:`data.records` 为空(被 `sdbz='N'` 过滤条件排除)。
- [ ] **Step 7: 验证无效用户被过滤**
请求:
```bash
curl -X POST http://localhost:<port>/user/listUserByQy \
-H "Content-Type: application/json" \
-d '{"dlzh":"<无效用户账号>","qymc":"","pageNo":1,"pageSize":10}'
```
请求(用文件 `verify-listUserByQy-invalid.json`,内容 `{"key":"<无效用户账号>","pageNo":1,"pageSize":10}`
预期:`data.records` 为空(被 `yxbz='Y'` 过滤条件排除)。
- [ ] **Step 8: 验证 SQL 注入安全**
请求:
请求(用文件 `verify-listUserByQy-inject.json`,内容 `{"key":"%' OR '1'='1","pageNo":1,"pageSize":10}`
```bash
curl -X POST http://localhost:<port>/user/listUserByQy \
-H "Content-Type: application/json" \
-d "{\"dlzh\":\"%' OR '1'='1\",\"qymc\":\"\",\"pageNo\":1,\"pageSize\":10}"
```
预期HTTP 200无 500 错误。`#{...}` 走预编译,转义后只匹配字面量 `'%' OR '1'='1'`,结果为空或仅含恰好等于该字面量的 dlzh数据库无此账号时为空
预期HTTP 200无 500 错误。`#{...}` 走预编译,转义后只匹配字面量 `'%' OR '1'='1'`,结果为空(数据库无此关键字时)。注意:`#{}` 不会转义 LIKE 元字符,因此输入中若含 `%``_` 仍会按 MySQL 通配符匹配。
- [ ] **Step 9: 验证分页边界**
请求:
请求(用文件 `verify-listUserByQy-page.json`,内容 `{"key":"<已知子串>","pageNo":99999,"pageSize":10}`
```bash
curl -X POST http://localhost:<port>/user/listUserByQy \
-H "Content-Type: application/json" \
-d '{"dlzh":"","qymc":"","pageNo":99999,"pageSize":10}'
```
预期:`data.records` 为空数组,`data.total` 仍正确(与 Step 1 一致),`data.current` 为 99999。
预期:`data.records` 为空数组,`data.total` 仍正确(与 Step 4 命中数一致),`data.current` 为 99999。
- [ ] **Step 10: 验收确认**
@ -438,7 +394,7 @@ curl -X POST http://localhost:<port>/user/listUserByQy \
**2. 占位符扫描:** 无 TBD / TODO / "implement later" / "类似 Task N"。
**3. 类型一致性:**
- `UserQyReqVO.dlzh` / `UserQyReqVO.qymc` 在 Task 1 定义Task 2 SQL 用 `reqVO.dlzh` / `reqVO.qymc` 引用Task 3-4 方法签名一致
- `UserQyReqVO.key` 在 Task 1 定义Task 2 SQL 用 `reqVO.key` 引用Task 3-4 方法签名一致
- `Page<UserQyVO>` 在 Task 1 定义Task 2 mapper 返回、Task 3 service 返回、Task 4 controller 返回,全程一致
- `@Param("reqVO")` 在 Task 2 mapper 声明Task 2 XML 内 `reqVO.dlzh` 一致
- `PageParam#getPageNo()` / `PageParam#getPageSize()` 是项目内既有方法Task 3 service 复用

View File

@ -10,7 +10,7 @@
新增一个按企业名称(及登录账号)查询用户的接口:
- 入参:`dlzh`(登录账号,可空)、`qymc`(企业名称,可空),均为**模糊匹配**
- 入参:单个 `key` 关键字,对 `dlzh`(登录账号)或 `qymc`(企业名称)做 **OR 模糊匹配**`key` 为空时返回空结果
- 出参:符合条件的 `dlzh` + `qymc` 列表,分页返回
- 一个用户可关联多个企业,匹配结果**每个企业一条记录**dlzh 可重复)
@ -43,12 +43,11 @@ Content-Type: application/json
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `dlzh` | String | 否 | 登录账号模糊匹配关键字 |
| `qymc` | String | 否 | 企业名称模糊匹配关键字 |
| `key` | String | 是 | 模糊匹配关键字,对 `dlzh``qymc` 任一命中即返回 |
| `pageNo` | Integer | 是 | 页码(继承 `PageParam` |
| `pageSize` | Integer | 是 | 每页大小(继承 `PageParam` |
`dlzh` 与 `qymc` 同时为空时,返回所有有效用户-企业匹配项,按 `lrrq DESC` 排序;只填一个时仅按该字段过滤
`key` 为空字符串或 null → 返回空结果;`key` 非空 → 对 `dlzh``qymc` 做 OR 模糊匹配
### 3.2 响应
@ -110,17 +109,21 @@ Controller#listUserByQy
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'
<if test="reqVO.dlzh != null and reqVO.dlzh != ''">
AND y.dlzh LIKE CONCAT('%', #{reqVO.dlzh}, '%')
</if>
<if test="reqVO.qymc != null and reqVO.qymc != ''">
AND q.qymc LIKE CONCAT('%', #{reqVO.qymc}, '%')
</if>
<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. 代码骨架
@ -138,11 +141,8 @@ import lombok.Data;
@Data
public class UserQyReqVO extends PageParam {
@Schema(description = "登录账号(模糊匹配,可空)")
private String dlzh;
@Schema(description = "企业名称(模糊匹配,可空)")
private String qymc;
@Schema(description = "关键字,对登录账号或企业名称做模糊匹配(必填,为空时返回空结果)")
private String key;
}
```
@ -205,8 +205,8 @@ public CommonResult<Page<UserQyVO>> listUserByQy(@RequestBody UserQyReqVO reqVO)
| 场景 | 行为 |
|---|---|
| `dlzh` 与 `qymc` 都为空 | 返回所有有效用户-企业项,按 `lrrq DESC` |
| 无匹配 | `Page{records=[], total=0}`HTTP 200不抛业务异常 |
| `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 '\\'`,或调用方自行过滤通配符。 |
@ -214,10 +214,10 @@ public CommonResult<Page<UserQyVO>> listUserByQy(@RequestBody UserQyReqVO reqVO)
与现有项目风格一致,以手动验证为主:
1. **空入参**`{dlzh:"",qymc:"",pageNo:1,pageSize:10}` → 返回所有有效用户-企业项
2. **只按 dlzh**:匹配 dlzh 含子串的所有项
3. **只按 qymc**:匹配 qymc 含子串的所有项
4. **同时填两个**AND 关系
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'` 的用户不出现在结果中

View File

@ -8,9 +8,6 @@ import lombok.Data;
@Data
public class UserQyReqVO extends PageParam {
@Schema(description = "登录账号(模糊匹配,可空)")
private String dlzh;
@Schema(description = "企业名称(模糊匹配,可空)")
private String qymc;
@Schema(description = "关键字,对登录账号或企业名称做模糊匹配(必填,为空时返回空结果)")
private String key;
}

View File

@ -48,12 +48,15 @@
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'
<if test="reqVO.dlzh != null and reqVO.dlzh != ''">
AND y.dlzh LIKE CONCAT('%', #{reqVO.dlzh}, '%')
</if>
<if test="reqVO.qymc != null and reqVO.qymc != ''">
AND q.qymc LIKE CONCAT('%', #{reqVO.qymc}, '%')
</if>
<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>
</mapper>

View File

@ -1 +1 @@
{"dlzh":"","qymc":"上海链坤数字科技有限公司","pageNo":1,"pageSize":10}
{"key":"上海","pageNo":1,"pageSize":10}