下单-支付-回调时序图怎么画:同步/异步、回调、超时、补偿一次画对
从研发/测试/架构视角讲清下单-支付-回调的时序图画法:分清同步与异步边界,明确回调验签、幂等、超时重试、补偿与对账链路,并给出可交付的规范、反例修正、检查清单与 FAQ。
如果你在画“下单 → 支付 → 回调”的时序图,最容易画错的不是箭头,而是边界:
- 你把“支付成功”画成了一个同步返回,结果评审时有人问:那回调呢?
- 你画了回调,但没画验签/幂等/重试,结果测试问:重复回调怎么测?
- 你画了超时,但没画补偿/对账,结果线上出了“用户扣款但订单没变更”的事故,大家发现:图里根本没这条路。
这篇文章给你一套能直接交付给研发、测试、架构评审的画法:
- 先画一条**“可验收”的主成功链路**(每一步都有可观测点)
- 再用 alt/opt/loop 把支付链路里必须补齐的异常分支补全(超时、重复回调、补偿、对账)
- 最后用清单把“你画的图”变成“别人能照着实现/写用例/排查事故的图”
文中默认你画的是 UML 序列图(时序图),读者主要是产品/研发/测试/架构/写技术文档的人;学生作业也能套用。
1. 先明确:你要画的是哪一种“支付链路”(范围不定,图必乱)
支付相关的时序图至少有三层粒度:
- 收银台/支付发起:用户点击支付 → 跳转/唤起三方 → 返回商户页
- 订单状态流转:订单创建/锁定库存/支付中/已支付/关闭
- 资金与通知闭环:支付平台扣款成功 → 异步通知 → 幂等入账 → 对账与差错处理
建议你在图左上角写一句范围(相当于这张图的“契约”):
- “本图描述:电商订单下单后,通过支付平台完成支付,并通过异步回调确认支付结果;包含超时重试、重复回调、补偿与对账;不包含退款。”
范围写清楚,评审时就不会把“退款/分账/二清/跨境”全部塞进来。
2. 为什么很多“下单-支付-回调”时序图不专业:把它画成了流程图
流程图擅长表达“步骤”;时序图擅长表达:
- 谁调用谁(职责边界)
- 同步/异步分界(哪里必须等待,哪里只能事件驱动)
- 失败如何收敛(重试、补偿、对账、人工介入)
- 一致性策略(最终一致 vs 强一致)
支付链路是典型的“跨系统、异步、最终一致”场景,所以时序图的关键,是把控制点画出来,而不是把 UI 步骤列出来。
3. 参与者(生命线)怎么拆:用“职责边界”拆,而不是用“代码模块”拆
一张能交付的支付链路时序图,通常至少包含这些生命线:
- User:用户
- Client(App/Web/H5):客户端
- Order Service:订单服务(订单状态机)
- Inventory Service(可选):库存锁定/释放
- Payment Service(Pay Center):支付服务(你们系统内的支付聚合/收单层)
- Payment Gateway / 3rd Pay Platform:第三方支付平台(微信/支付宝/银联/Stripe 等)
- Callback Endpoint(通常也是 Payment Service):回调接收入口(同一个服务也行,但建议单独标注为“回调入口”以强调异步)
- Message Queue / Event Bus(强烈建议):异步解耦(回调入库后发布事件)
- Accounting/Ledger(视业务):记账/流水
- Notification(可选):短信/站内信/Push
- Observability(Log/Metric/Tracing):审计与监控
经验:如果你们没有 MQ,也可以画“Outbox 表 + 异步投递器”,效果类似:把“回调线程”从“订单状态变更”里剥离出去。
4. 先画主成功链路:把“同步”和“异步”分成两段
很多争论来自一句话:“支付成功到底是什么时候?”
在工程上,通常分两种口径:
- 用户体验口径:用户在收银台看到“支付成功”,并返回商户页
- 商户系统口径(更重要):商户系统收到并处理异步回调/通知后,才把订单标记为已支付
因此主链路建议拆成两段:
4.1 下单与发起支付(同步链路)
- User → Client:提交订单(商品、地址、优惠)
- Client → Order Service:CreateOrder
- Order Service → Inventory Service:ReserveStock(可选)
- Order Service → Payment Service:CreatePayment(生成支付单/支付流水,状态 = INIT)
- Payment Service → 3rd Pay Platform:Prepay/UnifiedOrder(拿到 prepayId / paymentIntent)
- Payment Service → Client:返回支付参数(sdk 参数/跳转 url)
- Client → 3rd Pay Platform:唤起/跳转支付
- 3rd Pay Platform → Client:展示支付结果(成功/失败/取消)
- Client → Payment Service(可选):QueryPayment(前端轮询查询,只能用于展示,不要作为最终确认)
关键标注:
- 第 5 步是同步 RPC(你发起请求拿到支付凭证)
- 第 8 步只是“用户看到成功”,不等于你系统已确认
4.2 支付平台通知商户(异步回调链路)
- 3rd Pay Platform → Callback Endpoint:PaymentNotify(异步 HTTP 回调)
- Callback Endpoint:VerifySign(验签)+ Parse
- Callback Endpoint → Payment Service:UpsertPaymentResult(幂等写入支付结果,状态 = SUCCESS)
- Payment Service → MQ/Event:PublishPaymentSucceeded(事件驱动)
- Order Service ← MQ/Event:ConsumePaymentSucceeded
- Order Service:ChangeOrderStatus(PAID)(幂等)
- Order Service → Inventory Service:ConfirmReservation(或扣减)
- Order Service → Notification:SendPaid通知(可选)
你可以在图上用注释写清楚:
- “订单变更以回调为准;前端查询仅用于体验兜底。”
这句话能减少 80% 的评审争论。
5. 回调必须画的 4 个控制点:验签、幂等、顺序、应答
5.1 验签(VerifySign)
回调里最常见的安全坑:
- 没验签/验签错误 → 被伪造通知
- 用错误的 key/证书 → 线上偶发失败
时序图里建议把“验签失败”单独画一个 alt 分支(见第 6 节)。并且把“验签用的材料”写进注释:
- “使用平台公钥/证书链验签;验签通过才入库。”
5.2 幂等(Idempotency)
同一笔支付,回调可能会:
- 重复到达(平台重试、网络抖动)
- 乱序到达(先到退款通知、后到支付成功,取决于平台)
因此 Payment Service 入库时要有幂等键:
- 推荐:
pay_platform + transaction_id或merchant_order_no + notify_id
在图里用一条注释标注:
- “以 transaction_id 做唯一键;重复通知直接返回 success(不重复发事件)。”
5.3 顺序与状态机(State Machine)
支付单/订单不是“写个 boolean”。你至少要画出状态流转原则:
- INIT → PROCESSING → SUCCESS/FAILED/CLOSED
- 订单:CREATED → PAYING → PAID / CLOSED
图里可以不用画状态图,但要在关键消息上标注“状态改变点”。否则别人不知道你们在哪一步落状态。
5.4 回调应答(Ack)
很多平台要求你在回调里返回特定格式表示“已收到”。如果你处理完成后才 ack,可能因为内部慢导致平台重试暴增;如果你先 ack 再处理,又可能丢数据。
推荐做法(可在图里体现):
- 回调入口:快速验签 + 幂等入库(或写入 outbox)
- 成功入库后立即 ack
- 后续业务变更(订单/库存/通知)走异步事件
这就是“把回调线程从业务线程里剥离”。
6. 必须补齐的异常分支:用 alt/loop/opt 画到“可测试”
下面这些分支,建议你至少选与你业务相关的 6~8 个补齐;否则图看起来“完整”,但落不到测试用例。
6.1 alt:预下单失败(下单阶段失败)
场景:支付平台返回错误(参数不合法、商户配置错、风控拦截)。
- Payment Service ← 3rd Pay Platform:PrepayFail(code, msg)
- Payment Service → Order Service:MarkPaymentInitFailed
- Order Service:保持订单为 PAYING 或直接 CLOSED(取决于产品策略)
反例:只画“返回失败给前端”,但订单依旧停留在 PAYING,后续没人收敛。
修正:给订单一个明确状态,并建立“超时关闭”任务(见 6.3)。
6.2 alt:用户取消/支付失败(用户侧结果)
用户取消并不代表最终没扣款(极端情况下可能扣款成功但页面没展示)。所以:
- 用户结果只影响“前端展示”
- 系统确认必须靠“回调/对账”
图里可以标注:
- “Client 展示 Cancel;Order 仍保持 PAYING,等待回调/超时收敛。”
6.3 loop:订单支付超时(关单/释放库存)
场景:用户发起支付后长时间未支付。
画法:
- Order Service → Scheduler:Delay(15min)
- Scheduler → Order Service:CloseOrderIfUnpaid
- Order Service → Payment Service:ClosePayment(可选:调用平台关单)
- Order Service → Inventory Service:ReleaseStock
关键点:
- “关单必须幂等”
- “关单与回调可能并发到达”(见 6.6)
6.4 alt:回调验签失败
场景:伪造回调、证书更新、时钟偏差。
- Callback Endpoint:VerifySignFail
- Callback Endpoint → 3rd Pay Platform:AckFail(或 HTTP 4xx/5xx)
- Observability:记录告警(不要只写日志)
修正建议:
- 把验签失败计入指标(按商户号/渠道)
- 加上“证书/密钥轮换”说明(文档里也要写)
6.5 loop:支付平台重复回调(平台重试)
场景:平台因为没收到 ack 或网络抖动,重复通知。
- 3rd Pay Platform → Callback:Notify
- Callback:幂等判断(已处理)
- Callback:直接 AckSuccess(不重复发事件)
反例:重复通知导致订单重复发货。
修正:在时序图里明确“事件只发布一次”“消费端也要幂等”。
6.6 alt:回调与关单并发(经典竞态)
场景:超时任务刚关单,下一秒回调到达。
可用 alt 表达两种顺序:
- A:先回调成功 → 订单已 PAID → 超时任务执行时发现已支付,跳过
- B:先关单 → 订单 CLOSED → 回调到达时如何处理?
工程上常见策略:
- 若平台交易已成功:允许“闭单后支付成功”转为 PAID(并记录告警/补偿)
- 或者:若 CLOSED 后收到 SUCCESS,则进入“异常状态”交给人工/补偿流程(少见但也有)
你在图里必须选一个,不然别人实现时各写各的。
6.7 opt:回调到达但下游(订单服务)处理失败(需要补偿)
场景:Payment Service 已成功入库并 ack,但事件消费失败(订单服务宕机、DB 锁)。
画法:
- Payment Service:事件投递失败 → 重试(loop)
- 或 Outbox:定时扫描未投递记录 → 重投
再补一条:
- Order Service:消费幂等(根据 paymentId/orderId 去重)
这就是“补偿”在时序图里的位置:不是一句“补偿机制”,而是具体的重试与兜底路径。
6.8 opt:对账与差错处理(T+1/准实时对账)
对账常被忽略,但它是支付系统最后的安全网。
典型对账链路:
- Reconciliation Job → 3rd Pay Platform:DownloadBill/QueryTrade
- Reconciliation Job → Payment Service:Compare
- alt:发现“平台成功但商户未入账” → 触发补单/补发事件
- alt:发现“商户成功但平台失败” → 触发退款/人工介入
对产品/架构评审来说,这一段是“你们系统靠谱不靠谱”的关键。
7. 消息命名与注释:让图能落到接口、日志、测试用例
画图时不要只写“请求/响应”。建议用“动词 + 对象 + 关键字段”的方式命名消息:
- CreateOrder(orderNo, items)
- CreatePayment(paymentId, amount, channel)
- Prepay(paymentId)
- PaymentNotify(transactionId, status)
- UpsertPaymentResult(transactionId, status)
- PublishPaymentSucceeded(paymentId)
- ChangeOrderStatus(orderNo, PAID)
并在关键节点加注释(对评审很有用):
- “幂等键:transactionId”
- “回调 ack:入库成功即 ack;业务走异步”
- “订单状态以回调为准;前端查询仅展示”
你会发现:这种命名方式直接对应到 API 文档与日志字段,写用例也容易。
8. 常见反例(看起来对,但线上一定出事)+ 修正方式
反例 1:把“支付成功”画成同步返回
症状:图里只有“Client → Pay → 返回 success”,没有异步通知。
后果:
- 用户支付成功但返回页失败 → 商户系统永远不知道成功
- 订单状态无法闭环,只能靠客服/人工
修正:
- 主链路拆成“发起支付(同步)+ 回调确认(异步)”两段
- 用 MQ/outbox 解耦回调线程
反例 2:回调里直接改订单 + 发货
症状:Callback 线程里调用 Order Service 改状态并触发发货。
后果:
- 回调超时导致平台重试 → 重复发货
- 回调入口被下游拖慢 → 回调堆积雪崩
修正:
- 回调入口只做:验签、幂等入库、快速 ack
- 后续动作走事件(订单消费、发货消费都做幂等)
反例 3:只做“回调幂等”,不做“消费幂等”
症状:Payment Service 去重了,但 Order Service 消费事件时没去重。
后果:重放/重复投递仍会导致二次变更。
修正:
- 每个消费端都要幂等:用 orderNo/paymentId 作为幂等键
反例 4:没画“关单与回调并发”
症状:图里只有超时关单,没有并发顺序。
后果:
- 线上出现 CLOSED 与 PAID 打架
- 数据修复成本极高
修正:
- 用 alt 画出两种顺序,并在注释里写清楚“最终裁决规则”
9. 交付检查清单(研发/测试/架构评审都能用)
你画完图后,按下面清单自检。能全部打勾,这张图基本就“可交付”。
9.1 参与者与边界
- 是否区分了 Order Service 与 Payment Service(或至少在图上标注职责)
- 是否明确第三方支付平台是外部系统(异步回调)
- 是否标注了 MQ/outbox(如果没有,是否说明回调线程如何避免阻塞)
9.2 主链路
- 是否拆成“发起支付(同步)+ 回调确认(异步)”
- 订单状态变更点是否明确(PAYING→PAID/CLOSED)
- 是否标注“前端查询仅展示,不作最终确认”
9.3 回调安全与一致性
- 是否画了验签(失败分支也画了)
- 是否写了幂等键(transactionId/notifyId)
- 是否说明 ack 时机(入库成功即 ack)
9.4 异常与补偿
- 是否画了超时关单(含库存释放)
- 是否画了重复回调(loop/幂等)
- 是否画了关单与回调并发(alt 两种顺序)
- 是否画了下游失败后的重试/补偿(Outbox/MQ 重投)
- 是否画了对账差错处理(至少说明入口与动作)
9.5 可测试性
- 每个 alt/loop 分支都能对应到一条或多条测试用例
- 关键失败都有可观测字段(错误码、状态、日志/指标)
10. 什么时候需要“再画一张图”(把复杂度拆开)
如果你的支付链路包含以下任意一项,建议拆成两张或三张图:
- 同时支持多渠道(微信/支付宝/银联/海外)且差异大
- 涉及分账、二清、代扣、订阅扣款
- 强依赖风控挑战(3DS、短信验证)
- 退款/撤销链路复杂
拆图的原则:
- 一张图回答一个问题
- 主链路清晰优先,细节用“子图链接/引用”补充
11. 用工具把时序图画得更快、更规范(并且能导出交付)
如果你不想从零对齐箭头样式/片段边框/命名规范,建议用在线时序图生成器把“文字交互”快速变成可交付图。
在 Sequence Diagram Generator 这类工具里,一般会有这些对工程同学更省时间的能力:
- 自动排版:消息多了也不至于一团糟,减少手动拖拽
- 左侧编辑器可以**点选生命线/消息/组合片段(alt/opt/loop/par)**快速插入,避免记语法
- 右侧实时预览:改一行文本立刻看到图,评审时能当场调整
- 一键导出 SVG/PNG/JPEG/draw.io:
- SVG 适合放到文档/PRD/设计评审(清晰)
- PNG/JPEG 适合群里发、贴到 wiki
- draw.io 方便二次编辑、统一到团队图库
- 支持 AI 生成:你把“下单-支付-回调”的文字描述/接口列表丢进去,让它先生成一个可用的草图,再由你补齐异常分支
你可以把这篇文章的主链路复制出来,直接在这里生成一版初稿,然后对照第 9 节清单补齐:
12. FAQ:评审时最常被问的 8 个问题
Q1:能不能只靠前端返回判断支付成功?
不建议。前端返回只能代表“用户看到了什么”,不代表“商户系统确认了什么”。网络抖动、跳转失败、App 崩溃都会让你错过结果。
工程上更稳的做法:
- 以异步回调作为最终确认
- 前端查询/轮询只用于体验兜底
- 对账作为最后安全网
Q2:回调必须用 MQ 吗?
不是“必须”,但强烈建议。没有 MQ 也要有等价机制:
- Outbox 表 + 异步投递器
- 先入库再异步处理
核心目标是:回调入口快、可重试、不被下游拖垮。
Q3:回调 ack 是先 ack 还是后 ack?
折中方案最常见:
- 验签 + 幂等入库成功后 ack
- 后续业务处理走异步
这样既不会丢数据,也不会因为下游慢导致平台疯狂重试。
Q4:订单状态以回调为准,那用户支付后页面一直转圈怎么办?
做法:
- 前端返回商户页后,轮询 QueryPayment/QueryOrder(展示用)
- 后端收到回调后尽快落库并发布事件
- 超过阈值仍未确认时提示“正在确认,请稍后在订单列表查看”
把“最终一致”讲清楚,产品体验也能被接受。
Q5:怎么表达“支付成功但订单变更失败”的补偿?
在时序图里体现:
- Payment Service 事件投递失败的重试(loop)
- Order Service 消费失败的重试/死信队列
- 对账任务发现差错后的补单/补发事件
一句“有补偿”不算,必须画出“补偿从哪触发、怎么执行、如何幂等”。
Q6:要不要把“发货/开通权益”也画进来?
如果你的读者是架构评审或跨团队协作,建议画,但可以放在“支付成功事件”之后,以事件驱动方式表达;如果只是讲支付闭环,建议拆成子图。
Q7:回调里要不要写数据库事务?
可以在注释里写:
- “回调入库与 outbox 写入同一事务”
但不建议把所有事务细节画成消息,除非你们正在做一致性方案评审。
Q8:对账一定要 T+1 吗?
不一定。看业务风险与平台能力:
- 高风险业务:准实时对账(分钟级)
- 普通电商:T+1 足够
关键是:对账是最后的安全网,哪怕频率不高也要有。
13. 你可以直接套用的“支付链路文字版模板”(方便丢给 AI/生成器)
把下面这段复制到你的时序图工具里,然后按你们系统的实际服务名改一改:
- 用户提交订单 → OrderService 创建订单(PAYING)并锁库存
- OrderService 调用 PaymentService 创建支付单(INIT)
- PaymentService 调用第三方支付预下单,返回支付参数
- 客户端唤起支付完成,返回商户页(仅展示)
- 第三方异步回调商户 Callback,回调入口验签并幂等入库
- PaymentService 发布“支付成功”事件(Outbox/MQ)
- OrderService 消费事件,幂等把订单改为 PAID,并确认库存/发通知
- 超时任务关单:到期未支付则关闭订单、释放库存,并可调用平台关单
- 对账任务:对比平台账单与商户流水,发现差错触发补单/人工处理
再把第 6 节的异常分支逐个补进去,你就能得到一张“可交付”的时序图。
14. 再给 2 个落地提醒(写文档的人会很受用)
- 把错误码写进图注释:比如“验签失败”“金额不一致”“订单不存在”。测试会感谢你。
- 把观测点写出来:回调 QPS、验签失败率、重复回调比例、订单 PAYING 超时比例、对账差错数。否则出了事故只能靠猜。
如果你想把这张图做成“团队统一模板”,建议先用生成器产出一个主链路版本,再逐步把你们真实遇到的事故分支沉淀进去。最开始不需要一步到位,但每次线上出一次坑,就把那条分支补进图里,这张图会越来越值钱。
再放一个入口,方便你直接生成并导出到文档里: