refactor(mybatis): 去掉代码内拼 SQL 片段并用分页封装 limit

超时跨表查询迁 Mapper @Select+IPage;SortParam 排序字段加标识符白名单;listLimit/firstOpt 替代 .last。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
DaxPay Dev
2026-07-23 16:15:50 +08:00
parent 9e26d6a2b2
commit 7f5d9b8701
7 changed files with 96 additions and 45 deletions

View File

@@ -39,23 +39,19 @@ public class DouyinDirectAppManager extends BaseManager<DouyinDirectAppMapper, D
/// 根据通道商户号取首个应用(支付/回调,租户内)
public Optional<DouyinDirectApp> findFirstByChannelMchNo(String channelMchNo) {
return lambdaQuery()
return firstOpt(q -> q
.eq(DouyinDirectApp::getChannelMchNo, channelMchNo)
.orderByAsc(DouyinDirectApp::getCreateTime)
.orderByAsc(DouyinDirectApp::getId)
.last("limit 1")
.oneOpt();
.orderByAsc(DouyinDirectApp::getId));
}
/// 根据通道商户号与应用类型取首个应用(支付/回调,租户内)
public Optional<DouyinDirectApp> findFirstByChannelMchNoAndAppType(String channelMchNo, String appType) {
return lambdaQuery()
return firstOpt(q -> q
.eq(DouyinDirectApp::getChannelMchNo, channelMchNo)
.eq(DouyinDirectApp::getAppType, appType)
.orderByAsc(DouyinDirectApp::getCreateTime)
.orderByAsc(DouyinDirectApp::getId)
.last("limit 1")
.oneOpt();
.orderByAsc(DouyinDirectApp::getId));
}
/// 按通道商户号与 douyinAppId 查询应用(支付/回调,租户内)

View File

@@ -61,13 +61,11 @@ public class GatewayPayOrderManager extends BaseManager<GatewayPayOrderMapper, G
/// 已过期仍待支付/支付中的网关单(超时兜底)
@IgnoreTenant
public List<GatewayPayOrder> findTimeoutOrders(OffsetDateTime now) {
return lambdaQuery()
return listLimit(500, q -> q
.in(GatewayPayOrder::getStatus,
GatewayOrderStatusEnum.WAIT_PAY.getCode(),
GatewayOrderStatusEnum.PAYING.getCode())
.lt(GatewayPayOrder::getExpiredTime, now)
.orderByAsc(GatewayPayOrder::getExpiredTime)
.last("limit 500")
.list();
.orderByAsc(GatewayPayOrder::getExpiredTime));
}
}

View File

