232 lines
8.6 KiB
Markdown
232 lines
8.6 KiB
Markdown
# 按企业查询用户接口设计
|
||
|
||
- 日期: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#listUserByQy(XML <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` 超过总页数 → 返回空 records,total 仍正确
|
||
|
||
## 9. 不在范围内
|
||
|
||
- 不修改现有 `getAllUser` 接口及 `UserVO`
|
||
- 不新增权限校验(与 `getAllUser` 一致,无显式权限控制,依赖网关层)
|
||
- 不缓存查询结果(用户与企业关联可能变化,首次上线不强加缓存策略)
|
||
- 不支持按其他字段(手机号、身份证号、纳税人识别号)筛选 |