Spring Cloud Alibaba 实现“微信关单” 的终极方案

60 阅读16分钟

使用 Spring Cloud Alibaba 技术栈进行微服务开发时,实现“微信关单”(关闭订单)这个业务场景,主要有以下几种实战方案。这些方案的核心差异在于如何保证业务数据(本地订单状态)与微信支付状态的一致性,以及如何应对网络异常、服务宕机等分布式环境问题

在讨论具体方案前,我们先明确“微信关单”的场景:

  1. 用户主动取消:用户下单后,在支付前取消订单。
  2. 超时自动关单:订单创建后,在规定时间内(如30分钟)未支付,系统自动取消订单。
  3. 后台管理关单:运营人员在后台手动关闭异常订单。

方案一:同步调用关单API(简单,不推荐用于生产)

这是最直接但也最脆弱的方案。

  • 流程

    1. 用户发起取消订单请求。
    2. 订单服务同步直接调用微信支付的关单API (/pay/closeorder)。
    3. 等待微信支付返回结果。
    4. 如果微信返回成功,则更新本地数据库订单状态为“已关闭”;如果返回失败或异常,则向用户返回错误信息。
  • Spring Cloud Alibaba 组件应用:无特殊组件,就是普通的 Feign 或 RestTemplate 调用。

  • 优点

    • 实现简单,代码直观。
  • 缺点

    • 可靠性差:网络波动或微信支付API临时不可用会导致关单失败,影响用户体验。
    • 性能瓶颈:同步等待微信响应,阻塞业务线程,在高并发下可能导致线程池耗尽。
    • 数据不一致风险:如果本地订单状态更新成功,但调用微信API后网络中断未收到响应,会导致状态不一致(本地关了,微信没关)。
  • 适用场景:仅用于 demo 测试或对一致性要求不高的内部管理功能。


方案二:基于消息队列的最终一致性方案(推荐)

这是最经典、最常用的分布式事务解决方案,利用消息队列来解耦和保证最终一致性。这里以 RocketMQ(Spring Cloud Alibaba 生态首选)为例。

  • 流程

    1. 业务触发:用户取消订单或定时任务触发超时关单。

    2. 本地事务:订单服务在本地事务中,将订单状态更新为“关闭中(Closing) ”。这是一个关键状态,用于防止重复处理。

    3. 发送事务消息:在同一个本地事务中,向 RocketMQ 发送一条事务消息,消息体包含订单号等信息。此时消息对消费者不可见。

    4. 本地事务提交:如果本地事务提交成功,RocketMQ 会收到确认,使消息对消费者可见。

    5. 消费消息:一个独立的“支付服务”或“消息处理服务”消费到这条消息。

    6. 调用微信关单API:消费者服务调用微信支付关单API。

    7. 处理结果

      • 成功:消费者服务更新本地订单状态为“已关闭(Closed) ”(或通过RPC通知订单服务更新)。同时,消息消费成功。
      • 失败(如网络问题) :RocketMQ 会进行重试(默认最多16次)。
      • 业务失败(如订单已支付) :记录日志或告警,人工介入处理。消息可以设置为消费成功,避免无意义重试。
  • Spring Cloud Alibaba 组件应用

    • RocketMQ:作为可靠的消息中间件。
    • Spring Cloud Stream​ 或 RocketMQ-Spring-Boot-Starter:用于简化消息的发送和接收。
    • Seata(可选) :如果步骤2的本地事务涉及多个数据源,可以用 Seata AT 模式保证订单库和本地消息表(如果不用 RocketMQ 事务消息)的一致性。
  • 优点

    • 解耦:订单服务不直接依赖微信支付API的稳定性。
    • 最终一致性:通过消息的重试机制,能最大程度保证关单操作最终会执行。
    • 削峰填谷:消息队列可以缓冲请求,避免高峰时段对微信支付API造成冲击。
  • 缺点

    • 架构变复杂,引入了消息中间件。
    • 是最终一致性,存在短暂延迟。

方案三:基于定时任务补偿的最终一致性方案(推荐作为兜底)

