【项目亮点四】支付订单超时处理与状态补偿机制设计

目录

一、订单超时处理与状态补偿机制

[1.1 为什么需要主动查询兜底](#1.1 为什么需要主动查询兜底)

[1.2 兜底方案设计 超时订单处理流程](#1.2 兜底方案设计 超时订单处理流程)

[1.3 订单超时处理方案对比](#1.3 订单超时处理方案对比)

[1.4 RabbitMQ延迟队列原理](#1.4 RabbitMQ延迟队列原理)

[1.5 常见状态补偿策略](#1.5 常见状态补偿策略)

二、使用RabbitMQ延迟队列实现订单超时自动取消功能

[2.1 RabbitMQ延迟队列时序图](#2.1 RabbitMQ延迟队列时序图)

[2.2 延迟队列配置说明](#2.2 延迟队列配置说明)

[2.3 MQ配置实现](#2.3 MQ配置实现)

[2.4 订单创建时发送延迟消息](#2.4 订单创建时发送延迟消息)

三、超时支付订单MQ处理和关闭订单设计实战

[3.1 消费者交互时序图](#3.1 消费者交互时序图)

[3.2 MQ监听器实现](#3.2 MQ监听器实现)

[3.3 超时订单处理逻辑](#3.3 超时订单处理逻辑)


一、订单超时处理与状态补偿机制

1.1 为什么需要主动查询兜底

  • 需求背景

    • 微信支付回调可能因网络问题丢失,需要自动关闭长时间未支付的订单

    • 用户创建订单后可能不支付,订单一直占用库存

    • 回调处理可能因系统异常失败,需要确保订单状态最终一致性

  • 核心作用

    • 补偿机制:回调丢失时主动查询补全状态

    • 一致性保障:确保订单状态与微信支付一致

    • 异常恢复:系统异常后恢复订单状态

    • 资源释放:超时未支付自动释放库存

1.2 兜底方案设计 超时订单处理流程

1.3 订单超时处理方案对比

方案 优点 缺点 适用场景
定时任务 实现简单 精度低,压力大 小规模系统
延迟队列 精度高,实时性好 依赖消息队列 中大型系统
Redis过期 性能好 可靠性差 高并发场景

1.4 RabbitMQ延迟队列原理

  • 关键点

    • 延迟队列本身没有消费者,只做消息暂存

    • 消息过期后自动变成"死信",路由到死信队列

    • 死信队列有消费者监听,执行实际业务逻辑

    • 通过 TTL + 死信交换机实现延迟功能(RabbitMQ 官方推荐方案)

1.5 常见状态补偿策略

微信状态 处理逻辑
SUCCESS 补偿更新为已支付状态
USERPAYING 暂不处理,等待下次扫描
NOTPAY/CLOSED/REVOKED 取消本地订单
查询失败 直接取消订单(保守策略)

二、使用RabbitMQ延迟队列实现订单超时自动取消功能

2.1 RabbitMQ延迟队列时序图

2.2 延迟队列配置说明

组件 名称 作用
TopicExchange order.event.exchange 消息交换机,负责路由消息
延迟队列 order.close.delay.queue 暂存消息,设置 TTL 过期时间,不被消费者监听
死信队列 order.close.queue 接收过期消息,被消费者监听处理
TTL 60秒(正常10分钟,方便测试才60秒) 消息在延迟队列中的存活时间

2.3 MQ配置实现

java 复制代码
package net.xdclass.config;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
@Slf4j
@Data
public class ProductOrderMQConfig {

    // 订单交换机
    private String orderEventExchange = "order.event.exchange";

    // 延迟队列(不能被消费者监听)
    private String orderCloseDelayQueue = "order.close.delay.queue";

    // 死信队列(被消费者监听)
    public static final String orderCloseQueue = "order.close.queue";

    // 延迟队列路由key
    private String orderCloseDelayRoutingKey = "order.close.delay.routing.key";

    // 死信队列路由key
    private String orderCloseRoutingKey = "order.close.delay.key";

    // 过期时间:1分钟
    private Integer ttl = 1000 * 60 ;

    /**
     * 配置 JSON 消息转换器,替代默认的 Java 原生序列化
     * SpringAMQP3.x默认使用 Java 原生序列化时,会校验反序列化白名单,ProductOrderDTO 不在白名单中导致反序列化失败。
     * 最佳解决方案是配置 Jackson2JsonMessageConverter,改用 JSON 序列化替代 Java 原生序列化。
     */
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    /**
     * 配置 RabbitTemplate,使用 JSON 消息转换器
     */
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(messageConverter());
        return rabbitTemplate;
    }

    /**
     * 创建交换机
     */
    @Bean
    public TopicExchange orderEventExchange() {
        return new TopicExchange(orderEventExchange, true, false);
    }

    /**
     * 延迟队列
     */
    @Bean
    public Queue orderCloseDelayQueue() {
        Map<String, Object> args = new HashMap<>(3);
        // 消息过期后转发的交换机
        args.put("x-dead-letter-exchange", orderEventExchange);
        // 消息过期后转发的路由key
        args.put("x-dead-letter-routing-key", orderCloseRoutingKey);
        // 消息过期时间
        args.put("x-message-ttl", ttl);
        return new Queue(orderCloseDelayQueue, true, false, false, args);
    }

    /**
     * 死信队列(普通队列,被监听)
     */
    @Bean
    public Queue orderCloseQueue() {
        return new Queue(orderCloseQueue, true, false, false);
    }

    /**
     * 延迟队列绑定
     */
    @Bean
    public Binding orderCloseDelayBinding() {
        return BindingBuilder.bind(orderCloseDelayQueue())
                .to(orderEventExchange())
                .with(orderCloseDelayRoutingKey);
    }

    /**
     * 死信队列绑定
     */
    @Bean
    public Binding orderCloseBinding() {
        return BindingBuilder.bind(orderCloseQueue())
                .to(orderEventExchange())
                .with(orderCloseRoutingKey);
    }
}

2.4 订单创建时发送延迟消息

java 复制代码
 /**
     * 根据支付方式路由到对应的支付逻辑
     */
    private JsonData handlePayment(ProductOrderDO orderDO,String payType){
        ProductOrderDTO productOrderDTO = SpringBeanUtil.copyProperties(orderDO, ProductOrderDTO.class);
        rabbitTemplate.convertAndSend(rabbitMQConfig.getOrderEventExchange(), rabbitMQConfig.getOrderCloseDelayRoutingKey(), productOrderDTO);
        if(PaymentEnum.WECHAT_PAY.name().equals(payType)){
            //微信支付
            return doWechatNativePay(orderDO);
        }else if(PaymentEnum.ALI_PAY.name().equals(payType)){
            return JsonData.buildError("支付宝支付暂时未实现");
        }
        return JsonData.buildError("不支持的支付方式");
    }

三、超时支付订单MQ处理和关闭订单设计实战

需求:支付回调丢失导致的状态不一致

  • 场景:用户支付成功,但微信支付回调因网络问题未到达系统

  • 效果:即使回调丢失,超时检查时也能发现并补偿更新

  • 通过主动查询补偿,确保本地与第三方支付状态一致

3.1 消费者交互时序图

3.2 MQ监听器实现

java 复制代码
@Component
@Slf4j
@RabbitListener(queues = ProductOrderMQConfig.orderCloseQueue)
public class ProductOrderMQListener {

    @Autowired
    private ProductOrderService productOrderService;

    @RabbitHandler
    public void productOrderHandler(ProductOrderDTO productOrderDTO,
                                    Message message, Channel channel) {
        log.info("监听到超时未支付订单消息,订单号:{}",
                productOrderDTO.getOutTradeNo());

        try {
            // 处理超时未支付订单
            productOrderService.handleTimeoutOrder(productOrderDTO);

            log.info("超时订单处理成功,订单号:{}",
                    productOrderDTO.getOutTradeNo());

            // 手动ACK确认消息
            channel.basicAck(message.getMessageProperties()
                    .getDeliveryTag(), false);

        } catch (Exception e) {
            log.error("处理超时订单失败,订单号:{}",
                    productOrderDTO.getOutTradeNo(), e);

            try {
                // 拒绝消息,不重新入队
                channel.basicNack(message.getMessageProperties()
                        .getDeliveryTag(), false, false);
            } catch (Exception ex) {
                log.error("消息拒绝失败", ex);
            }
        }
    }
}

3.3 超时订单处理逻辑

java 复制代码
    @Override
    public void handleTimeoutOrder(ProductOrderDTO productOrderDTO) {
        String outTradeNo = productOrderDTO.getOutTradeNo();
        log.info("开始处理超时未支付订单,订单号:{}", outTradeNo);

        try {
            // 查询订单
            ProductOrderDO orderDO = productOrderMapper.selectOne(
                    new LambdaQueryWrapper<ProductOrderDO>()
                            .eq(ProductOrderDO::getOutTradeNo, outTradeNo)
            );

            if (orderDO == null) {
                log.warn("订单不存在,订单号:{}", outTradeNo);
                return;
            }

            // 已支付订单直接返回
            if (OrderStateEnum.PAY.name().equals(orderDO.getOrderState())) {
                log.info("订单已支付,正常结束");
                return;
            }

            // 已取消订单直接返回
            if (OrderStateEnum.CANCEL.name().equals(orderDO.getOrderState())) {
                log.info("订单已取消");
                return;
            }

            // 查询第三方支付状态
            if (PaymentEnum.WECHAT_PAY.name().equals(orderDO.getPayType())) {
                handleWechatTimeoutOrder(orderDO);
            }

        } catch (Exception e) {
            log.error("处理超时未支付订单失败", e);
        }
    }
    
    
    /** 处理微信支付超时订单,主动查询第三方状态进行补偿 */
    private void handleWechatTimeoutOrder(ProductOrderDO orderDO) {
        String outTradeNo = orderDO.getOutTradeNo();
        Transaction transaction = wechatPayUtil.queryOrderByOutTradeNo(outTradeNo);

        Transaction.TradeStateEnum tradeState = transaction.getTradeState();
        log.info("微信超时订单查询,订单号:{},状态:{}", outTradeNo, tradeState);

        if (Transaction.TradeStateEnum.SUCCESS.equals(tradeState)) {
            log.warn("回调异常补偿:第三方已支付,本地未更新,订单号:{}", outTradeNo);
            int updated = updateOrderState(outTradeNo, transaction.getTransactionId(), OrderStateEnum.PAY.name());
            if (updated<1) {
                log.error("订单{}状态补偿更新失败", outTradeNo);
            }
        } else if (Transaction.TradeStateEnum.USERPAYING.equals(tradeState)) {
            log.info("用户支付中,暂不处理,订单号:{}", outTradeNo);
        } else {
            // NOTPAY / CLOSED / REVOKED 等终态,直接取消
            log.info("第三方未支付({}),取消本地订单,订单号:{}", tradeState, outTradeNo);
            cancelOrder(orderDO);
        }
    }

    /** 取消订单 */
    private void cancelOrder(ProductOrderDO orderDO) {
        wechatPayUtil.closeOrder(orderDO.getOutTradeNo());
        orderDO.setOrderState(OrderStateEnum.CANCEL.name());
        productOrderMapper.updateById(orderDO);
        log.info("订单已取消,订单号:{}", orderDO.getOutTradeNo());
    }
相关推荐
@Murphy3 小时前
java 面试
java·开发语言·面试
楼田莉子3 小时前
C#学习:分支与循环
服务器·后端·学习·c#
lsx2024063 小时前
Scala 字符串处理指南
开发语言
XovH3 小时前
Django 从 0 到 1 打造完整电商平台:商品列表页实现
后端
小许同学记录成长3 小时前
Qt 自研测控软件-配置测试项
开发语言·qt
kunge20133 小时前
Claude Code Hooks 类型与使用指南
人工智能·后端·程序员
迈巴赫车主3 小时前
码蹄集 MC0457符咒封印java
java·数据结构·算法
biter down3 小时前
6:控件操作与鼠标模拟
开发语言·python
摇滚侠3 小时前
Java 零基础全套教程,数据结构与集合源码,笔记 168-174
java·数据结构·笔记