积分兑换架构设计
业务模型
混合支付,本质是让一笔交易同时结算两种货币:现金(强一致性)与 积分(内部资产,弱一致性)。
混合支付的公式很简单:
订单总额 = 现金支付额 + (消耗积分数 / 兑换汇率)
不过,这里会有两个问题:
- 汇率波动: 积分汇率是否动态?下单时100:1,支付时已变成200:1,咋搞?
- 原则:下单锁定汇率。 在创建订单时,将当时的汇率固化在订单数据中。
- 抵扣上限: 一般不会允许100%积分抵扣,要配置最大抵扣比例(例如最多抵扣50%)。
分布式事务场景
用户点击支付按钮,系统要处理两个事:
- 调用第三方支付网关扣用户的钱
- 调用内部积分服务扣用户的分
两个动作,一个在外部,一个在内部,怎么保证原子性?
先扣钱,后扣分?如果钱扣了,分没扣成功(服务挂了),用户白嫖了积分?或者反过来,分扣了,钱没付,用户亏了?
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积分。账目清晰,永不出错。