支付流程图指南
怎么画一张经得起生产环境考验的支付流程图:钱到底在哪一刻动、为什么跳回来那一下不算数,以及三个可直接改的例子。
一、什么是支付流程图
支付流程图 画的是一笔订单怎么变成你账上的钱 —— 以及更有用的那部分:这条路上每一个它不再是直线的地方。
多数人画一次就放弃了:五个方框,顾客付款、渠道通过、订单发货,然后得出结论「不值得画」。这个版本与其说是错的,不如说它描述的是整条流程里那百分之二从来不出事故的部分。真正出事故的是:一笔支付不是一个事件。它至少是两个 —— 授权,发卡行同意冻结这笔钱;和 扣款,钱真的动了 —— 而这两者可能隔着好几天。中间还夹着一件事:顾客的银行可能会在一个你控制不了的页面上弹出验证,而顾客可能就再也没回来。
五个方框还藏了另一件事:「已支付」不是终点。钱会倒着流,而且时间尺度是卡组织定的,不是你定的:下周的一次退款、一件商品退回时的部分退款、销售之后最长 120 天内提起的一次拒付。一个止步于 Paid 的支付状态机,注定会在生产环境里、由值班的人、在压力下被临时改掉。
所以这张图诚实的主题不是结账。它是「顾客以为自己付了」和「我们知道自己收到了」这两件事之间的那道缝 —— 以及缝里能发生的一切。
二、一张支付流程图要画出哪些东西
六样。漏掉第二样,这张图描述的就是一个你并不拥有的系统。
- 授权和扣款是两步,分开画。 就算你是立即扣款,也把两步都画出来 —— 因为哪天有人问 「能不能发货时才扣」,答案会是改一条箭头,而不是重做。用大白话标出各自的含义:授权是一次带有效期的冻结,扣款是真正的转账。一笔从未扣款的授权不会大声失败,它大约一周后无声地失效。
- 结果真正是从哪儿来的。 浏览器跳回你的返回页,只说明顾客的会话结束了,别的什么都不说明。权威答案是渠道带外、异步送来的,而且不管顾客的浏览器活没活下来它都会来。两条箭头都画,并标出你被允许相信的是哪一条。把跳转当成答案,是结账带着窟窿上线的第一大原因。
- 那个离开你的流程的步骤。 3-D Secure、跳银行 App、钱包弹层 —— 顾客去了一个不属于你的地方,而且可能不回来。把它画成一个有 三个 结果的分支,不是两个:通过、拒绝、以及放弃。第三个结果压根没有箭头指回你这边 —— 而这正是它必须出现在图上的理由。
- 一个幂等键,而且取自稳定的东西。 双击的按钮、重发的请求、刷新的标签页,对顾客是同一笔,对他的卡是三笔。这个键必须来自订单,而不是时间戳或者每次尝试新生成的随机值 —— 一个重试时会变的键不叫键。把它在哪儿生成画出来,它能不能扛住重试就一目了然。
- 「已支付」之后的一切。 全额退款、部分退款、争议、拒付,以及你打赢的那次拒付。这是钱倒着流的地方,也是状态机里通常被漏掉的那一半 —— 因为结账那一周没人在想这些。一个有用的检验:如果你的图里没有任何一个「销售之后三个月还能进入」的状态,那就是漏了。
- 金额,在每一个它可能不一致的位置。 授权额、扣款额、退款额、结算额是四个数,而它们不总是同一个。部分扣款、部分退款、货币换算、渠道手续费,每一样都会把它们拉开。这些不必全画进主图,但你必须知道自己
orders.total那一列装的是哪一个 —— 而图正是这个问题被大声问出来的地方。
这是几乎所有人第一次都会画的骨架,而它里面就带着那个 bug。每条箭头单独看都对。问题是整件事是一个同步请求:订单是在「刷卡」的同一次调用里被标记成已支付的。
这个形状只描述了一种支付 —— 发卡行秒过、没有任何验证、回程一切顺利。这在真实结果里是少数,而在欧洲,对多数交易它甚至不是合法的默认路径。它没地方放银行的验证页,也没地方放一个「顾客关掉标签页四秒后才到」的答案。
看 Mermaid 源码
sequenceDiagram
participant U as Customer
participant App as Your App
participant PSP as Payment Provider
U->>App: submit the payment form
App->>PSP: charge the card
PSP-->>App: approved
App->>App: mark the order as paid
App-->>U: redirect to the success page三、支付流程图怎么画
三步。第一步决定这张图讲的是什么 —— 而它很容易以一种「感觉很高效」的方式画错。
第 1 步 · 画钱,不要画界面
第一反应通常是照着用户看到的顺序走一遍:购物车、地址、支付方式、确认、提交。这五个界面里有四个和「你能不能收到钱」毫无关系,把它们画上去,只会把真正要紧的部分挤到角落。
选参与者只用一个判据:这个方框能不能持有这笔钱,或者拒绝把它放出来? 对一笔卡支付,这给你四个:顾客、你的应用、你的支付渠道、发卡行。发卡行是最常被漏掉的那个,而它恰恰是那个说不、要求验证、或者三个月后把钱划回去的角色。它不在图上,每一次拒付都会变成一条从渠道里冒出来、没有解释的箭头。
支付方式之前的一切 —— 购物车、运费、税 —— 属于另一张图,如果它真的属于任何一张的话。这张图从「金额已定、马上要向某人收这笔钱」的那一刻开始。
第 2 步 · 标出你被允许相信的那条箭头
画完的图上会有两条箭头看起来都像「支付成功了」。一条是顾客的浏览器落到你的返回页。另一条是渠道带外告诉你的服务器,这笔 intent 成功了。只有第二条算证据。在图上用文字标出来,别让人靠箭头方向自己推。
理由不在于微妙,而在于:浏览器根本不是这笔支付的参与者。它只是一个碰巧在场了一段时间的旁观者。它可能在验证页被关掉、在电梯里断网、是一个被系统杀掉的移动 App,或者干脆是某个从没付过钱的人拿到 URL 之后打开的。上面每一种情况里,钱都照样动了,而唯一知道这件事的,是渠道那条异步消息。
实际后果是一个多数初稿没有的形状:返回页读当前状态,如果还没看到结果就说 「我们正在确认你的付款」,而履约挂在那条带外消息上。这在图上是两条箭头、在代码里大约十五行,而没有它们的版本,失败方式恰好是最难复现、最贵发现的那种 —— 钱收了、订单没建、没人知道,直到顾客写信来。
那条带外消息就是一个 webhook,所以那套规矩全都适用 —— 对原始 body 验签、按事件 id 去重、在干活之前先确认。在这张图上它是一条箭头;它的内部是另一张图,本页底部有链接。
第 3 步 · 画出来
用句子描述,让 text2diagram 排版 ——「顾客下单,我们用订单号当幂等键创建一个 payment intent,顾客在渠道那边输入卡号,发卡行可能要求 3-D Secure 验证,然后渠道通知我们的服务器,我们把订单标记为已支付」,还回来的是一段分支已经嵌好、可以直接改的 sequenceDiagram。
或者把下面三张打开、改掉参与方名字。值得留下的是分支结构。
四、三个支付流程图例子
三张图:一次画对的结账、一笔支付的完整生命(包括几个月后才发生的部分),以及一次没成功的扣款该怎么判。
例 1 · 卡支付结账,验证和确认都放在正确的位置
底下那条 Note 就是这张图的全部意义。末尾附近到达的两条箭头都长得像成功,但只有一条是。
三个细节值得原样抄。幂等键是 订单号,在任何东西发出去之前就生成 —— 于是双击、重发、刷新页面都会归到同一个 intent,而不是三笔扣款。一个每次尝试新生成的键在图上长得一模一样,但什么都防不住。
卡号从顾客 直接 到渠道,一次都不经过你的服务器,这是你合规范围能保持很小的原因。在图上这表现为一条跳过了一个参与者的箭头,而它值得精确画出来,恰恰因为错误的版本 —— 卡号先 POST 到你的后端、再转发出去 —— 作为一段时序看上去同样合情合理。
还有,订单是在 渠道到你服务器 的那条箭头上被标记为已支付的,不是在它上面那条跳转上。这两条箭头的区别,就是「一个扛得住关标签页的结账」和「一个悄悄弄丢已付款订单的结账」的区别。
看 Mermaid 源码
sequenceDiagram
autonumber
participant U as Customer
participant App as Your App
participant PSP as Payment Provider
participant Bank as Issuing Bank
U->>App: place the order
App->>App: create the order, status awaiting_payment
App->>PSP: create a payment intent, idempotency key is the order id
PSP-->>App: intent id plus a client secret
App-->>U: payment form bound to that intent
U->>PSP: card details, they never reach our server
PSP->>Bank: authorization request
alt the issuer wants a challenge
Bank-->>PSP: 3-D Secure required
PSP-->>U: challenge screen hosted by the bank
U->>Bank: approve in the banking app
Bank-->>PSP: authenticated
else no challenge needed
Bank-->>PSP: authorized
end
PSP-->>U: send the browser back to our return page
PSP->>App: payment_intent.succeeded
App->>App: mark the order paid, then fulfil it
Note over PSP,App: this arrow is the truth - the redirect above is only a hint例 2 · 一笔支付的完整生命
用状态图,因为一笔支付是一个会变老的对象,不是一段对话。谁觉得活儿到结账就结束了,就把这张图摆到他面前 —— 图上大约一半描述的是「顾客早已忘了这笔订单之后」才发生的事。
Authorized 和 Captured 是两个状态,这一点就算你立即扣款也要保留。备注写了原因:授权有有效期,而且它失效时不通知任何人。一个要九天才发货的仓库拿到的不是一笔失败的支付,而是一笔从来没真正扣过钱的订单。
Expired 值得单列一个状态,而不是画一条箭头进 Declined,因为这两者的处理完全不同 —— 一个顾客需要换张卡,另一个需要被请求「为一件他已经同意买的东西再付一次」。
Disputed 在你打赢时有一条边回到 Captured,这是最常被忘掉的分支,也是让这张图真正可运维的那一条:一笔支付可以在销售三个月后、按别人的时间表,离开一个你以为是终态的状态。如果你的模型里没有任何东西能做到这件事,那你的模型对「支付是什么」的理解就是错的。
看 Mermaid 源码
stateDiagram-v2
[*] --> RequiresPayment: order placed
RequiresPayment --> RequiresAction: the issuer asks for 3-D Secure
RequiresAction --> Authorizing: the customer approved it
RequiresPayment --> Authorizing: no challenge needed
Authorizing --> Authorized: funds held, not moved
Authorizing --> Declined: the issuer said no
Authorized --> Captured: we ship, or we charge straight away
Authorized --> Expired: never captured, the hold is released
Captured --> PartiallyRefunded: one item came back
PartiallyRefunded --> Refunded: the rest came back
Captured --> Refunded: refunded in full
Captured --> Disputed: chargeback filed, up to 120 days later
PartiallyRefunded --> Disputed: chargeback filed
Disputed --> Won: our evidence was accepted
Disputed --> Lost: the funds are pulled back plus a fee
Won --> Captured: the money stays with us
Declined --> [*]
Expired --> [*]
Refunded --> [*]
Lost --> [*]
note right of Authorized
an authorization is a promise with an expiry
seven days is typical and it lapses silently
end note例 3 · 扣款没成功时怎么办
用流程图,因为这一张是一次判断而不是一段时序 —— 而且做判断的代码,是在顾客早已离开之后才跑的。
软拒付 和 硬拒付 的分野就是这张图的全部。余额不足、发卡行短暂不可达,意思是 过会儿再试,可能就成了;卡被盗刷、账户已注销、或者干脆一句「不予承兑」,意思是 下周它照样以同样的方式失败。重试第二种不只是没用:卡组织会盯你的授权成功率,而对着一堆死卡持续重试,正是一个商户号开始引人注意的方式之一。
注意订阅和单次结账从同一个拒付走的是不同的路。订阅续费没人盯着,所以按计划重试是正确行为;而结账 有人 盯着 —— 在他盯着转圈的时候偷偷重试,比立刻告诉他并保住购物车要糟得多。
最常被漏掉的是重试用尽那条分支。取消订阅并通知顾客 是个不起眼的方框,而没有它,失败模式是:一个账号安安静静地继续为一个四个月没付过钱的顾客服务。
看 Mermaid 源码
flowchart TD
A[A charge attempt comes back declined] --> B{What kind of decline}
B -->|Insufficient funds, issuer unavailable| C[Soft decline]
B -->|Stolen card, closed account, do not honour| D[Hard decline]
B -->|Authentication required| E[Send the customer to a challenge]
C --> F{Is this a subscription}
F -->|Yes| G[Retry on day 3, day 5 and day 7, then stop]
F -->|No| H[Tell the customer now and keep the cart]
G --> I{Did one of them go through}
I -->|Yes| J[Captured]
I -->|No| K[Cancel the subscription and email the customer]
D --> L[Never retry, ask for a different card]
E --> M{Did they finish the challenge}
M -->|Yes| J
M -->|No| H把手指按在验证页上,然后关掉标签页
把画完的结账图拿来,找到顾客离开去银行验证页的那条箭头,手指按在那儿。现在假定:顾客通过了验证,然后关掉了标签页 —— 验证成功了,浏览器再也没回来。
只用图上的箭头,把你的系统从这一刻起会做什么走一遍。如果答案是 「渠道的消息到达,我们把订单标记为已支付,履约,顾客收到邮件」,那这个设计立得住,关标签页这件事无关紧要 —— 这正是正确的结果。如果走这一遍需要你说出「然后用户会回到成功页」这句话,那你就找到了一个真的窟窿:钱动了,而图上没有任何一条箭头会创建这张订单。
这不是罕见情况。这是一个在火车上用手机、并且完全照你说的做了的顾客。在流程离开你控制范围的那三个位置各做一次这个手指测试 —— 存在的窟窿,一定就在其中之一。
常见问题
授权和扣款有什么区别?两个都要画进图里吗?
授权 是发卡行同意冻结一笔金额,扣款 是钱真的动了。就算你是立即扣款,两者也都该画进图里 —— 因为它们之间那条箭头,正是后来一整类需求的落点:发货时扣款、缺货时调整金额、扣款少于授权额。让团队意外的细节是:授权会 过期,通常七天左右,而且是无声的 —— 什么都不会失败,冻结就是消失了。如果你的履约可能超过这个时间,图上就需要一条从
Authorized到过期状态的路径,以及「到了那儿怎么办」的决策 —— 否则你会以「一笔已发货但从未扣款的订单」的形式发现它。顾客跳回我的站点时,能不能就把订单标记为已支付?
不能。那次跳转告诉你的是「顾客的浏览器到达了一个 URL」,这和 钱动了 不是同一个论断。浏览器可能在验证页被关掉、被系统杀掉、断网,或者因为有人分享了链接而落到你的返回页。前几种情况里支付可能已经成功而你的跳转处理压根没跑;最后一种情况里它跑了,背后却根本没有支付。履约要挂在渠道的 异步确认 上、并且验签,返回页只做一件事:读当前状态,还没看到结果就说 「正在确认你的付款」。在图上这是末尾两条互不相同的箭头 —— 如果你只有一条,那就是 bug。
3-D Secure 或 SCA 在支付流程图上怎么画?
画成一个 有三个结果的分支,不是两个。发卡行可能要求验证也可能不要求,所以第一层分叉是 要验证 / 不要验证;在要验证那一支里,顾客可能通过、失败、或者 放弃 —— 第三个才是要紧的,因为它没有任何箭头回到你这边。把验证画成发生在银行自己的页面上 —— 这正是你的会话可能撑不过去的原因,也是结果必须经由渠道而不是浏览器到达你的原因。这么画会自然掉出两件事:返回页不能作为事实来源;以及支付需要一个「正在等顾客的银行」的状态,它既不是待处理也不是失败。
支付状态机该有几个状态?
八到十二个 左右,不过比数量更有用的是一个形状检验:如果没有任何状态是「销售三个月后还能进入」的,那就是漏了 —— 拒付最长可以在 120 天后提起。少于八个,通常是把授权和扣款合并了,或者丢掉了部分退款。多于十二个,检查是不是有步骤伪装成状态混了进来:
SendingReceipt是系统正在 做 的事,不是支付所 处于 的状态,那类东西该待在时序图里。另外把支付的状态和订单的状态分开 —— 一笔订单可以是已送达而它的支付是争议中,想用一个状态机装下两者,最后会得到已送达但已退款这种状态。哪些失败的支付该重试,多久一次?
重试 软拒付 —— 余额不足、发卡行不可达、临时冻结;永远不要重试 硬拒付,也就是卡被盗刷、账户注销、或者直接拒绝,因为它们下周会以同样的方式失败。这个区分不只是礼貌问题:卡组织盯着你的授权成功率,对着死卡反复捶打是让商户号被审视的一种方式。订阅场景下,一周内三次 —— 第 3、5、7 天 —— 是个常见形状,而图上必须画出来的是重试在哪儿 停止、停止之后怎样,因为那条分支才是通常缺的那条。交互式结账则完全不要静默重试:顾客就在现场,立刻告诉他,并保住购物车。
免费吗?
免费。没登录每天 20 次,登录后每天 500 次。把本页任何一个例子在编辑器里打开完全不计次 —— 那条路径根本不打模型。