diff --git a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppManager.java b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppManager.java index a0e1e600e..c76fddde7 100644 --- a/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppManager.java +++ b/daxpay-channel/daxpay-channel-douyin/src/main/java/cn/daxpay/open/channel/douyin/dao/direct/DouyinDirectAppManager.java @@ -39,23 +39,19 @@ public class DouyinDirectAppManager extends BaseManager 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 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 查询应用(支付/回调,租户内) diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/dao/GatewayPayOrderManager.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/dao/GatewayPayOrderManager.java index 914fce6b1..773e61a2d 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/dao/GatewayPayOrderManager.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/dao/GatewayPayOrderManager.java @@ -61,13 +61,11 @@ public class GatewayPayOrderManager extends BaseManager 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)); } } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/dao/PayTradeManager.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/dao/PayTradeManager.java index 5838b1777..524c6bd3f 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/dao/PayTradeManager.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/dao/PayTradeManager.java @@ -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 { } /// 查询网关支付已超时但仍处理中的资金交易(兜底) - /// expiredTime 在容器(pay_gateway_order)上, 用子查询关联 + /// + /// expiredTime 在容器(pay_gateway_order)上, SQL 见 [PayTradeMapper#findGatewayTimeoutTrades]。 + /// 跨租户扫描(定时任务无 HTTP 上下文), 单次上限 500 防积压爆量(分页插件生成方言 limit)。 @IgnoreTenant public List 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 page = new Page<>(1, 500); + page.setSearchCount(false); + return getBaseMapper().findGatewayTimeoutTrades(page, now).getRecords(); } /// 分页查询(管理端), 默认按创建时间倒序 @@ -88,18 +84,14 @@ public class PayTradeManager extends BaseManager { /// 查询普通支付已超时但仍处理中的资金交易(兜底定时任务用) /// - /// 条件: 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 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 page = new Page<>(1, 500); + page.setSearchCount(false); + return getBaseMapper().findNormalTimeoutTrades(page, now).getRecords(); } /// 根据id进行更新,失败时抛出异常 diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/dao/PayTradeMapper.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/dao/PayTradeMapper.java index cffe9ef89..a8c80298b 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/dao/PayTradeMapper.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/order/dao/PayTradeMapper.java @@ -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 { + + /// 查询普通支付已超时但仍处理中的资金交易(兜底定时任务用) + /// + /// trade_type=normal, status=processing, 容器 pay_normal_order.expired_time < 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 findNormalTimeoutTrades(IPage page, @Param("now") OffsetDateTime now); + + /// 查询网关支付已超时但仍处理中的资金交易(兜底定时任务用) + /// + /// trade_type=gateway, status=processing, 容器 pay_gateway_order.expired_time < 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 findGatewayTimeoutTrades(IPage page, @Param("now") OffsetDateTime now); } diff --git a/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/impl/BaseManager.java b/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/impl/BaseManager.java index 0afd4b251..232e6a850 100644 --- a/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/impl/BaseManager.java +++ b/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/impl/BaseManager.java @@ -108,6 +108,21 @@ public class BaseManager, T> { return new DaxLambdaQueryChainWrapper<>(getBaseMapper(), getEntityClass()); } + /// 按条件取有限条(方言无关,由分页插件按当前数据库方言生成 limit,不查总数) + /// + /// 用于超时扫描等「最多 N 条」场景,替代 `.last("limit N")` 写死方言片段。 + /// + /// @param size 最大条数 + /// @param customizer 查询条件定制(eq/orderBy 等) + /// @return 最多 size 条记录 + public List listLimit(int size, Consumer> customizer) { + var query = lambdaQuery(); + customizer.accept(query); + Page 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, T> { /// @param customizer 查询条件定制(eq/orderBy 等) /// @return 第一条的 Optional 包装,无数据返回 empty public Optional firstOpt(Consumer> customizer) { - var query = lambdaQuery(); - customizer.accept(query); - Page page = new Page<>(1, 1); - page.setSearchCount(false); - List records = query.page(page).getRecords(); + List records = listLimit(1, customizer); return records.isEmpty() ? Optional.empty() : Optional.of(records.getFirst()); } diff --git a/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/query/generator/AnnotationQueryGenerator.java b/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/query/generator/AnnotationQueryGenerator.java index 784cd212d..7dadfd349 100644 --- a/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/query/generator/AnnotationQueryGenerator.java +++ b/daxpay-platform/daxpay-platform-common/common-mybatis-plus/src/main/java/cn/daxpay/open/platform/common/mybatisplus/query/generator/AnnotationQueryGenerator.java @@ -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); } } diff --git a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/dao/protocol/UserProtocolVersionManager.java b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/dao/protocol/UserProtocolVersionManager.java index 849334c64..453281880 100644 --- a/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/dao/protocol/UserProtocolVersionManager.java +++ b/daxpay-platform/daxpay-platform-service/service-system/src/main/java/cn/daxpay/open/platform/system/dao/protocol/UserProtocolVersionManager.java @@ -46,13 +46,11 @@ public class UserProtocolVersionManager extends BaseManager q .eq(UserProtocolVersion::getProtocolId, protocolId) .eq(UserProtocolVersion::getLanguage, language) .eq(UserProtocolVersion::getStatus, "ARCHIVED") - .orderByDesc(UserProtocolVersion::getVersionNo) - .last("LIMIT 1") - .oneOpt(); + .orderByDesc(UserProtocolVersion::getVersionNo)); } /// 查询某协议下所有版本(级联删除/复制用)