无人售货机订单业务MQ架构:创建订单、支付回调、出货完成、订单归档

16-订单业务MQ架构:创建订单、支付回调、出货完成、订单归档

作者:黒漂技术佬 系列:RocketMQ 核心原理与无人售货柜项目实战 面向:新手小白也能看懂的 MQ 架构设计指南


一、为什么要用 MQ 驱动订单流程?

先想象一个场景:你在一台无人售货柜前扫码下单,系统需要同时完成"创建订单 → 扣减库存 → 请求支付",如果这三个操作写在一个同步方法里,任何一个环节卡住,用户就只能盯着屏幕转圈圈。更要命的是,支付成功后还要通知设备出货、更新订单状态、发积分------这一串操作如果全部串行执行,接口耗时轻松破秒。

这就是引入 消息队列(Message Queue) 的核心原因:解耦。订单服务只管把"订单创建好了"这件事告诉 MQ,之后库存服务、支付服务各取所需,互不干扰。支付回来也一样,发一条"钱到账了"的消息,订单服务、出货服务、积分服务各干各的。

消息队列本质上是一个"异步通信的中间人":生产者把消息扔进去就不管了,消费者按自己的节奏取消息处理。RocketMQ 是这个领域的国产优秀选手,阿里出品,经双十一验证,特别适合我们这种订单驱动的业务场景。


二、订单全生命周期消息流转设计

无人售货柜的订单从创建到归档,一共经历四个关键节点,每个节点由一条 MQ 消息驱动:

阶段 生产者 消息 消费者 做什么
1. 创建订单 订单服务 订单创建消息 库存服务 锁定库存(预占)
2. 支付回调 支付服务 支付成功消息 订单服务 + 出货服务 更新订单状态 + 触发出货
3. 出货完成 设备服务 出货完成消息 订单服务 + 库存服务 完成订单 + 扣减库存
4. 订单归档 定时任务 归档消息 归档服务 迁移历史订单到归档库

下面逐一拆解。


三、Topic / Tag 设计

一个设计良好的 Topic 体系是 MQ 架构的基石。RocketMQ 中,Topic 是一类消息的逻辑分类,Tag 是 Topic 下面的二级标签,用于进一步区分消息类型。消费者可以用 Topic + Tag 精确订阅自己关心的消息。

我们的设计如下:

yaml 复制代码
Topic: OrderTopic
├── Tag: ORDER_CREATED        # 订单创建
├── Tag: ORDER_PAID           # 支付成功
├── Tag: ORDER_DISPATCHED     # 出货完成
└── Tag: ORDER_ARCHIVE        # 订单归档

为什么所有订单相关消息放在同一个 Topic? 因为同一个 Topic 下的消息可以保证顺序消费(配合 MessageQueue 选择器),这对于同一笔订单的状态流转至关重要------你必须先"创建"再"支付"再"出货",不能乱。

消费者订阅关系:

  • 库存服务:订阅 OrderTopic,Tag 为 ORDER_CREATED || ORDER_DISPATCHED
  • 支付服务:订阅 OrderTopic,Tag 为 ORDER_CREATED(但一般支付由外部回调触发)
  • 出货服务:订阅 OrderTopic,Tag 为 ORDER_PAID
  • 订单服务:订阅 OrderTopic,Tag 为 ORDER_PAID || ORDER_DISPATCHED
  • 归档服务:订阅 OrderTopic,Tag 为 ORDER_ARCHIVE

四、消息体设计(Message DTO)

消息体就是消息的"内容"。我们定义一个统一的订单消息结构:

java 复制代码
public class OrderMessageDTO {
    /**
     * 消息版本号,用于后续消息格式升级兼容
     */
    private int version = 1;

    /**
     * 全链路追踪ID,贯穿订单创建→支付→出货→归档
     */
    private String traceId;

    /**
     * 消息类型,对应 Tag 值的枚举
     */
    private String messageType;  // ORDER_CREATED / ORDER_PAID / ...

    /**
     * 订单ID(幂等去重的唯一业务键)
     */
    private Long orderId;

    /**
     * 设备ID(哪台售货柜)
     */
    private String deviceId;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 商品列表
     */
    private List<OrderItemDTO> items;

    /**
     * 订单金额(分)
     */
    private Long amount;

    /**
     * 实付金额(分),支付回调时才赋值
     */
    private Long paidAmount;

    /**
     * 支付渠道:WECHAT / ALIPAY
     */
    private String payChannel;

    /**
     * 第三方支付流水号
     */
    private String transactionId;

    /**
     * 消息产生时间戳(毫秒)
     */
    private Long timestamp;
}

几个关键设计决策:

  • traceId:从订单创建时就生成,后续所有消息都带上它,查问题时有这条线就能串起整个链路。
  • version:为未来消息格式变更预留,消费者可以根据 version 选择不同的反序列化逻辑。
  • 金额用 Long 存分:浮点数有精度问题,用"分"做单位,100 分 = 1 元,安全又简单。

