16 KiB
按企业查询用户接口 实施计划
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
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
文件内容(完整复制):
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
文件内容(完整复制):
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 工程,运行:
cd E:/00项目/T_碳信网/code/txw/txw-mhzc/txw-mhzc-service-biz
mvn -q -DskipTests compile
预期:BUILD SUCCESS,无报错。两个新 VO 已纳入编译路径。
- Step 4: 提交
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 区域增加:
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() 之后)新增:
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> 之前插入:
<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/qymcresultType必须写 VO 类的完全限定名,否则 MyBatis 无法实例化结果yxbz='Y' AND sdbz='N'是与现有getYhxxByDlzh、getYhxxByYhuuid等查询一致的硬性过滤条件
- Step 3: 编译校验
cd E:/00项目/T_碳信网/code/txw/txw-mhzc/txw-mhzc-service-biz
mvn -q -DskipTests compile
预期:BUILD SUCCESS。
- Step 4: 提交
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); 这一行之后新增:
Page<UserQyVO> listUserByQy(UserQyReqVO reqVO);
- Step 2: 在
TxwMhzcYhxxbServiceImpl.java中新增实现
在该文件顶部 import 区域追加:
import com.baomidou.mybatisplus.core.metadata.IPage;
(Page 与 List 已有 import,无须重复)
在 getAllUser(UserReqVO reqVO) 方法体之后新增:
@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: 编译校验
cd E:/00项目/T_碳信网/code/txw/txw-mhzc/txw-mhzc-service-biz
mvn -q -DskipTests compile
预期:BUILD SUCCESS。
- Step 4: 提交
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 之前)新增:
@PostMapping("/listUserByQy")
@Operation(summary = "按企业查询用户",
description = "按登录账号与企业名称模糊查询用户-企业匹配项")
public CommonResult<Page<UserQyVO>> listUserByQy(@RequestBody UserQyReqVO reqVO) {
return CommonResult.success(yhxxbService.listUserByQy(reqVO));
}
- Step 2: 编译校验
cd E:/00项目/T_碳信网/code/txw/txw-mhzc/txw-mhzc-service-biz
mvn -q -DskipTests compile
预期:BUILD SUCCESS。
- Step 3: 提交
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: 验证空入参
请求:
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模糊匹配
请求:
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模糊匹配
请求:
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)
请求:
curl -X POST http://localhost:<port>/user/listUserByQy \
-H "Content-Type: application/json" \
-d '{"dlzh":"<某个已知账号前缀>","qymc":"<某个已知企业名称子串>","pageNo":1,"pageSize":10}'
预期:data.records 同时满足两个条件。
- Step 5: 验证多企业用户返回多行
请求:
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: 验证锁定用户被过滤
请求:
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: 验证无效用户被过滤
请求:
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 注入安全
请求:
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: 验证分页边界
请求:
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 8(SQL 注入)、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 复用- 无方法重命名风险
通过。