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

16 KiB
Raw Blame History

按企业查询用户接口 实施计划

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 SQLyhxxbyhqygxbqyxxb),写在 TxwMhzcYhxxbMapper.xmlMapper 接口显式接收 IPage 参数以触发 MyBatis-Plus 分页拦截器注入 LIMIT 与 COUNT(*)。Controller → Service → Mapper 三层调用,与现有 getAllUser 完全同构。

Tech Stack:

  • Spring Boot + MyBatis-Plus 3.x项目内 BaseMapperX + IService
  • MySQL 8项目数据库
  • Lombok、@SchemaSwagger 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 新建 入参 VOdlzhqymc,继承 PageParam
txw-mhzc/txw-mhzc-service-biz/src/main/java/com/css/txw/mhzc/pojo/vo/UserQyVO.java 新建 出参 VOdlzh + 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 1Page<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 参数时才会自动注入 LIMITSELECT 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 / qymc
  • resultType 必须写 VO 类的完全限定名,否则 MyBatis 无法实例化结果
  • yxbz='Y' AND sdbz='N' 是与现有 getYhxxByDlzhgetYhxxByYhuuid 等查询一致的硬性过滤条件
  • 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;

PageList 已有 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 方法

PageyhxxbService 已有 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 200code=0data.total ≥ 1data.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: 验证 dlzhqymc 同时生效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 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 复用
  • 无方法重命名风险

通过。