五、每个环节的详细设计

5.1 环节一:创建订单 → 锁定库存

java 复制代码
// ========== 生产者:订单服务 ==========
@Service
public class OrderService {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Transactional
    public CreateOrderResult createOrder(CreateOrderRequest request) {
        // 1. 保存订单到数据库,状态为 PENDING_PAY
        Order order = saveOrder(request);
        // 2. 构建消息
        OrderMessageDTO msg = OrderMessageDTO.builder()
            .traceId(TraceContext.getTraceId())
            .messageType("ORDER_CREATED")
            .orderId(order.getId())
            .deviceId(request.getDeviceId())
            .userId(request.getUserId())
            .items(request.getItems())
            .amount(order.getTotalAmount())
            .timestamp(System.currentTimeMillis())
            .build();
        // 3. 发送消息:订单ID作为key,保证同一订单的消息进同一个队列
        //    同步发送确保消息不丢失
        rocketMQTemplate.syncSendOrderly(
            "OrderTopic:ORDER_CREATED",
            MessageBuilder.withPayload(msg)
                .setHeader(RocketMQHeaders.KEYS, String.valueOf(order.getId()))
                .build(),
            String.valueOf(order.getId())  // hashKey → 选择队列
        );
        return new CreateOrderResult(order.getId());
    }
}

// ========== 消费者:库存服务 ==========
@Service
@RocketMQMessageListener(
    topic = "OrderTopic",
    selectorExpression = "ORDER_CREATED",
    consumerGroup = "inventory-lock-group",
    consumeMode = ConsumeMode.ORDERLY   // 顺序消费
)
public class InventoryLockConsumer implements RocketMQListener<OrderMessageDTO> {

    @Override
    public void onMessage(OrderMessageDTO msg) {
        // 幂等检查:是否已经锁定过
        if (inventoryLocked(msg.getOrderId())) {
            return;
        }
        // Lua脚本原子锁定:遍历商品列表,每个商品预占库存
        for (OrderItemDTO item : msg.getItems()) {
            boolean locked = inventoryService.lockStock(
                item.getSkuId(), item.getQuantity(), msg.getOrderId()
            );
            if (!locked) {
                // 库存不足,发送库存不足消息,触发订单关闭
                sendStockInsufficientEvent(msg.getOrderId());
                return;
            }
        }
        // 记录锁定日志
        saveLockRecord(msg.getOrderId());
    }
}

幂等 是指同一个操作执行多次的结果和执行一次相同。这里用 inventoryLocked(orderId) 检查,防止消息重复消费导致重复锁定。

5.2 环节二:支付回调 → 更新订单 + 触发出货

当微信/支付宝回调通知我们"用户付钱了",支付服务发送支付成功消息:

java 复制代码
// ========== 生产者:支付服务 ==========
@RestController
public class PayCallbackController {

    @PostMapping("/callback/wechat")
    public String wechatCallback(@RequestBody WechatPayNotify notify) {
        // 1. 验签(防止伪造回调)
        if (!wechatPayService.verifySignature(notify)) {
            return "FAIL";
        }
        // 2. 解析订单ID
        Long orderId = Long.valueOf(notify.getOutTradeNo());
        // 3. 发送支付成功消息
        OrderMessageDTO msg = buildPaidMessage(orderId, notify);
        rocketMQTemplate.syncSendOrderly(
            "OrderTopic:ORDER_PAID",
            MessageBuilder.withPayload(msg)
                .setHeader(RocketMQHeaders.KEYS, String.valueOf(orderId))
                .build(),
            String.valueOf(orderId)
        );
        return "SUCCESS";
    }
}

// ========== 消费者:出货服务 ==========
@Service
@RocketMQMessageListener(
    topic = "OrderTopic",
    selectorExpression = "ORDER_PAID",
    consumerGroup = "dispatch-group",
    consumeMode = ConsumeMode.ORDERLY
)
public class OrderDispatchConsumer implements RocketMQListener<OrderMessageDTO> {

    @Override
    public void onMessage(OrderMessageDTO msg) {
        // 1. 向设备下发出货指令(通过MQTT或WebSocket)
        DispatchCommand cmd = DispatchCommand.builder()
            .deviceId(msg.getDeviceId())
            .orderId(msg.getOrderId())
            .items(msg.getItems())
            .build();
        deviceService.sendDispatchCommand(cmd);
        // 2. 记录出货任务
        dispatchTaskService.createTask(msg.getOrderId(), msg.getDeviceId());
    }
}

5.3 环节三:出货完成 → 完成订单 + 扣减库存

设备出货成功后,设备服务(或 IoT 网关)发送出货完成消息:

java 复制代码
// ========== 消费者:订单服务 ==========
@Service
@RocketMQMessageListener(
    topic = "OrderTopic",
    selectorExpression = "ORDER_DISPATCHED",
    consumerGroup = "order-finish-group",
    consumeMode = ConsumeMode.ORDERLY
)
public class OrderFinishConsumer implements RocketMQListener<OrderMessageDTO> {

    @Override
    public void onMessage(OrderMessageDTO msg) {
        // 状态机校验:只有 PAID 状态的订单才能流转到 FINISHED
        Order order = orderService.getById(msg.getOrderId());
        if (order.getStatus() != OrderStatus.PAID) {
            // 状态不对,记录告警
            log.warn("订单状态异常,当前:{},期望:PAID", order.getStatus());
            return;
        }
        // 更新订单状态
        orderService.updateStatus(msg.getOrderId(), OrderStatus.FINISHED);
    }
}

// ========== 消费者:库存服务(扣减库存,区别于之前的锁定) ==========
@Service
@RocketMQMessageListener(
    topic = "OrderTopic",
    selectorExpression = "ORDER_DISPATCHED",
    consumerGroup = "inventory-deduct-group",
    consumeMode = ConsumeMode.ORDERLY
)
public class InventoryDeductConsumer implements RocketMQListener<OrderMessageDTO> {

    @Override
    public void onMessage(OrderMessageDTO msg) {
        // 从预占转为实际扣减
        for (OrderItemDTO item : msg.getItems()) {
            inventoryService.deductStock(item.getSkuId(), item.getQuantity(), msg.getOrderId());
        }
    }
}

注意这里库存经历了两个阶段:锁定(预占)→ 实际扣减。锁定发生在下单时,防止超卖;实际扣减发生在出货后,因为只有货真的出去了才算消耗。

5.4 环节四:定时归档 → 迁移历史订单

java 复制代码
// ========== 生产者:定时任务 ==========
@Component
public class OrderArchiveJob {

    @Scheduled(cron = "0 0 3 * * ?")  // 每天凌晨3点执行
    public void archiveOrders() {
        // 查询3天前完成的订单
        List<Long> orderIds = orderService.findFinishedBefore(3);
        for (Long orderId : orderIds) {
            OrderMessageDTO msg = OrderMessageDTO.builder()
                .traceId(UUID.randomUUID().toString())
                .messageType("ORDER_ARCHIVE")
                .orderId(orderId)
                .timestamp(System.currentTimeMillis())
                .build();
            rocketMQTemplate.syncSend("OrderTopic:ORDER_ARCHIVE", msg);
        }
    }
}

// ========== 消费者:归档服务 ==========
@Service
@RocketMQMessageListener(
    topic = "OrderTopic",
    selectorExpression = "ORDER_ARCHIVE",
    consumerGroup = "archive-group"
)
public class OrderArchiveConsumer implements RocketMQListener<OrderMessageDTO> {

    @Override
    public void onMessage(OrderMessageDTO msg) {
        // 1. 把订单数据写入归档库(MongoDB 或独立的归档 MySQL)
        Order order = orderService.getById(msg.getOrderId());
        archiveRepository.save(order);
        // 2. 从主库删除(可选,取决于业务需要保留多久)
        orderService.softDelete(msg.getOrderId());
    }
}

六、异常场景处理

真实世界不是理想世界,四条消息都可能出问题。

场景1:支付成功但出货失败怎么办?

出货指令下发后,设备可能因为"卡货"、"断网"等原因无法完成出货。这时候需要补偿退款

java 复制代码
// 出货服务定时扫描:超过5分钟还未收到出货完成的订单
@Scheduled(fixedDelay = 60000)
public void checkTimeoutDispatch() {
    List<DispatchTask> timeoutTasks = dispatchTaskService.findTimeout(5);
    for (DispatchTask task : timeoutTasks) {
        // 发送退款消息
        RefundMessageDTO refundMsg = RefundMessageDTO.builder()
            .orderId(task.getOrderId())
            .reason("出货超时未完成")
            .amount(task.getAmount())
            .build();
        rocketMQTemplate.syncSend("RefundTopic:REFUND_REQUEST", refundMsg);
    }
}

场景2:出货成功但订单更新失败

这种情况通常由数据库瞬时故障导致。RocketMQ 的消费失败默认会重试 16 次 (递增间隔),加上消息消费者的幂等设计(状态机校验),最终一定能成功。

场景3:库存锁定后订单超时未支付

用户下单后 15 分钟不付款,需要释放库存:

java 复制代码
// 定时任务:扫描超时订单
@Scheduled(fixedDelay = 30000)
public void releaseTimeoutLocks() {
    List<Order> timeoutOrders = orderService.findTimeoutPending(15);
    for (Order order : timeoutOrders) {
        orderService.updateStatus(order.getId(), OrderStatus.CLOSED);
        // 发送库存回滚消息
        rocketMQTemplate.syncSend("OrderTopic:ORDER_CLOSED",
            buildCloseMessage(order.getId()));
    }
}