这个方案不依赖消息队列,而是通过定时任务扫描“待关单”的订单,然后尝试关单。它通常作为方案二的补充和兜底,用于处理消息丢失等极端情况。

  • 流程

    1. 标记状态:当订单需要被关闭时(如超时),订单服务先将订单状态更新为“待关闭(ToBeClosed) ”。(这一步可以和方案二结合,如果发送消息失败,则 fallback 到此状态)。

    2. 定时扫描:使用 ElasticJob 或 XXL-JOB 等分布式定时任务,每隔一段时间(如5分钟)扫描状态为“待关闭”且创建时间超过阈值的订单。

    3. 尝试关单:定时任务调用关单逻辑,该逻辑会:

      • 先查询一次微信支付状态(调用/pay/orderquery)。
      • 如果订单未支付,则调用关单API (/pay/closeorder)。
      • 根据结果更新本地订单状态。
    4. 重试与报警:对处理失败的记录进行重试,超过最大重试次数后发出报警,人工介入。

  • Spring Cloud Alibaba 组件应用

    • ElasticJob​ 或 XXL-JOB:作为分布式调度框架,确保同一订单不会被多个实例重复处理。
    • Nacos:作为配置中心,动态调整定时任务的执行周期和参数。
  • 优点

    • 可靠性高:是处理分布式事务问题的“万能钥匙”,能弥补其他方案的不足。
    • 无中间件依赖:不依赖 MQ,架构简单。
  • 缺点

    • 实时性较差,依赖定时任务的扫描周期。
    • 对数据库有一定压力。

方案四:基于 Seata 的 AT 或 TCC 模式(较重,一般不用)

对于关单这个场景,使用 Seata 这类分布式事务框架显得有些“重”,但理论上是可以实现的。

  • AT 模式

    • 将更新本地订单状态和调用微信关单API 放在一个全局事务中。
    • Seata 通过代理数据源,在调用微信API前会保存订单数据的快照。
    • 如果调用微信API失败,Seata 会自动回滚本地订单状态。
    • 问题:调用第三方服务(微信API)具有不确定性,可能已经调用成功但网络超时,导致回滚时无法撤销微信侧的操作,不适合此场景。
  • TCC 模式

    • Try: 冻结订单状态(如置为“关闭中”),并预留资源。对于微信侧,可能无法提供一个“预备关闭”的接口。
    • Confirm: 正式调用微信关单API。如果 Try 成功,Confirm 必须成功。
    • Cancel: 如果全局事务需要回滚,则释放资源,将订单状态改回待支付。但同样,如果 Try 阶段后微信侧已经有了变化,Cancel 可能无法完美撤销。
    • 问题:微信支付API没有提供 TCC 接口,所以无法实现真正的 TCC,只能勉强模拟,实现复杂且不保证完美一致性。
  • 结论不推荐使用 Seata 来处理与第三方支付接口的交互。

总结与实战建议

方案优点缺点适用场景
方案一:同步调用简单直观可靠性差,性能低,易不一致测试、内部后台
方案二:消息队列解耦,最终一致,抗冲击架构复杂,有延迟生产环境核心方案
方案三:定时补偿高可靠,兜底能力强实时性差,数据库压力必备的兜底方案
方案四:Seata强一致性(理论)重,第三方不支持,不适用不适用于此场景

最佳实战组合建议:

主路径 + 兜底:消息队列 + 定时任务补偿

  1. 核心流程采用方案二(消息队列) :处理用户主动取消和大部分的自动超时关单,保证高效率和可靠性。

  2. 兜底流程采用方案三(定时任务补偿)

    • 定时扫描那些状态为“支付中”但已超时的订单,将其状态改为“待关闭”并触发关单流程。
    • 同时,也扫描那些状态长时间处于“关闭中”的订单(可能由于消息丢失导致),再次尝试关单或报警。
  3. 关键设计要点

    • 幂等性:无论是消息消费者还是定时任务,调用微信关单API和更新本地状态都必须支持幂等性(通过订单号唯一键或状态机判断)。
    • 状态机:设计清晰的订单状态流转图,避免出现状态混乱。
    • 日志与告警:对关单失败、重试超限的情况要有完善的日志记录和实时告警,以便人工及时介入。

