记录一次日常需求开发:给房费台账分页查询接口增加
paySource(支付来源)筛选条件。
一、需求背景
项目是一个基于 Spring Boot + MyBatis 的 PMS 酒店管理系统。
业务方希望在前端页面中能够按照"支付来源"对房费台账进行筛选,因此需要在后端查询接口中新增 paySource 入参,并在 SQL 中对该字段进行过滤。
数据库表 checkin_cost_records 中早已存在 pay_source 字段,但之前的查询 BO 和 Mapper XML 都没有将其作为查询条件使用。本次改动就是要把这个能力补上。
二、整体思路
我的实现步骤非常直接:
- 在查询参数对象
CheckinCostRecordQueryBO中增加paySource字段及其 getter/setter - 在 Mapper XML 的三个相关查询中增加
paySource的动态 SQL 条件 - 编译验证,确保没有语法错误
改动面很小,但能完整体现"加字段"的标准流程。
三、具体改动
1)修改查询 BO:增加 paySource 字段
文件:src/main/java/com/crt/pms/bean/bo/checkincostrecord/CheckinCostRecordQueryBO.java
java
public class CheckinCostRecordQueryBO {
private String checkInNo;
private Integer type;
private Integer payMethod;
private Integer paySource; // 新增
private Integer costRecordType;
private String roomNo;
private String handlerName;
private String startTime;
private String endTime;
// getters & setters ...
public Integer getPaySource() {
return paySource;
}
public CheckinCostRecordQueryBO setPaySource(Integer paySource) {
this.paySource = paySource;
return this;
}
}
说明:
- 类型选择
Integer,与数据库pay_source的定义保持一致 - 采用链式 setter,方便后续扩展(虽然查询场景并不强制)
2)修改 Mapper XML:在查询中增加条件
文件:src/main/resources/mapper/CheckinCostRecordMapper.xml
需要在以下三个 <select> 中都加上 paySource 的条件,保证分页查询、计数查询、统计查询行为一致。
(1) 分页查询:searchCheckinCostRecordsPaged
xml
<select id="searchCheckinCostRecordsPaged"
resultType="com.crt.pms.bean.vo.checkincostrecord.CheckinCostRecordPageVO">
SELECT c.*,
rc.check_in_no AS checkInNo,
rc.actual_guest_name AS actualGuestName,
rc.actual_check_in_time AS actualCheckInTime,
rc.status AS checkInStatus,
rc.current_room_number AS currentRoomNumber
FROM checkin_cost_records c
JOIN room_checkin rc ON c.check_in_id = rc.id
<where>
c.deleted = 0
AND rc.deleted = 0
<if test="query != null and query.checkInNo != null and query.checkInNo != ''">
AND rc.check_in_no LIKE CONCAT('%', #{query.checkInNo}, '%')
</if>
<if test="query != null and query.type != null">
AND c.type = #{query.type}
</if>
<if test="query != null and query.payMethod != null">
AND c.pay_method = #{query.payMethod}
</if>
<!-- 新增:paySource -->
<if test="query != null and query.paySource != null">
AND c.pay_source = #{query.paySource}
</if>
<if test="query != null and query.costRecordType != null">
AND c.cost_record_type = #{query.costRecordType}
</if>
<if test="query != null and query.roomNo != null and query.roomNo != ''">
AND rc.current_room_number LIKE CONCAT('%', #{query.roomNo}, '%')
</if>
<if test="query != null and query.handlerName != null and query.handlerName != ''">
AND c.update_by_name LIKE CONCAT('%', #{query.handlerName}, '%')
</if>
<if test="query != null and query.startTime != null and query.startTime != ''">
AND DATE_FORMAT(c.update_time, '%Y-%m-%d') >= #{query.startTime}
</if>
<if test="query != null and query.endTime != null and query.endTime != ''">
AND DATE_FORMAT(c.update_time, '%Y-%m-%d') <= #{query.endTime}
</if>
</where>
ORDER BY c.create_time DESC
LIMIT #{size} OFFSET #{start}
</select>
(2) 计数查询:searchCheckinCostRecordsCount
xml
<select id="searchCheckinCostRecordsCount" resultType="long">
SELECT COUNT(1)
FROM checkin_cost_records c
JOIN room_checkin rc ON c.check_in_id = rc.id
<where>
c.deleted = 0
AND rc.deleted = 0
<!-- 其他条件省略 -->
<!-- 新增:paySource -->
<if test="query != null and query.paySource != null">
AND c.pay_source = #{query.paySource}
</if>
<!-- 其他条件省略 -->
</where>
</select>
(3) 统计查询:getCheckinCostRecordsStatistics
xml
<select id="getCheckinCostRecordsStatistics"
resultType="com.crt.pms.bean.vo.checkincostrecord.CheckinCostRecordStatisticsVO">
SELECT COALESCE(SUM(CASE WHEN c.type = #{incomeType} THEN c.amount ELSE #{zero} END), #{zero}) AS income,
COALESCE(SUM(CASE WHEN c.type = #{costType} THEN c.amount ELSE #{zero} END), #{zero}) AS refunded,
COALESCE(SUM(CASE
WHEN c.type = #{incomeType} THEN c.amount
WHEN c.type = #{costType} THEN -c.amount
ELSE #{zero}
END), #{zero}) AS total
FROM checkin_cost_records c
JOIN room_checkin rc ON c.check_in_id = rc.id
<where>
c.deleted = 0
AND rc.deleted = 0
<!-- 其他条件省略 -->
<!-- 新增:paySource -->
<if test="queryVO != null and queryVO.query != null and queryVO.query.paySource != null">
AND c.pay_source = #{queryVO.query.paySource}
</if>
<!-- 其他条件省略 -->
</where>
</select>
四、为什么是这三个地方都要改?
- 分页查询:真正返回数据的查询,必须支持新条件
- 计数查询:前端分页依赖总条数,若不一致会导致"查到的数据条数"和"分页总数"不匹配
- 统计查询:台账页面的汇总金额需要与当前筛选条件一致,否则会出现"列表和总额对不上"的问题
这三者保持相同的过滤逻辑,才能保证前端体验一致、数据自洽。
五、一些实现细节与避坑指南
- 判空很重要
使用<if test="query != null and query.paySource != null">避免传 null 时 SQL 拼接出错。 - 命名一致性
Java 用paySource,数据库列名用pay_source,MyBatis 自动映射更省心。 - 类型对齐
BO 中字段类型与数据库一致,减少隐式转换带来的潜在问题。 - 同步修改所有相关查询
凡是受筛选条件影响的查询,都要一并更新,避免"顾此失彼"。
六、调用示例
前端请求体示例(分页查询):
json
{
"page": 1,
"size": 20,
"query": {
"paySource": 1,
"checkInNo": "CI20260312001",
"type": 2,
"startTime": "2026-03-01",
"endTime": "2026-03-12"
}
}
生成的 SQL 片段类似:
sql
AND c.pay_source = 1
七、小结
这次改动非常"工程日常":
- 代码量不大
- 思路清晰
- 收益明确(前端可按支付来源精确筛选)
同时也再次验证了一个经验:在 MyBatis 里加查询条件,本质就是两件事------BO 加字段、XML 加条件。只要记住"分页 + 计数 + 统计一起改",基本就不会踩坑。
如果你也维护着类似的后台系统,这类"加字段"的操作完全可以当成标准模板来用。