docs(plan): 新增按企业查询用户接口实施计划

This commit is contained in:
liulujian 2026-07-27 12:06:59 +08:00
parent a97d84290c
commit f4ac83d552

View File

@ -0,0 +1,447 @@
# 按企业查询用户接口 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**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` 完全同构。
**Tech Stack:**
- Spring Boot + MyBatis-Plus 3.x项目内 BaseMapperX + IService
- MySQL 8项目数据库
- Lombok、`@Schema`Swagger v3
- 分页由 `PaginationInnerInterceptor` 自动包装
**Spec:** [`docs/superpowers/specs/2026-07-27-user-list-by-qy-design.md`](../specs/2026-07-27-user-list-by-qy-design.md)
## Global Constraints
- 模块归属:`txw-mhzc`,所有改动只动该模块
- 包路径:`com.css.txw.mhzc`
- 入参 `UserQyReqVO` 继承 `com.css.ggzc.framework.common.pojo.PageParam`
- 出参 `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 注入安全
- 排序:`ORDER BY y.lrrq DESC`
- 一个用户关联多个企业 → 结果中 dlzh 重复出现,每个企业占一行
- 项目无单测基建,按规格 §8 的 9 项手动验证清单作为验收标准
- 严禁修改现有 `getAllUser` 接口与 `UserVO`
## File Structure
| 文件 | 类型 | 作用 |
|---|---|---|
| `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/vo/UserQyReqVO.java` | 新建 | 入参 VO`dlzh`、`qymc`,继承 `PageParam` |
| `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/vo/UserQyVO.java` | 新建 | 出参 VO`dlzh` + `qymc` |
| `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/mapper/TxwMhzcYhxxbMapper.java` | 修改 | 新增 `listUserByQy(IPage<UserQyVO>, UserQyReqVO)` 接口方法 |
| `txw-mhzc/txw-mhzc-service-biz/src/main/resources/mapper/TxwMhzcYhxxbMapper.xml` | 修改 | 新增 `<select id="listUserByQy">` |
| `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/TxwMhzcYhxxbService.java` | 修改 | 新增 `listUserByQy(UserQyReqVO)` 接口方法 |
| `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/impl/TxwMhzcYhxxbServiceImpl.java` | 修改 | 新增实现,构造 Page 后调用 mapper |
| `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/controller/UserController.java` | 修改 | 新增 `POST /listUserByQy` 端点 |
---
## Task 1: 新增入参/出参 VO
**Files:**
- Create: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/vo/UserQyReqVO.java`
- Create: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/vo/UserQyVO.java`
**Interfaces:**
- Consumes: 无(最底层数据载体)
- Produces:
- `UserQyReqVO extends PageParam { String dlzh; String qymc; }`
- `UserQyVO { String dlzh; String qymc; }`
- [ ] **Step 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 dlzh;
@Schema(description = "企业名称(模糊匹配,可空)")
private String qymc;
}
```
- [ ] **Step 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;
}
```
- [ ] **Step 3: 编译校验**
打开 `txw-mhzc/txw-mhzc-service-biz` 目录下的 Maven 工程,运行:
```bash
cd E:/00项目/T_碳信网/code/txw/txw-mhzc/txw-mhzc-service-biz
mvn -q -DskipTests compile
```
预期:`BUILD SUCCESS`,无报错。两个新 VO 已纳入编译路径。
- [ ] **Step 4: 提交**
```bash
cd E:/00项目/T_碳信网/code/txw
git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/vo/UserQyReqVO.java
git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/vo/UserQyVO.java
git commit -m "feat(mhzc): 新增按企业查询用户接口入参/出参 VO"
```
---
## Task 2: 在 Mapper 接口与 XML 中新增 `listUserByQy`
**Files:**
- Modify: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/mapper/TxwMhzcYhxxbMapper.java`
- Modify: `txw-mhzc/txw-mhzc-service-biz/src/main/resources/mapper/TxwMhzcYhxxbMapper.xml`
**Interfaces:**
- Consumes: `UserQyReqVO`(来自 Task 1、`Page<UserQyVO>`MyBatis-Plus 分页)
- Produces: `Page<UserQyVO>` —— 返回类型供 Service 层直接返回
- [ ] **Step 1: 在 `TxwMhzcYhxxbMapper.java` 中新增 import 与方法声明**
在该文件顶部 import 区域增加:
```java
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.css.txw.mhzc.pojo.vo.UserQyReqVO;
import com.css.txw.mhzc.pojo.vo.UserQyVO;
```
然后在该接口类内(`getAllUserId()` 方法之后、`unlock(...)` 之前均可,本计划插在 `getAllUserId()` 之后)新增:
```java
Page<UserQyVO> listUserByQy(IPage<UserQyVO> page, @Param("reqVO") UserQyReqVO reqVO);
```
> **关键说明**:方法签名必须显式包含 `IPage<UserQyVO> page` 参数。MyBatis-Plus 的 `PaginationInnerInterceptor` 仅在 mapper 方法签名出现 `IPage` 参数时才会自动注入 `LIMIT``SELECT COUNT(*)` 子查询。如果只传 `reqVO`,分页不会生效。
- [ ] **Step 2: 在 `TxwMhzcYhxxbMapper.xml` 中新增 `<select>`**
`</mapper>` 之前插入:
```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'
<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>
ORDER BY y.lrrq DESC
</select>
```
> **关键说明**
> - 因为 mapper 参数加了 `@Param("reqVO")`XML 内 `<if test>``#{...}` 都必须用 `reqVO.dlzh` / `reqVO.qymc` 限定,不能直接写 `dlzh` / `qymc`
> - `resultType` 必须写 VO 类的完全限定名,否则 MyBatis 无法实例化结果
> - `yxbz='Y' AND sdbz='N'` 是与现有 `getYhxxByDlzh`、`getYhxxByYhuuid` 等查询一致的硬性过滤条件
- [ ] **Step 3: 编译校验**
```bash
cd E:/00项目/T_碳信网/code/txw/txw-mhzc/txw-mhzc-service-biz
mvn -q -DskipTests compile
```
预期:`BUILD SUCCESS`。
- [ ] **Step 4: 提交**
```bash
cd E:/00项目/T_碳信网/code/txw
git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/mapper/TxwMhzcYhxxbMapper.java
git add txw-mhzc/txw-mhzc-service-biz/src/main/resources/mapper/TxwMhzcYhxxbMapper.xml
git commit -m "feat(mhzc): 在用户 mapper 中新增 listUserByQy 三表 JOIN 查询"
```
---
## Task 3: 在 Service 接口与实现中接入 `listUserByQy`
**Files:**
- Modify: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/TxwMhzcYhxxbService.java`
- Modify: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/impl/TxwMhzcYhxxbServiceImpl.java`
**Interfaces:**
- Consumes: `UserQyReqVO`(来自 Task 1
- Produces: `Page<UserQyVO>` —— Controller 直接消费
- [ ] **Step 1: 在 `TxwMhzcYhxxbService.java` 接口中新增方法声明**
在该文件顶部 import 区域(已有 `import ... vo.*;` 通配)无需改动。在 `Page<UserVO> getAllUser(UserReqVO reqVO);` 这一行之后新增:
```java
Page<UserQyVO> listUserByQy(UserQyReqVO reqVO);
```
- [ ] **Step 2: 在 `TxwMhzcYhxxbServiceImpl.java` 中新增实现**
在该文件顶部 import 区域追加:
```java
import com.baomidou.mybatisplus.core.metadata.IPage;
```
`Page` 与 `List` 已有 import无须重复
`getAllUser(UserReqVO reqVO)` 方法体之后新增:
```java
@Override
public Page<UserQyVO> listUserByQy(UserQyReqVO reqVO) {
Page<UserQyVO> page = new Page<>(reqVO.getPageNo(), reqVO.getPageSize());
return yhxxbMapper.listUserByQy(page, reqVO);
}
```
> **关键说明**
> - Service 接口只接收 `UserQyReqVO`,由 Service 内部 `new Page<>(pageNo, pageSize)` 后显式传给 mapper。这是项目内 `getAllUser` 同款的分层方式
> - 复用现有的 `yhxxbMapper` 注入,无需新增 `@Resource`
- [ ] **Step 3: 编译校验**
```bash
cd E:/00项目/T_碳信网/code/txw/txw-mhzc/txw-mhzc-service-biz
mvn -q -DskipTests compile
```
预期:`BUILD SUCCESS`。
- [ ] **Step 4: 提交**
```bash
cd E:/00项目/T_碳信网/code/txw
git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/TxwMhzcYhxxbService.java
git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/service/impl/TxwMhzcYhxxbServiceImpl.java
git commit -m "feat(mhzc): 在用户 service 中接入 listUserByQy 分页查询"
```
---
## Task 4: 在 `UserController` 中暴露 `POST /listUserByQy` 端点
**Files:**
- Modify: `txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/controller/UserController.java`
**Interfaces:**
- Consumes: HTTP `POST /user/listUserByQy`,请求体 `UserQyReqVO`
- Produces: `CommonResult<Page<UserQyVO>>`
- [ ] **Step 1: 在 `UserController` 中新增 controller 方法**
`Page`、`yhxxbService` 已有 import无须改动。在 `getAllUser` 之后(`getAllUserId` 之前)新增:
```java
@PostMapping("/listUserByQy")
@Operation(summary = "按企业查询用户",
description = "按登录账号与企业名称模糊查询用户-企业匹配项")
public CommonResult<Page<UserQyVO>> listUserByQy(@RequestBody UserQyReqVO reqVO) {
return CommonResult.success(yhxxbService.listUserByQy(reqVO));
}
```
- [ ] **Step 2: 编译校验**
```bash
cd E:/00项目/T_碳信网/code/txw/txw-mhzc/txw-mhzc-service-biz
mvn -q -DskipTests compile
```
预期:`BUILD SUCCESS`。
- [ ] **Step 3: 提交**
```bash
cd E:/00项目/T_碳信网/code/txw
git add txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/controller/UserController.java
git commit -m "feat(mhzc): 暴露 POST /user/listUserByQy 端点"
```
---
## Task 5: 手动端到端验证
**Files:** 无新文件
**前置:**
- 已启动 `txw-mhzc-service-biz`(开发环境 `DevAppStarter`
- 端口、网关、Swagger UI 路径以项目 `application.yaml` / `bootstrap-local.yml` 为准
- 数据库存在测试数据:至少有 1 个有效用户(`yxbz='Y' AND sdbz='N'`)关联 2 个企业1 个被锁定用户1 个无效用户
- [ ] **Step 1: 验证空入参**
请求:
```bash
curl -X POST http://localhost:<port>/user/listUserByQy \
-H "Content-Type: application/json" \
-d '{"dlzh":"","qymc":"","pageNo":1,"pageSize":10}'
```
预期HTTP 200`code=0``data.total` ≥ 1`data.records` 中所有 `dlzh` 都属于有效用户。
- [ ] **Step 2: 验证只按 `dlzh` 模糊匹配**
请求:
```bash
curl -X POST http://localhost:<port>/user/listUserByQy \
-H "Content-Type: application/json" \
-d '{"dlzh":"<某个已知账号前缀>","qymc":"","pageNo":1,"pageSize":10}'
```
预期:`data.records[*].dlzh` 都包含该前缀;`qymc` 不影响结果。
- [ ] **Step 3: 验证只按 `qymc` 模糊匹配**
请求:
```bash
curl -X POST http://localhost:<port>/user/listUserByQy \
-H "Content-Type: application/json" \
-d '{"dlzh":"","qymc":"<某个已知企业名称子串>","pageNo":1,"pageSize":10}'
```
预期:`data.records[*].qymc` 都包含该子串。
- [ ] **Step 4: 验证 `dlzh``qymc` 同时生效AND**
请求:
```bash
curl -X POST http://localhost:<port>/user/listUserByQy \
-H "Content-Type: application/json" \
-d '{"dlzh":"<某个已知账号前缀>","qymc":"<某个已知企业名称子串>","pageNo":1,"pageSize":10}'
```
预期:`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}'
```
预期:`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}'
```
预期:`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}'
```
预期:`data.records` 为空(被 `yxbz='Y'` 过滤条件排除)。
- [ ] **Step 8: 验证 SQL 注入安全**
请求:
```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数据库无此账号时为空
- [ ] **Step 9: 验证分页边界**
请求:
```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。
- [ ] **Step 10: 验收确认**
9 步全部通过 → 在 SPEC 文件 §8 旁记录「已验收」日期与签名;如有未通过项,回到对应 Task 修正后重新执行该步骤。
---
## Self-Review
执行计划前的自审:
**1. Spec 覆盖:**
- §3 接口契约 → Task 4 controller 实现
- §4 组件设计 → Task 1-4 各自负责
- §5 SQL 设计 → Task 2 XML
- §6 代码骨架 → 拆分到 Task 1-4
- §7 错误处理 → Task 5 Step 1空入参、Step 8SQL 注入、Step 9分页边界
- §8 测试清单 9 项 → Task 5 Step 1-9 逐项对应
- §9 不在范围内 → 全局约束与各 Task 注释中已声明(不改 getAllUser、不加权限
**2. 占位符扫描:** 无 TBD / TODO / "implement later" / "类似 Task N"。
**3. 类型一致性:**
- `UserQyReqVO.dlzh` / `UserQyReqVO.qymc` 在 Task 1 定义Task 2 SQL 用 `reqVO.dlzh` / `reqVO.qymc` 引用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 复用
- 无方法重命名风险
**通过。**