库存服务收到 ORDER_CLOSED 消息后,回滚预占的库存。


七、消息时序保障

同一订单的创建 → 支付 → 出货 → 归档四条消息,必须保证严格有序。否则可能出现"还没支付就出货"的恐怖场景。

保障手段:

  1. syncSendOrderly :发送时用订单 ID 作为 hashKey,RocketMQ 会把相同 hashKey 的消息路由到同一个 MessageQueue。
  2. ConsumeMode.ORDERLY:消费者设置为顺序消费模式,同一队列的消息单线程处理。
  3. 状态机校验:消费者处理前检查订单当前状态,不符合预期就拒绝处理。

八、完整架构图

scss 复制代码
┌──────────┐    创建订单     ┌──────────────┐    ORDER_CREATED     ┌──────────┐
│ 订单服务  │ ─────────────→ │  RocketMQ    │ ──────────────────→ │ 库存服务  │
│          │                │              │                     │ (锁定库存)│
└──────────┘                │  OrderTopic  │                     └──────────┘
                            │              │
┌──────────┐   支付回调      │              │    ORDER_PAID        ┌──────────┐
│ 支付服务  │ ─────────────→ │  ├─Q0─────── │ ──────────────────→ │ 订单服务  │
│(微信支付) │                │  ├─Q1─────── │                     │(更新状态)│
└──────────┘                │  ├─Q2─────── │                     └──────────┘
                            │  ├─Q3─────── │
┌──────────┐   出货完成      │              │    ORDER_PAID        ┌──────────┐
│ 设备服务  │ ─────────────→ │              │ ──────────────────→ │ 出货服务  │
│(IoT网关) │                │              │                     │(下发指令)│
└──────────┘                └──────────────┘                     └──────────┘
                                    │
                            ORDER_DISPATCHED
                                    │
                    ┌───────────────┼───────────────┐
                    ↓                               ↓
            ┌──────────┐                   ┌──────────┐
            │ 订单服务  │                   │ 库存服务  │
            │(完成订单)│                   │(扣减库存)│
            └──────────┘                   └──────────┘

┌──────────┐   定时归档     ┌──────────────┐    ORDER_ARCHIVE     ┌──────────┐
│ 定时任务  │ ─────────────→ │  RocketMQ    │ ──────────────────→ │ 归档服务  │
│(每天3点) │                │  OrderTopic  │                     │(MongoDB) │
└──────────┘                └──────────────┘                     └──────────┘

九、订单状态机

java 复制代码
public enum OrderStatus {
    PENDING_PAY,   // 待支付
    PAID,          // 已支付(等待出货)
    DISPATCHING,   // 出货中
    FINISHED,      // 已完成
    CLOSED,        // 已关闭(超时/退款)
    REFUNDING,     // 退款中
    REFUNDED       // 已退款
}

// 合法的状态流转
// PENDING_PAY → PAID(支付成功)
// PENDING_PAY → CLOSED(超时关闭)
// PAID → DISPATCHING → FINISHED(正常出货)
// PAID → REFUNDING → REFUNDED(出货失败退款)

订单 MQ 架构的核心心法就八个字:异步解耦、消息驱动。每个服务只关心自己订阅的消息,出问题各自重试,互不阻塞。下一篇我们聊库存异步更新,看看早高峰集中购买时如何扛住并发压力。

相关推荐
微三云 - 廖会灵 (私域系统开发)1 小时前
抖店 OPC(Operational Process Control)自动化运营系统:架构、痛点与服务商商业体系分析
运维·架构·自动化
jianqiang.xue1 小时前
收官:从v0.1到“研发新常态“,一套嵌入式智能体的落地与边界
stm32·单片机·物联网·架构·esp32
阿拉斯攀登2 小时前
SpringBoot整合RocketMQ:生产者、消费者快速搭建
架构
楚兴3 小时前
ACP 到底解决了什么?让 IDE 和 Coding Agent 解耦
人工智能·后端·架构
Rescenix3 小时前
流式瀑布渐变闪烁根治-硬核技术版
架构·github
楚兴3 小时前
DeepSeek Harness 到底在做什么?拆开 Agent 的运行时
人工智能·后端·架构
贺公子之数据科学与艺术3 小时前
向量数据库实操:Milvus 与 Chroma 的 Collection、索引、相似度度量与标量过滤
架构
用户6919026813393 小时前
Agent 记忆处理机制详解:从变量数据类型到内存模型
javascript·架构
行者全栈架构师3 小时前
WorkBuddy 开放平台个人开发者接入实战:从零到 Agent 应用的完整路径
人工智能·架构·代码规范