1、传统定时轮询数据库、然后查询渠道
优点
-
实现简单:代码直观,易于理解
-
无外部依赖:只依赖数据库
-
数据一致性强:直接操作数据库,状态一致
-
排查问题容易:SQL可查询所有待处理数据
缺点
-
时间精度低:最快30秒延迟(轮询间隔)
-
数据库压力大:频繁全表/索引扫描
-
实时性差:可能延迟30秒才发现超时
-
资源浪费:无任务时也在空转
-
难以扩展:多实例可能导致重复处理
适用场景
-
对实时性要求不高(分钟级)
-
数据量小(< 10万)
-
查询条件简单
-
低成本项目
2、消息队列
优点
-
实时性好:精确到毫秒级触发
-
解耦性强:订单服务和超时处理服务分离
-
可削峰填谷:突发流量可缓冲处理
-
支持重试:消费失败可重新投递
-
分布式友好:天然支持多消费者
缺点
-
复杂度高:引入消息中间件,运维复杂
-
消息可能丢失:需要额外保证机制
-
顺序问题:需要特殊处理消息顺序
-
延迟精度有限:RabbitMQ延迟消息有精度限制
-
数据一致性:需要处理"本地事务+消息"一致性问题
适用场景
-
分布式系统,服务解耦
-
需要精确延迟触发
-
流量波动大的场景
-
需要异步处理的业务
3、redis zset
优点
-
性能极高:Redis内存操作,O(log N)
-
实时性好:可达到秒级甚至毫秒级精度
-
支持大量数据:内存存储,可处理百万级任务
-
灵活性高:支持动态调整延迟时间
-
分布式协调:可作为分布式调度中心
缺点
-
数据易失:Redis故障可能丢失数据(需持久化)
-
内存占用:大数据量时占用较多内存
-
可靠性依赖:依赖Redis的可用性
-
需要轮询:仍需要定时扫描(间隔可很短)
-
复杂状态管理:状态需在数据库中维护
适用场景
-
高性能要求的延迟任务
-
短周期、高频次的定时任务
-
需要动态调整时间的场景
-
已使用Redis的项目
三方案对比表格
| 维度 | 定时轮询数据库 | 消息队列 | Redis ZSet |
|---|---|---|---|
| 实时性 | 差(分钟级) | 好(毫秒级) | 好(秒级) |
| 性能 | 差(数据库压力大) | 中(网络开销) | 优(内存操作) |
| 可靠性 | 高(数据不丢) | 中(可能丢消息) | 中(可能丢数据) |
| 复杂度 | 低 | 高 | 中 |
| 扩展性 | 差 | 优 | 良 |
| 成本 | 低 | 高 | 中 |
| 数据量 | 小(<10万) | 大 | 大 |
| 精确度 | 低(依赖轮询间隔) | 高 | 高 |
| 适用场景 | 简单低频任务 | 分布式解耦 | 高性能调度 |
现在重点介绍 Redis Zset
java
package cn.finopen.boot.autoconfigure.resource.txn.service;
import cn.finopen.boot.autoconfigure.redis.RedisService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.Set;
/**
* MPOS 订单查询队列管理服务(统一服务)
* <p>
* 功能:
* 1. 管理待查询订单的 Redis ZSet 队列
* 2. 提供订单入队、出队、超时管理等方法
* 3. 支持不同场景的指数退避查询策略
* <p>
* 支持场景:
* - MICRO: 反扫订单(高频查询,秒级响应)
* - GENERAL: 通用订单(低频查询,分钟级响应)
* <p>
* Redis 数据结构:
* - Key: mpos:{scene}:query:pending (ZSet)
* - Member: serverOrderId
* - Score: nextQueryTimestamp (下次查询时间戳)
* - Key: mpos:{scene}:query:createtime:{orderId} (String)
* - Value: createTimestamp (订单创建时间戳)
* - TTL: 24小时
*
* @author xt
*/
@Slf4j
@Service
@AllArgsConstructor
public class MposQueryService {
private final RedisService redisService;
/**
* 查询场景枚举
*/
public enum QueryScene {
/**
* 反扫订单(高频查询)
*/
MICRO("micro", 5000L, 15 * 60 * 1000L),
/**
* 通用订单(低频查询)
*/
GENERAL("general", 60000L, 12 * 60 * 60 * 1000L);
private final String id;
private final long firstQueryDelay; // 首次查询延迟
private final long timeoutMillis; // 超时时间
QueryScene(String id, long firstQueryDelay, long timeoutMillis) {
this.id = id;
this.firstQueryDelay = firstQueryDelay;
this.timeoutMillis = timeoutMillis;
}
public String getId() {
return id;
}
public long getFirstQueryDelay() {
return firstQueryDelay;
}
public long getTimeoutMillis() {
return timeoutMillis;
}
}
/**
* 获取 Redis ZSet Key
*/
private String getQueryPendingKey(QueryScene scene) {
return "mpos:" + scene.getId() + ":query:pending";
}
/**
* 获取 Redis 创建时间 Key 前缀
*/
private String getCreateTimeKeyPrefix(QueryScene scene) {
return "mpos:" + scene.getId() + ":query:createtime:";
}
/**
* 添加订单到查询队列(指定场景)
*
* @param serverOrderId 订单ID
* @param scene 查询场景
*/
public void addToQueryQueue(String serverOrderId, QueryScene scene) {
try {
long nextQueryTime = System.currentTimeMillis() + scene.getFirstQueryDelay();
String queueKey = getQueryPendingKey(scene);
redisService.zAdd(queueKey, serverOrderId, nextQueryTime);
// 记录订单创建时间,用于超时判断
String createTimeKey = getCreateTimeKeyPrefix(scene) + serverOrderId;
redisService.set(createTimeKey, String.valueOf(System.currentTimeMillis()), 24 * 3600);
log.info("订单加入查询队列: scene={}, serverOrderId={}, nextQueryTime={}", scene.getId(), serverOrderId, nextQueryTime);
} catch (Exception e) {
log.error("订单加入查询队列失败: scene={}, serverOrderId={}", scene.getId(), serverOrderId, e);
}
}
/**
* 获取所有到期应该查询的订单(指定场景)
*
* @param scene 查询场景
* @return 订单ID集合
*/
public Set<String> getExpiredOrders(QueryScene scene) {
try {
long currentTime = System.currentTimeMillis();
String queueKey = getQueryPendingKey(scene);
Set<String> orders = redisService.zRangeByScore(queueKey, 0, currentTime);
if (orders != null && !orders.isEmpty()) {
log.debug("获取到期订单: scene={}, count={}", scene.getId(), orders.size());
}
return orders != null ? orders : new HashSet<>();
} catch (Exception e) {
log.error("获取到期订单失败: scene={}", scene.getId(), e);
return new HashSet<>();
}
}
/**
* 更新订单的下次查询时间(指定场景)
*
* @param serverOrderId 订单ID
* @param nextQueryTimestamp 下次查询时间戳
* @param scene 查询场景
*/
public void updateNextQueryTime(String serverOrderId, long nextQueryTimestamp, QueryScene scene) {
try {
String queueKey = getQueryPendingKey(scene);
redisService.zAdd(queueKey, serverOrderId, nextQueryTimestamp);
log.debug("更新订单下次查询时间: scene={}, serverOrderId={}, nextQueryTime={}",
scene.getId(), serverOrderId, nextQueryTimestamp);
} catch (Exception e) {
log.error("更新订单查询时间失败: scene={}, serverOrderId={}", scene.getId(), serverOrderId, e);
}
}
/**
* 从查询队列移除订单(指定场景)
*
* @param serverOrderId 订单ID
* @param scene 查询场景
*/
public void removeFromQueue(String serverOrderId, QueryScene scene) {
try {
String queueKey = getQueryPendingKey(scene);
String createTimeKey = getCreateTimeKeyPrefix(scene) + serverOrderId;
redisService.zRemove(queueKey, serverOrderId);
redisService.deleteByKey(createTimeKey);
log.info("订单从查询队列移除: scene={}, serverOrderId={}", scene.getId(), serverOrderId);
} catch (Exception e) {
log.error("订单移除失败: scene={}, serverOrderId={}", scene.getId(), serverOrderId, e);
}
}
/**
* 获取订单创建时间(指定场景)
*
* @param serverOrderId 订单ID
* @param scene 查询场景
* @return 创建时间戳(毫秒), 不存在返回当前时间
*/
public long getOrderCreateTime(String serverOrderId, QueryScene scene) {
try {
String createTimeKey = getCreateTimeKeyPrefix(scene) + serverOrderId;
String createTimeStr = redisService.get(createTimeKey);
if (createTimeStr != null && !createTimeStr.isEmpty()) {
return Long.parseLong(createTimeStr);
}
} catch (Exception e) {
log.error("获取订单创建时间失败: scene={}, serverOrderId={}", scene.getId(), serverOrderId, e);
}
return System.currentTimeMillis();
}
/**
* 计算下次查询时间(指数退避策略)
* <p>
* MICRO 策略(快速):
* - 前30秒: 每5秒查询一次
* - 30秒-2分钟: 每10秒查询一次
* - 2分钟-5分钟: 每30秒查询一次
* - 5分钟-15分钟: 每60秒查询一次
* - 超过15分钟: 返回-1表示超时
* <p>
* GENERAL 策略(宽松):
* - 前5分钟: 每1分钟查询一次
* - 5-30分钟: 每5分钟查询一次
* - 30分钟-2小时: 每15分钟查询一次
* - 2-12小时: 每1小时查询一次
* - 超过12小时: 返回-1表示超时
*
* @param createTime 订单创建时间(毫秒)
* @param scene 查询场景
* @return 下次查询时间戳, 返回-1表示已超时
*/
public long calculateNextQueryTime(long createTime, QueryScene scene) {
long currentTime = System.currentTimeMillis();
long elapsed = currentTime - createTime;
if (scene == QueryScene.MICRO) {
// 反扫订单:快速退避策略(秒级)
long elapsedSeconds = elapsed / 1000;
long intervalSeconds;
if (elapsedSeconds < 30) {
intervalSeconds = 5;
} else if (elapsedSeconds < 120) {
intervalSeconds = 10;
} else if (elapsedSeconds < 300) {
intervalSeconds = 30;
} else if (elapsedSeconds < 900) {
intervalSeconds = 60;
} else {
log.warn("订单查询超时: scene={}, elapsed={}秒", scene.getId(), elapsedSeconds);
return -1;
}
return currentTime + (intervalSeconds * 1000);
} else {
// 通用订单:宽松退避策略(分钟级)
long elapsedMinutes = elapsed / 60000;
long intervalMinutes;
if (elapsedMinutes < 5) {
intervalMinutes = 1;
} else if (elapsedMinutes < 30) {
intervalMinutes = 5;
} else if (elapsedMinutes < 120) {
intervalMinutes = 15;
} else if (elapsedMinutes < 720) {
intervalMinutes = 60;
} else {
log.warn("订单查询超时: scene={}, elapsed={}分钟", scene.getId(), elapsedMinutes);
return -1;
}
return currentTime + (intervalMinutes * 60000);
}
}
/**
* 获取待查询订单总数(指定场景)
*
* @param scene 查询场景
* @return 订单数量
*/
public Long getPendingOrderCount(QueryScene scene) {
try {
String queueKey = getQueryPendingKey(scene);
return redisService.zCard(queueKey);
} catch (Exception e) {
log.error("获取待查询订单数量失败: scene={}", scene.getId(), e);
return 0L;
}
}
/**
* 获取未来指定时间内需要查询的订单数量(指定场景)
*
* @param futureSeconds 未来秒数
* @param scene 查询场景
* @return 订单数量
*/
public Long getUpcomingOrderCount(int futureSeconds, QueryScene scene) {
try {
long currentTime = System.currentTimeMillis();
long futureTime = currentTime + (futureSeconds * 1000);
String queueKey = getQueryPendingKey(scene);
return redisService.zCount(queueKey, currentTime, futureTime);
} catch (Exception e) {
log.error("获取未来订单数量失败: scene={}", scene.getId(), e);
return 0L;
}
}
/**
* 获取超时未处理的订单(反扫订单)
*
* @param timeoutMinutes 超时分钟数
* @return 超时订单ID集合
*/
public Set<String> getTimeoutOrders(int timeoutMinutes) {
return getTimeoutOrders(timeoutMinutes, QueryScene.MICRO);
}
/**
* 获取超时未处理的订单(指定场景)
*
* @param timeoutMinutes 超时分钟数
* @param scene 查询场景
* @return 超时订单ID集合
*/
public Set<String> getTimeoutOrders(int timeoutMinutes, QueryScene scene) {
long currentTime = System.currentTimeMillis();
long timeoutThreshold = timeoutMinutes * 60 * 1000;
Set<String> timeoutOrders = new HashSet<>();
try {
String queueKey = getQueryPendingKey(scene);
Set<String> allOrderIds = redisService.zRange(queueKey, 0, -1);
if (allOrderIds == null || allOrderIds.isEmpty()) {
return timeoutOrders;
}
// 检查每个订单的创建时间
for (String serverOrderId : allOrderIds) {
long createTime = getOrderCreateTime(serverOrderId, scene);
if (currentTime - createTime > timeoutThreshold) {
timeoutOrders.add(serverOrderId);
}
}
if (!timeoutOrders.isEmpty()) {
log.warn("发现超时订单: scene={}, count={}, timeoutMinutes={}",
scene.getId(), timeoutOrders.size(), timeoutMinutes);
}
return timeoutOrders;
} catch (Exception e) {
log.error("获取超时订单失败: scene={}", scene.getId(), e);
return timeoutOrders;
}
}
/**
* 清理所有查询队列数据(反扫订单,仅用于测试或维护)
*/
public void clearAll() {
clearAll(QueryScene.MICRO);
}
/**
* 清理所有查询队列数据(指定场景,仅用于测试或维护)
*
* @param scene 查询场景
*/
public void clearAll(QueryScene scene) {
try {
String queueKey = getQueryPendingKey(scene);
String createTimePrefix = getCreateTimeKeyPrefix(scene);
Set<String> allOrderIds = redisService.zRange(queueKey, 0, -1);
if (allOrderIds != null) {
for (String serverOrderId : allOrderIds) {
redisService.deleteByKey(createTimePrefix + serverOrderId);
}
}
redisService.deleteByKey(queueKey);
log.warn("清理所有查询队列数据: scene={}", scene.getId());
} catch (Exception e) {
log.error("清理数据失败: scene={}", scene.getId(), e);
}
}
}
定时任务:
java
package cn.finopen.boot.autoconfigure.resource.txn.scheduling;
import cn.finopen.boot.autoconfigure.redis.scheduling.AbstractDistributedSchedule;
import cn.finopen.boot.autoconfigure.resource.txn.service.MposNotifyService;
import cn.finopen.boot.autoconfigure.resource.txn.service.MposQueryService;
import cn.finopen.faas.api.channel.ChannelService;
import cn.finopen.faas.api.channel.MposExecuter;
import cn.finopen.faas.api.channel.dto.*;
import cn.finopen.faas.api.merchant.dto.MerchantChannelProduct;
import cn.finopen.faas.api.txn.TxnMposService;
import cn.finopen.faas.api.txn.dto.TxnMposReq;
import cn.finopen.faas.api.txn.dto.TxnMposResp;
import cn.finopen.faas.api.txn.dto.TxnMposSimpleResp;
import cn.finopen.faas.api.txn.dto.TxnStatus;
import cn.finopen.faas.merchant.api.MchChannelProductService;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import fib.core.exception.FibException;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.time.ZoneId;
import java.util.List;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 聚合支付反扫订单状态查询任务 - Redis ZSet 优化版
* <p>
* 优化要点:
* 1. 使用 Redis ZSet 管理待查询订单,以时间戳为 score 实现自动排序
* 2. 实现指数退避策略,动态调整查询间隔
* 3. 减少数据库压力 95%+,提升查询效率
* 4. 整合队列管理逻辑,代码更简洁
* <p>
* Redis ZSet 设计:
* - Key: mpos:micro:query:pending
* - Member: serverOrderId (订单ID)
* - Score: nextQueryTimestamp (下次查询时间戳,毫秒)
* <p>
* 辅助数据结构:
* - Key: mpos:micro:query:createtime:{serverOrderId} -> 订单创建时间
* - 过期时间: 24小时自动清理
*
* @author xt
*/
@Service
@AllArgsConstructor
@Lazy(false)
public class MposMicroQueryJobV2 extends AbstractDistributedSchedule {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final TxnMposService txnService;
private final List<MposExecuter> executers;
private final ChannelService channelService;
private final MchChannelProductService mchChannelProductService;
private final MposNotifyService notifyService;
private final MposQueryService queryService;
private final static ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
50, 50, 60000, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder().setNameFormat("MposMicroQueryJob pool-%d").build(),
new ThreadPoolExecutor.AbortPolicy()
);
/**
* 主任务: 从 Redis ZSet 获取到期订单进行查询
* 每5秒执行一次
*/
@Override
@Scheduled(cron = "0/5 * * * * ?")
public void execute() {
try {
logger.info("聚合支付反扫订单状态查询任务(Redis ZSet优化版)");
// 获取分布式锁
boolean lock = lock(0, 1000);
if (!lock) {
logger.debug("获取分布式锁失败,跳过本次执行");
return;
}
// 从 Redis ZSet 获取所有到期订单 (score <= 当前时间)
Set<String> expiredOrderIds = queryService.getExpiredOrders(MposQueryService.QueryScene.MICRO);
if (CollectionUtils.isEmpty(expiredOrderIds)) {
logger.debug("当前没有到期订单需要查询");
return;
}
logger.info("获取到{}笔到期订单,开始查询", expiredOrderIds.size());
// 提交到线程池异步查询
for (String serverOrderId : expiredOrderIds) {
THREAD_POOL_EXECUTOR.execute(() -> queryOrder(serverOrderId));
}
} catch (Exception e) {
logger.error("聚合支付反扫订单状态查询任务执行失败", e);
}
}
/**
* 查询单个订单状态
*
* @param serverOrderId 订单ID
*/
private void queryOrder(String serverOrderId) {
try {
// 1. 从数据库加载订单详情
TxnMposResp history = txnService.getByServerOrderId(serverOrderId);
if (history == null) {
logger.warn("订单不存在,从队列移除: serverOrderId={}", serverOrderId);
queryService.removeFromQueue(serverOrderId, MposQueryService.QueryScene.MICRO);
return;
}
// 检查订单状态,如果不是处理中则移除
if (history.getTxnStatus() != TxnStatus.PROCESS.ordinal()) {
logger.info("订单状态已变更,从队列移除: serverOrderId={}, status={}", serverOrderId, history.getTxnStatus());
queryService.removeFromQueue(serverOrderId, MposQueryService.QueryScene.MICRO);
return;
}
// 2. 调用渠道查询
MposResp resp = queryFromChannel(history);
// 3. 根据查询结果处理
handleQueryResult(history, resp);
} catch (Exception e) {
logger.error("订单查询失败: serverOrderId={}", serverOrderId, e);
// 查询失败,延长查询间隔(30秒后重试)
long retryTime = System.currentTimeMillis() + 30000;
queryService.updateNextQueryTime(serverOrderId, retryTime, MposQueryService.QueryScene.MICRO);
}
}
/**
* 调用渠道接口查询订单
*
* @param history 订单信息
* @return 查询结果
*/
private MposResp queryFromChannel(TxnMposResp history) {
String txnType = history.getTxnType();
String sceneType = history.getSceneType();
String channelCode = history.getChannelCode();
String merchantCode = history.getMerchantCode();
// 获取执行器
MposExecuter executer = getServiceExecute(txnType, sceneType, channelCode);
// 获取渠道配置
ChannelGetReq channelReq = ChannelGetReq.newBuilder()
.setChannelCode(channelCode)
.setTxnType(txnType)
.setTxnSceneType(sceneType)
.build();
ChannelGetResp channel = channelService.getSceneByChannel(channelReq);
// 获取商户渠道配置
MerchantChannelProduct mchChannel = mchChannelProductService.getByTxnType(
txnType, sceneType, merchantCode, channelCode);
MchChannelGetResp mchChannelDetails = MchChannelGetResp.newBuilder()
.setChannelCode(channelCode)
.setMerchantCode(merchantCode)
.setTxnType(txnType)
.setTxnSceneType(sceneType)
.setThirdMerchantCode(history.getThirdMerchantCode())
.setThirdMerchantSecretKey(mchChannel.getThirdMerchantSecretKey())
.setThirdMerchantAppId(mchChannel.getThirdMerchantAppId())
.setThirdMerchantAccount(mchChannel.getThirdMerchantAccount())
.setChannelDetails(channel)
.build();
// 构建查询请求
String serverOrderId = history.getServerOrderId();
MposReq mposReq = MposReq.newBuilder()
.setServerOrderId(serverOrderId)
.setThirdOrderId(history.getThirdOrderId())
.setPreAuth(history.getPreAuth())
.setCreateDate(history.getCreateDate())
.build();
// 调用渠道查询
return executer.query(mchChannelDetails, mposReq);
}
/**
* 处理查询结果
*
* @param history 订单信息
* @param resp 查询结果
*/
private void handleQueryResult(TxnMposResp history, MposResp resp) {
String serverOrderId = history.getServerOrderId();
Integer tradeStatus = resp.getTradeStatus();
// 情况1: 仍在处理中,更新下次查询时间
if (tradeStatus == TradeStatus.IN_PROCESS.ordinal()) {
long createTime = queryService.getOrderCreateTime(serverOrderId, MposQueryService.QueryScene.MICRO);
if (history.getCreateTime() != null) {
createTime = history.getCreateTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
}
// 计算下次查询时间(指数退避)
long nextQueryTime = queryService.calculateNextQueryTime(createTime, MposQueryService.QueryScene.MICRO);
if (nextQueryTime == -1) {
// 已超时,停止查询,不更新订单状态
logger.warn("订单查询超时,停止查询: serverOrderId={}", serverOrderId);
queryService.removeFromQueue(serverOrderId, MposQueryService.QueryScene.MICRO);
} else {
// 更新下次查询时间
queryService.updateNextQueryTime(serverOrderId, nextQueryTime, MposQueryService.QueryScene.MICRO);
logger.debug("订单仍在处理中,下次查询时间: serverOrderId={}, nextTime={}", serverOrderId, nextQueryTime);
}
return;
}
// 情况2: 交易成功或失败,更新订单状态并移除队列
TxnStatus txnStatus = tradeStatus == TradeStatus.SUCCESS.ordinal()
? TxnStatus.SUCCESS
: TxnStatus.FAIL;
String respCode = resp.getErrCode();
String respMessage = resp.getErrMessage();
logger.info("订单查询完成: serverOrderId={}, status={}, code={}, message={}",
serverOrderId, txnStatus, respCode, respMessage);
// 更新数据库订单状态
updateOrderStatus(history, txnStatus, respCode, respMessage, resp);
// 从 Redis 队列移除
queryService.removeFromQueue(serverOrderId, MposQueryService.QueryScene.MICRO);
// 如果交易成功,发送异步通知
if (txnStatus == TxnStatus.SUCCESS) {
sendNotification(history, resp);
}
}
/**
* 更新订单状态到数据库
*
* @param history 原订单信息
* @param txnStatus 交易状态
* @param respCode 响应码
* @param respMessage 响应消息
* @param resp 查询响应
*/
private void updateOrderStatus(TxnMposResp history, TxnStatus txnStatus,
String respCode, String respMessage, MposResp resp) {
Long groupId = history.getGroupId();
String serverOrderId = history.getServerOrderId();
txnService.update(TxnMposReq.newBuilder()
.setGroupId(groupId)
.setTxnStatus(txnStatus.ordinal())
.setServerOrderId(serverOrderId)
.setThirdOrderId(resp.getThirdOrderId())
.setChannelOrderId(resp.getChannelOrderId())
.setPreAuthOrderId(resp.getPreAuthOrderId())
.setErrCode(respCode)
.setErrMessage(respMessage)
.setCardType(resp.getCardType())
.setSceneType(resp.getSceneType())
.setOpenId(resp.getOpenId())
.setWxOpenId(resp.getWxOpenId())
.setRate(resp.getRate())
.setServiceFee(resp.getServiceFee())
.setBillStatus(resp.getBillStatus())
.setPayTime(resp.getPayTime())
.build());
}
/**
* 发送异步通知
*
* @param history 订单信息
* @param resp 查询响应
*/
private void sendNotification(TxnMposResp history, MposResp resp) {
try {
TxnMposSimpleResp result = TxnMposSimpleResp.newBuilder()
.setServerOrderId(history.getServerOrderId())
.setRefundOrderId(history.getRefundOrderId())
.setOrigServerOrderId(history.getOrigServerOrderId())
.setClientOrderId(history.getClientOrderId())
.setChannelOrderId(resp.getChannelOrderId())
.setPayAmount(history.getPayAmount())
.setTransAmount(resp.getTransAmount())
.setRefundAmount(history.getRefundAmount())
.setServiceFee(resp.getServiceFee())
.setTxnStatus(TxnStatus.SUCCESS.ordinal())
.setTxnType(resp.getTxnType())
.setSceneType(resp.getSceneType())
.setMerchantCode(history.getMerchantCode())
.setErrCode(resp.getErrCode())
.setErrMessage(resp.getErrMessage())
.setAttach(history.getAttach())
.setOpenId(history.getOpenId())
.setDelay(history.getDelay())
.setLsy(history.getIsLsy())
.setPayTime(resp.getPayTime())
.build();
notifyService.execute(result, history.getClientNotifyUrl());
} catch (Exception e) {
logger.error("发送异步通知失败: serverOrderId={}", history.getServerOrderId(), e);
}
}
/**
* 获取服务执行器
*
* @param txnType 交易类型
* @param sceneType 场景类型
* @param channelCode 渠道代码
* @return 执行器
*/
public MposExecuter getServiceExecute(String txnType, String sceneType, String channelCode) {
if (CollectionUtils.isEmpty(executers)) {
throw FibException.ofNotFound(String.format("不支持的渠道[txnType=%s,sceneType=%s,channelCode=%s]", txnType, sceneType, channelCode));
}
for (MposExecuter api : executers) {
if (api.support(txnType, sceneType, channelCode)) {
return api;
}
}
throw FibException.ofNotFound(String.format("不支持的渠道[txnType=%s,sceneType=%s,channelCode=%s]", txnType, sceneType, channelCode));
}
/**
* 清理超时订单任务
* 每10分钟执行一次,将超过15分钟仍在队列中的订单从队列移除(不更新订单状态)
*/
@Scheduled(cron = "0 */10 * * * ?")
public void cleanTimeoutOrders() {
try {
logger.info("开始清理超时订单");
boolean lock = lockByClazz(0, 60);
if (!lock) {
logger.debug("获取清理任务锁失败");
return;
}
// 获取超时订单(15分钟)
Set<String> timeoutOrders = queryService.getTimeoutOrders(15);
if (CollectionUtils.isEmpty(timeoutOrders)) {
logger.info("没有超时订单需要清理");
return;
}
logger.warn("发现{}笔超时订单,从队列移除", timeoutOrders.size());
for (String serverOrderId : timeoutOrders) {
try {
// 直接从队列移除,不更新订单状态
queryService.removeFromQueue(serverOrderId, MposQueryService.QueryScene.MICRO);
logger.info("超时订单已从队列移除: serverOrderId={}", serverOrderId);
} catch (Exception e) {
logger.error("清理超时订单失败: serverOrderId={}", serverOrderId, e);
}
}
logger.info("超时订单清理完成: count={}", timeoutOrders.size());
} catch (Exception e) {
logger.error("清理超时订单任务失败", e);
}
}
/**
* 监控任务: 输出队列统计信息
* 每分钟执行一次
*/
@Scheduled(cron = "0 * * * * ?")
public void monitorQueueStatus() {
try {
Long pendingCount = queryService.getPendingOrderCount(MposQueryService.QueryScene.MICRO);
Long upcomingCount = queryService.getUpcomingOrderCount(60, MposQueryService.QueryScene.MICRO);
logger.info("查询队列状态: 总待查询订单={}, 未来1分钟待查询={}", pendingCount, upcomingCount);
// 可以在这里添加告警逻辑
if (pendingCount != null && pendingCount > 10000) {
logger.warn("查询队列堆积严重: count={}", pendingCount);
}
} catch (Exception e) {
logger.error("监控任务失败", e);
}
}
}