Posted on ::

业务模型

混合支付,本质是让一笔交易同时结算两种货币:现金(强一致性)与 积分(内部资产,弱一致性)。

混合支付的公式很简单:

订单总额 = 现金支付额 + (消耗积分数 / 兑换汇率)

不过,这里会有两个问题:

  1. 汇率波动: 积分汇率是否动态?下单时100:1,支付时已变成200:1,咋搞?
    • 原则:下单锁定汇率。 在创建订单时,将当时的汇率固化在订单数据中。
  2. 抵扣上限: 一般不会允许100%积分抵扣,要配置最大抵扣比例(例如最多抵扣50%)。

分布式事务场景

用户点击支付按钮,系统要处理两个事:

  1. 调用第三方支付网关扣用户的钱
  2. 调用内部积分服务扣用户的分

两个动作,一个在外部,一个在内部,怎么保证原子性?

先扣钱,后扣分?如果钱扣了,分没扣成功(服务挂了),用户白嫖了积分?或者反过来,分扣了,钱没付,用户亏了?

TCC 还是 Saga?这种高频C端场景,上太重的分布式事务框架,性能扛不住。

推荐方案:积分预扣 + 异步实扣。


交易流程设计

流程围绕用户体验和资金安全,分三步:下单锁分、支付回调、异常回滚。

  sequenceDiagram
    actor User as 用户
    participant Order as 订单系统
    participant Point as 积分服务
    participant Pay as 第三方支付网关
    participant MQ as 延迟队列/定时任务

    User->>Order: 1. 提交订单 (现金+积分)
    Order->>Point: 2. 预扣/冻结对应积分
    
    alt 预扣失败
        Point-->>Order: 返回失败
        Order-->>User: 阻断下单,提示失败
    else 预扣成功
        Point-->>Order: 返回成功
        Order->>MQ: 投递延迟关单消息
        Order-->>User: 状态置为WAIT_PAY,唤起收银台
    end

    alt 正常支付成功
        User->>Pay: 完成现金支付
        Pay->>Order: 3. 支付成功回调 (核对金额)
        Order->>Order: 更新订单状态为 PAID_SUCCESS
        Order-)Point: 异步通知积分服务实扣积分
    else 超时未支付 / 主动取消
        MQ->>Order: 4. 触发超时关单逻辑
        Order->>Order: 订单状态流转为已取消
        Order->>Point: 发起解冻积分 (返还用户)
        Order->>Order: 释放商品库存
    end

1. 下单锁分

支付成功后再扣积分,会有并发扣减压力和超卖风险。用户提交订单时,调用积分服务进行预扣

  • 成功: 订单状态流转为 WAIT_PAY,生成支付参数,唤起收银台。
  • 失败: 阻断下单,提示用户。

2. 支付回调

用户在第三方完成支付后,支付网关回调系统。

  • 核对金额: 确保回调金额与订单现金一致。
  • 更新订单: 状态变更为 PAID_SUCCESS
  • 实扣积分: 异步通知积分服务,将预扣的积分真正扣除。

3. 异常回滚

对于下单后一直没支付或主动取消订单的:

  • 超时关单: 延迟队列触发关单逻辑。
  • 释放库存: 回滚商品库存。
  • 解冻积分: 通知积分服务,将预扣的积分返还用户账户。

系统架构分层设计

流程梳理完,接下来就是系统设计。

  graph TD
    Client[客户端] --> Agg[商城聚合层]
    Agg -- 计算“现金+积分”\n混合明细 --> Domain
    
    subgraph Domain [领域服务层]
        direction LR
        Order[订单域\n状态机维护/交易流转]
        Point[积分域\n账户增删改查/冻结解冻流水]
        Pay[支付域\n对接微信支付宝/对账退款]
    end

    Agg --> Order
    Order -. RPC调用: 预扣/实扣/解冻 .-> Point
    Order -. RPC调用: 发起支付/退款 .-> Pay

    subgraph Infra [基础设施层]
        DB[(业务数据库)]
        MQ[[消息队列 / 定时任务]]
    end

    Order --> DB
    Point --> DB
    Pay --> DB
    Order -- 异步实扣/解冻/补偿机制 --> MQ
    MQ --> Point
  • 商城聚合层: 计算“现金+积分”的混合明细。
  • 订单域: 状态机维护,负责交易流转,不处理积分细节。
  • 积分域: 负责积分账户的增删改查、冻结解冻流水。
  • 支付域: 对接微信/支付宝,处理退款和对账。

核心原则: 各领域间相互解耦,订单系统不需要知道积分是怎么计算的,只需知道这单要扣多少分。


订单状态机与一致性

混合支付最怕状态不一致,那么,用状态机来约束流转。

  stateDiagram-v2
    [*] --> CREATED: 用户提交订单
    
    CREATED --> POINTS_LOCKED: 积分预扣成功 (等待付款)
    CREATED --> CLOSED: 积分预扣失败 (下单阻断)
    
    POINTS_LOCKED --> PAID_SUCCESS: 第三方支付回调成功\n(触发异步实扣积分)
    POINTS_LOCKED --> CLOSED: 超时未支付 / 主动取消\n(触发解冻积分)
    
    PAID_SUCCESS --> REFUNDED: 发起退款完成\n(按行分摊退钱+退分)
    
    CLOSED --> [*]
    REFUNDED --> [*]
    
    note right of PAID_SUCCESS
        若支付成功但异步实扣积分失败,
        由定时任务补偿重试,保证最终一致性。
    end note

关键状态流转:

  • CREATED:订单创建
  • POINTS_LOCKED:积分预扣成功,等待付款
  • PAID_SUCCESS:钱已到账,积分已扣
  • REFUNDED:退款完成,交易关闭

一致性补偿: 引入补偿机制。遇到支付成功后扣积分失败的,就通过定时任务不断重试,保证数据最终一致性。


退款逻辑设计

混合支付退款难在分摊

假设用户买了两件商品:

  • 商品A: 100元
  • 商品B: 200元
  • 总计: 300元。用户使用了 3000积分(抵扣30元) + 270元现金。

问题来了: 用户只退商品A,系统该退多少钱?多少分?

按行分摊原则

金额和积分的分摊,在下单的时候,就提前分摊到每行商品上。

对于商品A(占总价1/3):

  • 应分摊现金: 270 * (100/300) = 90元
  • 应分摊积分: 3000 * (100/300) = 1000分

退款时,直接读取数据库中商品A的实付明细,退90元现金 + 返还1000积分。账目清晰,永不出错。


Table of Contents