@@ -7,8 +7,6 @@ import cn.daxpay.open.platform.core.annotation.IgnoreTenant;
import cn.daxpay.open.platform.core.exception.DangerSqlException;
import cn.daxpay.open.platform.core.code.CommonCode;
import cn.daxpay.open.platform.core.rest.param.PageParam;
import cn.daxpay.open.payment.trade.enums.PayFundStatusEnum;
import cn.daxpay.open.payment.trade.enums.PayTradeTypeEnum;
import cn.daxpay.open.payment.trade.order.entity.PayTrade;
import cn.daxpay.open.payment.trade.order.param.PayTradeQuery;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@@ -65,16 +63,14 @@ public class PayTradeManager extends BaseManager<PayTradeMapper, PayTrade> {
}
/// 查询网关支付已超时但仍处理中的资金交易(兜底)
/// expiredTime 在容器(pay_gateway_order)上, 用子查询关联
///
/// expiredTime 在容器(pay_gateway_order)上, SQL 见 [PayTradeMapper#findGatewayTimeoutTrades]。
/// 跨租户扫描(定时任务无 HTTP 上下文), 单次上限 500 防积压爆量(分页插件生成方言 limit)。
@IgnoreTenant
public List<PayTrade> findGatewayTimeoutTrades(OffsetDateTime now) {
return lambdaQuery()
.eq(PayTrade::getTradeType, PayTradeTypeEnum.GATEWAY.getCode())
.eq(PayTrade::getStatus, PayFundStatusEnum.PROCESSING.getCode())
.apply("container_id IN (SELECT id FROM pay_gateway_order WHERE expired_time < {0})", now)
.orderByAsc(PayTrade::getCreateTime)
.last("limit 500")
.list();
Page<PayTrade> page = new Page<>(1, 500);
page.setSearchCount(false);
return getBaseMapper().findGatewayTimeoutTrades(page, now).getRecords();
}
/// 分页查询(管理端), 默认按创建时间倒序
@@ -88,18 +84,14 @@ public class PayTradeManager extends BaseManager<PayTradeMapper, PayTrade> {
/// 查询普通支付已超时但仍处理中的资金交易(兜底定时任务用)
///
/// 条件: tradeType=NORMAL 且 status=PROCESSING 且容器 expiredTime < now
/// expiredTime 在容器(pay_normal_order)上, 用子查询关联
/// 跨租户扫描(定时任务无 HTTP 上下文), 单次上限 500 防积压爆量。
/// 条件: tradeType=NORMAL 且 status=PROCESSING 且容器 expiredTime < now
/// expiredTime 在容器(pay_normal_order)上, SQL 见 [PayTradeMapper#findNormalTimeoutTrades]
/// 跨租户扫描(定时任务无 HTTP 上下文), 单次上限 500 防积压爆量(分页插件生成方言 limit)
@IgnoreTenant
public List<PayTrade> findNormalTimeoutTrades(OffsetDateTime now) {
return lambdaQuery()
.eq(PayTrade::getTradeType, PayTradeTypeEnum.NORMAL.getCode())
.eq(PayTrade::getStatus, PayFundStatusEnum.PROCESSING.getCode())
.apply("container_id IN (SELECT id FROM pay_normal_order WHERE expired_time < {0})", now)
.orderByAsc(PayTrade::getCreateTime)
.last("limit 500")
.list();
Page<PayTrade> page = new Page<>(1, 500);
page.setSearchCount(false);
return getBaseMapper().findNormalTimeoutTrades(page, now).getRecords();
}
/// 根据id进行更新失败时抛出异常

View File

@@ -1,11 +1,52 @@
package cn.daxpay.open.payment.trade.order.dao;
import cn.daxpay.open.payment.trade.order.entity.PayTrade;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.time.OffsetDateTime;
/// # 资金交易凭证 Mapper
///
@Mapper
public interface PayTradeMapper extends MPJBaseMapper<PayTrade> {
/// 查询普通支付已超时但仍处理中的资金交易(兜底定时任务用)
///
/// trade_type=normal, status=processing, 容器 pay_normal_order.expired_time &lt; now。
/// 条数由首参 [IPage] 经分页插件追加方言 limit勿在 SQL 写死 LIMIT。
@Select("""
SELECT t.*
FROM pay_trade t
WHERE t.trade_type = 'normal'
AND t.status = 'processing'
AND EXISTS (
SELECT 1 FROM pay_normal_order o
WHERE o.id = t.container_id
AND o.expired_time < #{now}
)
ORDER BY t.create_time ASC
""")
IPage<PayTrade> findNormalTimeoutTrades(IPage<PayTrade> page, @Param("now") OffsetDateTime now);
/// 查询网关支付已超时但仍处理中的资金交易(兜底定时任务用)
///
/// trade_type=gateway, status=processing, 容器 pay_gateway_order.expired_time &lt; now。
/// 条数由首参 [IPage] 经分页插件追加方言 limit勿在 SQL 写死 LIMIT。
@Select("""
SELECT t.*
FROM pay_trade t
WHERE t.trade_type = 'gateway'
AND t.status = 'processing'
AND EXISTS (
SELECT 1 FROM pay_gateway_order o
WHERE o.id = t.container_id
AND o.expired_time < #{now}
)
ORDER BY t.create_time ASC
""")
IPage<PayTrade> findGatewayTimeoutTrades(IPage<PayTrade> page, @Param("now") OffsetDateTime now);
}

View File

@@ -108,6 +108,21 @@ public class BaseManager<M extends MPJBaseMapper<T>, T> {
return new DaxLambdaQueryChainWrapper<>(getBaseMapper(), getEntityClass());
}
/// 按条件取有限条(方言无关,由分页插件按当前数据库方言生成 limit不查总数)
///
/// 用于超时扫描等「最多 N 条」场景,替代 `.last("limit N")` 写死方言片段。
///
/// @param size 最大条数
/// @param customizer 查询条件定制(eq/orderBy 等)
/// @return 最多 size 条记录
public List<T> listLimit(int size, Consumer<LambdaQueryChainWrapper<T>> customizer) {
var query = lambdaQuery();
customizer.accept(query);
Page<T> page = new Page<>(1, size);
page.setSearchCount(false);
return query.page(page).getRecords();
}
/// 取排序后第一条(方言无关,由分页插件按当前数据库方言生成 limit只查 1 条)
///
/// 适用于 findFirst* 场景(明确取首条,不校验唯一),区别于期望唯一的 [#lambdaQuery]...one()。
@@ -115,11 +130,7 @@ public class BaseManager<M extends MPJBaseMapper<T>, T> {
/// @param customizer 查询条件定制(eq/orderBy 等)
/// @return 第一条的 Optional 包装,无数据返回 empty
public Optional<T> firstOpt(Consumer<LambdaQueryChainWrapper<T>> customizer) {
var query = lambdaQuery();
customizer.accept(query);
Page<T> page = new Page<>(1, 1);
page.setSearchCount(false);
List<T> records = query.page(page).getRecords();
List<T> records = listLimit(1, customizer);
return records.isEmpty() ? Optional.empty() : Optional.of(records.getFirst());
}

View File

@@ -14,11 +14,14 @@ import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.experimental.UtilityClass;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/// # 注解参数查询生成器
@@ -26,6 +29,12 @@ import java.util.stream.Collectors;
@UtilityClass
public class AnnotationQueryGenerator {
private final Logger log = LoggerFactory.getLogger(AnnotationQueryGenerator.class);
/// 排序字段标识符白名单: column 或 table.column禁止空格/分号等注入片段)
private final Pattern SAFE_SORT_FIELD = Pattern.compile(
"^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$");
/// 生成查询条件 (根据实体对象生成), 生成的多个查询条件之间用And连接
/// @param queryParams 参数
/// @param clazz 数据库Entity类
@@ -205,12 +214,18 @@ public class AnnotationQueryGenerator {
return;
}
for (SortParam queryOrder : queryOrders) {
if (queryOrder.isUnderLine()) {
queryWrapper.orderBy(StrUtil.isNotBlank(queryOrder.getSortField()), queryOrder.isAsc(), StrUtil.toUnderlineCase(queryOrder.getSortField()));
String sortField = queryOrder.getSortField();
if (StrUtil.isBlank(sortField)) {
continue;
}
else {
queryWrapper.orderBy(StrUtil.isNotBlank(queryOrder.getSortField()), queryOrder.isAsc(), queryOrder.getSortField());
// 先做命名转换, 再校验最终落入 ORDER BY 的标识符
String column = queryOrder.isUnderLine() ? StrUtil.toUnderlineCase(sortField) : sortField;
if (!SAFE_SORT_FIELD.matcher(column).matches()) {
// 非法排序字段跳过, 避免 ORDER BY 注入导致列表页直接 500
log.warn("忽略非法排序字段: {}", sortField);
continue;
}
queryWrapper.orderBy(true, queryOrder.isAsc(), column);
}
}

View File

@@ -46,13 +46,11 @@ public class UserProtocolVersionManager extends BaseManager<UserProtocolVersionM
if (published.isPresent()) {
return published;
}
return this.lambdaQuery()
return firstOpt(q -> q
.eq(UserProtocolVersion::getProtocolId, protocolId)
.eq(UserProtocolVersion::getLanguage, language)
.eq(UserProtocolVersion::getStatus, "ARCHIVED")
.orderByDesc(UserProtocolVersion::getVersionNo)
.last("LIMIT 1")
.oneOpt();
.orderByDesc(UserProtocolVersion::getVersionNo));
}
/// 查询某协议下所有版本(级联删除/复制用)