交易、订单轮询策略(能用数据库轮询解决的不用Redis,能用Redis解决的不用消息队列)

1、传统定时轮询数据库、然后查询渠道

优点

  1. 实现简单:代码直观,易于理解

  2. 无外部依赖:只依赖数据库

  3. 数据一致性强:直接操作数据库,状态一致

  4. 排查问题容易:SQL可查询所有待处理数据

缺点

  1. 时间精度低:最快30秒延迟(轮询间隔)

  2. 数据库压力大:频繁全表/索引扫描

  3. 实时性差:可能延迟30秒才发现超时

  4. 资源浪费:无任务时也在空转

  5. 难以扩展:多实例可能导致重复处理

适用场景

  • 对实时性要求不高(分钟级)

  • 数据量小(< 10万)

  • 查询条件简单

  • 低成本项目

2、消息队列

优点

  1. 实时性好:精确到毫秒级触发

  2. 解耦性强:订单服务和超时处理服务分离

  3. 可削峰填谷:突发流量可缓冲处理

  4. 支持重试:消费失败可重新投递

  5. 分布式友好:天然支持多消费者

缺点

  1. 复杂度高:引入消息中间件,运维复杂

  2. 消息可能丢失:需要额外保证机制

  3. 顺序问题:需要特殊处理消息顺序

  4. 延迟精度有限:RabbitMQ延迟消息有精度限制

  5. 数据一致性:需要处理"本地事务+消息"一致性问题

适用场景

  • 分布式系统,服务解耦

  • 需要精确延迟触发

  • 流量波动大的场景

  • 需要异步处理的业务

3、redis zset

优点

  1. 性能极高:Redis内存操作,O(log N)

  2. 实时性好:可达到秒级甚至毫秒级精度

  3. 支持大量数据:内存存储,可处理百万级任务

  4. 灵活性高:支持动态调整延迟时间

  5. 分布式协调:可作为分布式调度中心

缺点

  1. 数据易失:Redis故障可能丢失数据(需持久化)

  2. 内存占用:大数据量时占用较多内存

  3. 可靠性依赖:依赖Redis的可用性

  4. 需要轮询:仍需要定时扫描(间隔可很短)

  5. 复杂状态管理:状态需在数据库中维护

适用场景

  • 高性能要求的延迟任务

  • 短周期、高频次的定时任务

  • 需要动态调整时间的场景

  • 已使用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);
        }
    }
}
相关推荐
周某人姓周2 小时前
sqlilabs靶场通关详解
数据库·mysql·安全·网络安全
ZeroNews内网穿透2 小时前
远程访问SQLITE-WEB服务
数据库·sqlite
霖霖总总2 小时前
[小技巧41]InnoDB 如何判断一行数据是否可见?MVCC 可见性机制深度解析
数据库·mysql
偷星星的贼113 小时前
数据分析与科学计算
jvm·数据库·python
Suchadar3 小时前
数据库DATABSE——sql server
数据库
梦茹^_^4 小时前
flask框架(笔记一次性写完)
redis·python·flask·cookie·session
檀越剑指大厂4 小时前
迁移之路的隐形陷阱:破解Oracle数据库国产化替代的核心痛点与策略
数据库·oracle
panzer_maus4 小时前
Redis简单介绍(3)-持久化的实现
java·redis·mybatis
wWYy.4 小时前
详解redis(1)
数据库·redis·缓存