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

8.6 KiB
Raw Blame History

按企业查询用户接口设计

  • 日期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_uuiddlzh(登录账号)、yxbz(有效标志 Y/Nsdbz(锁定标志 Y/N
txw_mhzc_qyxxb(企业表) qyuuidqymc(企业名称)
txw_mhzc_yhqygxb(用户-企业关系表) yh_uuidqyuuid一对多

用户过滤硬性条件:yxbz = 'Y' AND sdbz = 'N'(与 getYhxxByDlzh 等现有查询保持一致)。

3. 接口契约

3.1 请求

POST /user/listUserByQy
Content-Type: application/json

请求体 UserQyReqVO

字段 类型 必填 说明
key String 模糊匹配关键字,对 dlzhqymc 任一命中即返回
pageNo Integer 页码(继承 PageParam
pageSize Integer 每页大小(继承 PageParam

key 为空字符串或 null → 返回空结果;key 非空 → 对 dlzhqymc 做 OR 模糊匹配。

3.2 响应

{
  "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 入参 VOdlzhqymc,继承 PageParam
pojo/vo/UserQyVO.java 出参 VOdlzh + 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#listUserByQyXML <select>
    → DB: 三表 JOIN + LIKE + 分页
    → Page<UserQyVO>

5. SQL 设计

<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

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

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 新增方法

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 新增方法

Page<UserQyVO> listUserByQy(UserQyReqVO reqVO);

6.5 TxwMhzcYhxxbServiceImpl.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 新增方法

@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 与 qymcOR 关系,命中任一即返回
  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=10pageNo 超过总页数 → 返回空 recordstotal 仍正确

9. 不在范围内

  • 不修改现有 getAllUser 接口及 UserVO
  • 不新增权限校验(与 getAllUser 一致,无显式权限控制,依赖网关层)
  • 不缓存查询结果(用户与企业关联可能变化,首次上线不强加缓存策略)
  • 不支持按其他字段(手机号、身份证号、纳税人识别号)筛选