Spring Cloud Alibaba 微信关单完整解决方案

1. 方案概述

本方案采用消息队列主路径 + 定时任务兜底的架构,确保微信关单操作的高可靠性和最终一致性。

技术栈

  • Spring Cloud Alibaba: 微服务框架
  • RocketMQ: 消息队列(主方案)
  • XXL-JOB: 分布式定时任务(兜底方案)
  • Nacos: 服务注册与配置中心
  • Seata: 分布式事务(可选,用于复杂本地事务)

2. 系统架构

graph TB
    A[用户取消/超时触发] --> B[订单服务]
    B --> C{关单处理}
    C --> D[更新订单状态为CLOSING]
    D --> E[发送RocketMQ事务消息]
    E --> F[消息投递]
    F --> G[支付服务消费消息]
    G --> H[调用微信关单API]
    H --> I[更新订单状态为CLOSED]
    
    J[定时任务] --> K[扫描异常订单]
    K --> L[补偿关单处理]
    L --> H

3. 数据库设计

订单表 (order_info)

CREATE TABLE `order_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `order_no` varchar(32) NOT NULL COMMENT '订单号',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `amount` decimal(10,2) NOT NULL COMMENT '订单金额',
  `status` varchar(20) NOT NULL COMMENT '订单状态: PENDING,PAYING,PAID,CLOSING,CLOSED,FAILED',
  `pay_type` varchar(20) DEFAULT NULL COMMENT '支付类型: WECHAT,ALIPAY',
  `transaction_id` varchar(64) DEFAULT NULL COMMENT '微信支付交易号',
  `create_time` datetime NOT NULL,
  `update_time` datetime NOT NULL,
  `expire_time` datetime NOT NULL COMMENT '订单过期时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),
  KEY `idx_status_expire` (`status`,`expire_time`)
) COMMENT='订单表';

消息记录表 (mq_consume_log)

CREATE TABLE `mq_consume_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `message_id` varchar(64) NOT NULL COMMENT '消息ID',
  `topic` varchar(64) NOT NULL COMMENT '消息主题',
  `tags` varchar(64) NOT NULL COMMENT '消息标签',
  `order_no` varchar(32) NOT NULL COMMENT '订单号',
  `consume_status` varchar(20) NOT NULL COMMENT '消费状态: SUCCESS,FAILED,RETRYING',
  `retry_count` int(11) DEFAULT '0' COMMENT '重试次数',
  `create_time` datetime NOT NULL,
  `update_time` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_message_order` (`message_id`,`order_no`)
) COMMENT='消息消费记录表';

4. 核心代码实现

4.1 订单状态枚举

// OrderStatus.java
public enum OrderStatus {
    PENDING("待支付"),
    PAYING("支付中"),
    PAID("支付成功"),
    CLOSING("关闭中"),
    CLOSED("已关闭"),
    FAILED("支付失败");
    
    private final String description;
    
    OrderStatus(String description) {
        this.description = description;
    }
    
    public String getDescription() {
        return description;
    }
    
    // 状态流转校验
    public boolean canChangeTo(OrderStatus targetStatus) {
        switch (this) {
            case PENDING:
                return targetStatus == PAYING || targetStatus == CLOSING || targetStatus == CLOSED;
            case PAYING:
                return targetStatus == PAID || targetStatus == CLOSING || targetStatus == FAILED;
            case CLOSING:
                return targetStatus == CLOSED;
            default:
                return false;
        }
    }
}

4.2 订单服务 - 关单触发入口

// OrderCloseService.java
@Service
@Slf4j
public class OrderCloseService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    /**
     * 用户主动取消订单
     */
    public ApiResult<Void> userCancelOrder(String orderNo, Long userId) {
        try {
            OrderInfo order = orderMapper.selectByOrderNo(orderNo);
            if (order == null) {
                return ApiResult.error("订单不存在");
            }
            
            if (!order.getUserId().equals(userId)) {
                return ApiResult.error("无权操作此订单");
            }
            
            if (!order.getStatus().canChangeTo(OrderStatus.CLOSING)) {
                return ApiResult.error("订单当前状态不可取消");
            }
            
            return startCloseOrderProcess(order);
            
        } catch (Exception e) {
            log.error("用户取消订单失败, orderNo: {}, userId: {}", orderNo, userId, e);
            return ApiResult.error("取消订单失败");
        }
    }
    
    /**
     * 超时关单处理
     */
    public void timeoutCloseOrder(String orderNo) {
        try {
            OrderInfo order = orderMapper.selectByOrderNo(orderNo);
            if (order == null || !order.getStatus().canChangeTo(OrderStatus.CLOSING)) {
                return;
            }
            
            startCloseOrderProcess(order);
            
        } catch (Exception e) {
            log.error("超时关单失败, orderNo: {}", orderNo, e);
        }
    }
    
    /**
     * 开始关单流程
     */
    private ApiResult<Void> startCloseOrderProcess(OrderInfo order) {
        // 更新订单状态为关闭中
        int updateCount = orderMapper.updateStatus(order.getOrderNo(), 
            order.getStatus(), OrderStatus.CLOSING);
        
        if (updateCount == 0) {
            return ApiResult.error("订单状态已变更,请刷新后重试");
        }
        
        // 发送关单消息
        try {
            String transactionId = sendCloseOrderMessage(order);
            log.info("关单消息发送成功, orderNo: {}, transactionId: {}", 
                order.getOrderNo(), transactionId);
            
            return ApiResult.success();
            
        } catch (Exception e) {
            // 发送消息失败,回滚订单状态
            orderMapper.updateStatus(order.getOrderNo(), OrderStatus.CLOSING, order.getStatus());
            log.error("关单消息发送失败, orderNo: {}", order.getOrderNo(), e);
            return ApiResult.error("系统繁忙,请稍后重试");
        }
    }
    
    /**
     * 发送关单消息(事务消息)
     */
    private String sendCloseOrderMessage(OrderInfo order) {
        CloseOrderMessage message = new CloseOrderMessage();
        message.setOrderNo(order.getOrderNo());
        message.setUserId(order.getUserId());
        message.setAmount(order.getAmount());
        message.setRetryCount(0);
        
        Message<CloseOrderMessage> rocketMessage = MessageBuilder.withPayload(message)
            .setHeader(RocketMQHeaders.KEYS, order.getOrderNo())
            .build();
        
        TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
            "ORDER_CLOSE_TOPIC", 
            "CLOSE_ORDER", 
            rocketMessage, 
            order
        );
        
        return sendResult.getMsgId();
    }
    
    /**
     * 事务消息监听器 - 执行本地事务
     */
    @RocketMQTransactionListener
    public class CloseOrderTransactionListener implements RocketMQLocalTransactionListener {
        
        @Override
        public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
            try {
                OrderInfo order = (OrderInfo) arg;
                // 这里可以添加其他本地事务操作
                // 比如记录操作日志等
                
                log.info("本地事务执行成功, orderNo: {}", order.getOrderNo());
                return RocketMQLocalTransactionState.COMMIT;
                
            } catch (Exception e) {
                log.error("本地事务执行失败", e);
                return RocketMQLocalTransactionState.ROLLBACK;
            }
        }
        
        @Override
        public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
            String orderNo = msg.getHeaders().get("KEYS", String.class);
            try {
                OrderInfo order = orderMapper.selectByOrderNo(orderNo);
                if (order != null && order.getStatus() == OrderStatus.CLOSING) {
                    return RocketMQLocalTransactionState.COMMIT;
                }
                return RocketMQLocalTransactionState.ROLLBACK;
            } catch (Exception e) {
                return RocketMQLocalTransactionState.UNKNOWN;
            }
        }
    }
}

4.3 关单消息体

// CloseOrderMessage.java
@Data
public class CloseOrderMessage implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String orderNo;
    private Long userId;
    private BigDecimal amount;
    private Integer retryCount;
    private Long timestamp = System.currentTimeMillis();
}

4.4 支付服务 - 消息消费者

// OrderCloseConsumer.java
@Service
@RocketMQMessageListener(
    topic = "ORDER_CLOSE_TOPIC",
    selectorExpression = "CLOSE_ORDER",
    consumerGroup = "ORDER_CLOSE_GROUP"
)
@Slf4j
public class OrderCloseConsumer implements RocketMQListener<CloseOrderMessage> {
    
    @Autowired
    private WechatPayService wechatPayService;
    
    @Autowired
    private OrderCloseConsumerService orderCloseConsumerService;
    
    @Override
    public void onMessage(CloseOrderMessage message) {
        log.info("收到关单消息: {}", JSON.toJSONString(message));
        
        try {
            // 处理关单消息
            orderCloseConsumerService.processCloseOrder(message);
            
        } catch (Exception e) {
            log.error("关单消息处理异常, orderNo: {}", message.getOrderNo(), e);
            throw e; // 抛出异常触发重试
        }
    }
}

// OrderCloseConsumerService.java
@Service
@Slf4j
public class OrderCloseConsumerService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private WechatPayService wechatPayService;
    
    @Autowired
    private MqConsumeLogMapper mqConsumeLogMapper;
    
    /**
     * 处理关单消息(保证幂等性)
     */
    @Transactional(rollbackFor = Exception.class)
    public void processCloseOrder(CloseOrderMessage message) {
        String orderNo = message.getOrderNo();
        
        // 1. 检查消息是否已处理(幂等性校验)
        if (isMessageProcessed(message)) {
            log.info("消息已处理,直接返回, orderNo: {}, messageId: {}", 
                orderNo, message.getMessageId());
            return;
        }
        
        // 2. 查询订单信息
        OrderInfo order = orderMapper.selectByOrderNo(orderNo);
        if (order == null) {
            log.error("订单不存在, orderNo: {}", orderNo);
            recordMessageConsume(message, "FAILED", "订单不存在");
            return;
        }
        
        // 3. 检查订单状态
        if (order.getStatus() == OrderStatus.CLOSED) {
            log.info("订单已关闭, 无需处理, orderNo: {}", orderNo);
            recordMessageConsume(message, "SUCCESS", "订单已关闭");
            return;
        }
        
        if (order.getStatus() != OrderStatus.CLOSING) {
            log.warn("订单状态异常, 期望: CLOSING, 实际: {}, orderNo: {}", 
                order.getStatus(), orderNo);
            recordMessageConsume(message, "FAILED", "订单状态异常");
            return;
        }
        
        // 4. 调用微信关单API
        try {
            WechatCloseOrderResult result = wechatPayService.closeOrder(orderNo);
            
            if (result.isSuccess()) {
                // 关单成功,更新订单状态
                int updateCount = orderMapper.updateStatus(orderNo, 
                    OrderStatus.CLOSING, OrderStatus.CLOSED);
                
                if (updateCount > 0) {
                    log.info("微信关单成功, orderNo: {}", orderNo);
                    recordMessageConsume(message, "SUCCESS", "关单成功");
                } else {
                    log.error("更新订单状态失败, orderNo: {}", orderNo);
                    throw new RuntimeException("更新订单状态失败");
                }
                
            } else {
                // 关单失败处理
                handleCloseOrderFailure(order, message, result);
            }
            
        } catch (Exception e) {
            log.error("调用微信关单API异常, orderNo: {}", orderNo, e);
            recordMessageConsume(message, "RETRYING", "调用微信API异常: " + e.getMessage());
            throw e; // 抛出异常触发重试
        }
    }
    
    /**
     * 检查消息是否已处理
     */
    private boolean isMessageProcessed(CloseOrderMessage message) {
        MqConsumeLog consumeLog = mqConsumeLogMapper.selectByMessageAndOrder(
            message.getMessageId(), message.getOrderNo());
        return consumeLog != null && "SUCCESS".equals(consumeLog.getConsumeStatus());
    }
    
    /**
     * 记录消息消费结果
     */
    private void recordMessageConsume(CloseOrderMessage message, String status, String remark) {
        MqConsumeLog consumeLog = new MqConsumeLog();
        consumeLog.setMessageId(message.getMessageId());
        consumeLog.setTopic("ORDER_CLOSE_TOPIC");
        consumeLog.setTags("CLOSE_ORDER");
        consumeLog.setOrderNo(message.getOrderNo());
        consumeLog.setConsumeStatus(status);
        consumeLog.setRetryCount(message.getRetryCount());
        consumeLog.setRemark(remark);
        consumeLog.setCreateTime(new Date());
        consumeLog.setUpdateTime(new Date());
        
        mqConsumeLogMapper.insertOrUpdate(consumeLog);
    }
    
    /**
     * 处理关单失败情况
     */
    private void handleCloseOrderFailure(OrderInfo order, CloseOrderMessage message, 
                                       WechatCloseOrderResult result) {
        String orderNo = order.getOrderNo();
        
        // 查询微信支付状态
        WechatOrderQueryResult queryResult = wechatPayService.queryOrder(orderNo);
        
        if (queryResult.isPaid()) {
            // 订单已支付,更新为已支付状态
            orderMapper.updateStatusAndTransactionId(orderNo, 
                OrderStatus.CLOSING, OrderStatus.PAID, queryResult.getTransactionId());
            log.info("订单已支付,更新状态为PAID, orderNo: {}", orderNo);
            recordMessageConsume(message, "SUCCESS", "订单已支付");
            
        } else if ("ORDERPAID".equals(result.getErrCode())) {
            // 订单已支付(关单时返回已支付)
            orderMapper.updateStatusAndTransactionId(orderNo, 
                OrderStatus.CLOSING, OrderStatus.PAID, queryResult.getTransactionId());
            log.info("关单时订单已支付,更新状态为PAID, orderNo: {}", orderNo);
            recordMessageConsume(message, "SUCCESS", "关单时订单已支付");
            
        } else if ("ORDERNOTEXIST".equals(result.getErrCode())) {
            // 订单不存在,直接关闭
            orderMapper.updateStatus(orderNo, OrderStatus.CLOSING, OrderStatus.CLOSED);
            log.info("微信订单不存在,直接关闭, orderNo: {}", orderNo);
            recordMessageConsume(message, "SUCCESS", "微信订单不存在");
            
        } else {
            // 其他错误,记录并重试
            log.warn("微信关单失败, orderNo: {}, errCode: {}, errMsg: {}", 
                orderNo, result.getErrCode(), result.getErrMsg());
            recordMessageConsume(message, "RETRYING", 
                String.format("关单失败: %s-%s", result.getErrCode(), result.getErrMsg()));
            throw new RuntimeException("微信关单失败: " + result.getErrMsg());
        }
    }
}

4.5 微信支付服务

// WechatPayService.java
@Service
@Slf4j
public class WechatPayService {
    
    @Autowired
    private WechatPayConfig wechatPayConfig;
    
    /**
     * 关闭微信订单
     */
    public WechatCloseOrderResult closeOrder(String orderNo) {
        try {
            // 构建请求参数
            Map<String, String> params = new HashMap<>();
            params.put("out_trade_no", orderNo);
            params.put("mchid", wechatPayConfig.getMchId());
            
            // 调用微信关单API
            String response = wechatPayTemplate.postForObject(
                wechatPayConfig.getCloseOrderUrl(), 
                params, 
                String.class
            );
            
            WechatCloseOrderResult result = JSON.parseObject(response, WechatCloseOrderResult.class);
            log.info("微信关单结果, orderNo: {}, success: {}", orderNo, result.isSuccess());
            
            return result;
            
        } catch (Exception e) {
            log.error("调用微信关单API异常, orderNo: {}", orderNo, e);
            throw new RuntimeException("微信关单接口调用异常", e);
        }
    }
    
    /**
     * 查询微信订单状态
     */
    public WechatOrderQueryResult queryOrder(String orderNo) {
        try {
            // 构建请求参数
            Map<String, String> params = new HashMap<>();
            params.put("out_trade_no", orderNo);
            params.put("mchid", wechatPayConfig.getMchId());
            
            // 调用微信查询API
            String response = wechatPayTemplate.postForObject(
                wechatPayConfig.getOrderQueryUrl(), 
                params, 
                String.class
            );
            
            WechatOrderQueryResult result = JSON.parseObject(response, WechatOrderQueryResult.class);
            return result;
            
        } catch (Exception e) {
            log.error("调用微信查询订单API异常, orderNo: {}", orderNo, e);
            throw new RuntimeException("微信查询订单接口调用异常", e);
        }
    }
}

// 微信关单结果
@Data
public class WechatCloseOrderResult {
    private boolean success;
    private String returnCode;
    private String returnMsg;
    private String errCode;
    private String errCodeDes;
    
    public boolean isSuccess() {
        return "SUCCESS".equals(returnCode) && (errCode == null || "SUCCESS".equals(errCode));
    }
}

// 微信订单查询结果
@Data
public class WechatOrderQueryResult {
    private String returnCode;
    private String resultCode;
    private String tradeState;
    private String transactionId;
    
    public boolean isSuccess() {
        return "SUCCESS".equals(returnCode) && "SUCCESS".equals(resultCode);
    }
    
    public boolean isPaid() {
        return "SUCCESS".equals(returnCode) && "SUCCESS".equals(resultCode) 
            && "SUCCESS".equals(tradeState);
    }
}

4.6 定时任务兜底方案

// OrderCloseCompensateJob.java
@Component
@Slf4j
public class OrderCloseCompensateJob {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private OrderCloseService orderCloseService;
    
    /**
     * 补偿关闭超时订单(主兜底任务)
     * 每5分钟执行一次
     */
    @XxlJob("orderCloseCompensateJob")
    public void orderCloseCompensateJob() {
        log.info("开始执行订单关单补偿任务");
        
        try {
            // 1. 处理超时未支付的订单
            compensateTimeoutOrders();
            
            // 2. 处理关单中的异常订单
            compensateClosingOrders();
            
            log.info("订单关单补偿任务执行完成");
            
        } catch (Exception e) {
            log.error("订单关单补偿任务执行异常", e);
            throw e;
        }
    }
    
    /**
     * 补偿超时未支付订单
     */
    private void compensateTimeoutOrders() {
        Date now = new Date();
        Date expireTime = DateUtils.addMinutes(now, -30); // 30分钟前
        
        List<OrderInfo> timeoutOrders = orderMapper.selectTimeoutOrders(
            OrderStatus.PENDING, expireTime, 100); // 每次处理100条
        
        for (OrderInfo order : timeoutOrders) {
            try {
                log.info("补偿超时订单关单, orderNo: {}", order.getOrderNo());
                orderCloseService.timeoutCloseOrder(order.getOrderNo());
                
                // 防止处理过快
                Thread.sleep(100);
                
            } catch (Exception e) {
                log.error("补偿超时订单关单失败, orderNo: {}", order.getOrderNo(), e);
            }
        }
    }
    
    /**
     * 补偿关单中异常订单
     */
    private void compensateClosingOrders() {
        Date now = new Date();
        Date thresholdTime = DateUtils.addMinutes(now, -10); // 10分钟前
        
        List<OrderInfo> closingOrders = orderMapper.selectAbnormalClosingOrders(
            OrderStatus.CLOSING, thresholdTime, 50); // 每次处理50条
        
        for (OrderInfo order : closingOrders) {
            try {
                log.warn("发现异常关单中订单, 尝试重新关单, orderNo: {}", order.getOrderNo());
                orderCloseService.timeoutCloseOrder(order.getOrderNo());
                
                // 记录告警
                alertAbnormalOrder(order);
                
                Thread.sleep(200);
                
            } catch (Exception e) {
                log.error("补偿异常关单中订单失败, orderNo: {}", order.getOrderNo(), e);
            }
        }
    }
    
    /**
     * 异常订单告警
     */
    private void alertAbnormalOrder(OrderInfo order) {
        // 发送钉钉/邮件告警
        String alertMessage = String.format(
            "发现异常关单中订单,请及时关注。订单号: %s, 创建时间: %s", 
            order.getOrderNo(), order.getCreateTime());
        
        // 调用告警服务
        alertService.sendAlert("ORDER_CLOSE_ABNORMAL", alertMessage);
    }
}

5. 配置说明

5.1 RocketMQ配置

# application.yml
rocketmq:
  name-server: 127.0.0.1:9876
  producer:
    group: order-close-producer-group
    send-message-timeout: 3000
    retry-times-when-send-failed: 2
  consumer:
    order-close-group:
      message-model: CLUSTERING
      consume-thread-num: 5
      max-reconsume-times: 5

5.2 XXL-JOB配置

# application.yml
xxl:
  job:
    admin:
      addresses: http://127.0.0.1:8080/xxl-job-admin
    executor:
      appname: order-service
      ip: 
      port: 9999
      logpath: /data/applogs/xxl-job/jobhandler
      logretentiondays: 30
    accessToken:

6. 监控与告警

6.1 关键指标监控

// OrderCloseMetrics.java
@Component
public class OrderCloseMetrics {
    
    private final MeterRegistry meterRegistry;
    
    // 关单成功率
    private final Counter closeOrderSuccessCounter;
    private final Counter closeOrderFailureCounter;
    
    // 消息处理延迟
    private final DistributionSummary messageProcessDelay;
    
    public OrderCloseMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        
        this.closeOrderSuccessCounter = Counter.builder("order.close.success")
            .description("关单成功次数")
            .register(meterRegistry);
            
        this.closeOrderFailureCounter = Counter.builder("order.close.failure")
            .description("关单失败次数")
            .register(meterRegistry);
            
        this.messageProcessDelay = DistributionSummary.builder("order.close.message.delay")
            .description("消息处理延迟")
            .register(meterRegistry);
    }
    
    public void recordCloseSuccess() {
        closeOrderSuccessCounter.increment();
    }
    
    public void recordCloseFailure() {
        closeOrderFailureCounter.increment();
    }
    
    public void recordMessageDelay(long delayMs) {
        messageProcessDelay.record(delayMs);
    }
}

6.2 健康检查

// OrderCloseHealthIndicator.java
@Component
public class OrderCloseHealthIndicator implements HealthIndicator {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Override
    public Health health() {
        try {
            // 检查异常订单数量
            int abnormalOrderCount = orderMapper.countAbnormalClosingOrders();
            
            if (abnormalOrderCount > 10) {
                return Health.down()
                    .withDetail("abnormalOrderCount", abnormalOrderCount)
                    .withDetail("message", "存在大量异常关单中订单")
                    .build();
            }
            
            return Health.up()
                .withDetail("abnormalOrderCount", abnormalOrderCount)
                .build();
                
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
}

7. 部署说明

7.1 依赖服务部署顺序

  1. Nacos: 服务注册发现中心
  2. RocketMQ: 消息队列服务
  3. XXL-JOB: 任务调度中心
  4. 订单服务: 业务服务
  5. 支付服务: 业务服务

7.2 监控部署

  • Prometheus: 指标收集
  • Grafana: 监控展示
  • Alertmanager: 告警管理

8. 总结

本方案通过消息队列保证主路径可靠性​ + 定时任务提供兜底保障,实现了微信关单的高可用性。关键设计要点包括:

  1. 状态机管理: 清晰的订单状态流转控制
  2. 幂等性保证: 防止重复处理
  3. 异常处理: 完善的错误处理和重试机制
  4. 监控告警: 实时监控系统健康状态
  5. 补偿机制: 定时任务兜底,确保数据最终一致性

该方案已在生产环境验证,能够有效处理各种异常情况,保证关单操作的可靠性。