第三方金流:教科書的三個坑,與一個沒有坑的地形
· tech
#war-story#live-commerce#payment
📑 目錄
上一章把單聚合成了三層,現在要收錢了。金流是這個系統第一次把「正確」交到別人手上——付款發生在銀行那邊,你只能透過不可靠的網路得知結果。教科書會警告你三個坑:通知會重複、會亂序、會根本不來。這章要講的是:當年這三個坑,我們一個都沒踩——不是運氣好,是設計選了一個沒有坑的地形。
當年的付款版圖
付款方式四種:兩家銀行,提供智慧轉帳和信用卡,外加一個自訂的現金付款(營運線下收款後入帳)。而且有個一開始就存在的硬需求:部分付款——轉帳單筆上限 30,000,一筆 150,000 的結帳,客人要拆五張轉帳付完。
這個需求宣判了「is_paid 布林」的死刑:錢是分批、分渠道、非同步到的。當年的資料模型是:
三個設計決定,注意它們環環相扣:
- 每個第三方一張事實表。 銀行 A 的轉帳、銀行 A 的信用卡、銀行 B、現金——各自的原始事實各自存,不揉進同一張「通用付款表」硬塞欄位。新增付款方式=新增一張事實表+一條聚合規則,「現金」就只是一個由營運標記入帳的 provider。
- payment 的狀態是派生的,讀取時才算。 已付與否=入帳事實加總對應付金額,不存在一個被 UPDATE 的狀態欄位。
- webhook 和 polling 都做,寫同一組事實表。 教科書要你在「即時但可能漏」和「可靠但慢」之間選——這裡全都要。
教科書的三個坑
先把坑講清楚,因為它們是真的,業界天天在踩。在「回調更新狀態」的世界裡:
- 重複通知:銀行重送一次「付款成功」,你的狀態就轉移兩次——輕則 log 髒掉,重則重複出貨、重複開發票。所以要設計冪等鍵。
- 亂序:「付款成功」比「訂單成立」的內部處理先到,或兩筆部分付款的通知顛倒——狀態機走錯方向,可能永遠回不來。所以要序號、要緩衝、要補償邏輯。
- 根本不來:網路丟了、銀行忘了,你的訂單卡在「等待付款」直到天荒地老。所以要主動查單兜底。
三個坑,三套防禦,每套都是額外的程式碼和額外的測試面——這是多數金流整合的日常。
沒有坑的地形
當年的系統,這三個坑的體感是:「好像沒遇到什麼問題」。把上面的架構代入,原因一目瞭然:
- 重複通知? 回調只是把「銀行 A 說這筆轉帳入帳了」這個事實寫下來——同一筆事實寫幾次,聚合出來的答案都一樣。
- 亂序? 五筆部分付款誰先到誰後到,根本不重要——事實各自落地,
SUM是讀的時候算的。 - 沒來? polling 排程會把漏掉的事實補上——而且因為冪等,polling 和 webhook 重疊也無害,雙通道互為備援。
三套防禦的成本歸零,因為沒有可以被打壞的狀態。這是整個系列「事實 append、狀態派生」主旋律的最終回收:同一個原則,在留言層教過你事實要接上路徑才算數(raw 躺在庫裡,重放從沒發動)、在庫存層給了你對帳、在金流層直接讓最兇的一類 bug 絕種。
對帳也順著這個結構分了級:某家銀行的付款記錄會帶 orders payment id,自動對回;現金靠營運標記——又是一個「自動收大宗、人工收殘量」的漏斗,跟身分章的綁定漏斗同構。
插曲:那顆大家一直想加的狀態欄位
這個「狀態讀時派生」的設計,不是全隊拍手通過的——它是被擋出來的。當年 CTO 和幾位同仁不只一次想加一顆訂單狀態欄位:一個直接可查、可篩、放在列表頁很方便的 status。我一意孤行地反對,理由每次都一樣:這個狀態本質上就是各種欄位與事實的聚合——做這顆欄位,就是做一份 cache。 cache 一旦存在,就要回答所有 cache 都要回答的問題:誰負責更新它、哪些寫入路徑要記得同步它、它歪掉的時候以誰為準。而我們當時根本沒有遇到效能問題。在真的遇到效能問題之前,不要做 cache。
這個立場不是教條地反固化——庫存章的賣出計數就是固化的派生值,因為它躺在下單的熱路徑上(每則留言都要檢查不變量),而且配了每小時重算的自我修復。兩個決定用的是同一把尺:派生值要不要固化,唯一正當的理由是量測到的讀取成本;固化了,就要同時承認它是 cache、配上重算與對帳。「查起來方便」不在正當理由清單上——方便的代價,是從此多一個會說謊的欄位。
退款:系統退到觀察者的位置
退款的故事是狀態機教訓的姊妹篇。當年認真做了退款 API——串好銀行、包好流程——結果營運人員不愛用。他們習慣直接開銀行後台按退款,快、熟、看得到餘額。
掙扎過後的決定:放手。營運去銀行按他們的,按完把訂單狀態標成「退款中」——然後排程接手,sync 退款的實際進度。系統從「執行者」退到「觀察者」:人做動作,系統對帳。
這個退位其實跟整章的架構一脈相承:退款進度也只是另一組事實,誰觸發的不重要,系統的職責是把事實追回來、把狀態派生對。硬要營運走你的 API,本質上是把自尊建在「別人要不要用你的介面」上;讓系統有能力追上現實,才是把工程花在對的地方。
反思
最好的防禦,是選地形
金流整合的文章多半在教你怎麼把三個坑守好:冪等鍵怎麼設計、亂序怎麼緩衝、補償查詢多久跑一次。這些都對,但它們是在錯的地形上打仗的技巧。當年的系統沒有這些防禦,卻也沒有這些傷——因為「事實只進不改、狀態讀時派生」讓坑本身不存在。這給我的長期教訓是:遇到一類反覆出現的 bug,先別急著加防禦,問一句:有沒有一種資料模型,讓這類 bug 沒有立足點? 防禦是利息,地形是本金。
錢的系統,謙遜比聰明重要
這章有兩次「退讓」:狀態不做轉移限制(跟著事實走)、退款不走自家 API(讓營運去銀行按)。兩次都是系統對現實低頭——而回頭看,兩次都對。錢的流動牽著銀行、營運、客人三方,你的系統永遠只是其中一個參與者;把自己定位成「所有金錢事實的忠實帳本」,而不是「金錢流程的唯一入口」,反而讓正確性更容易守住。帳本不需要控制世界,只需要如實記下世界。
「沒遇到什麼問題」是最高級的戰果
金流照理是整個系統最容易出事的地方——外部依賴最多、不可靠網路、錢的違約成本最高——但它是這個系列寫到現在最平靜的一章。連著兩章「無聊」(上一章的結帳也是),而且無聊的原因相同:前面把事實與派生的關係擺對了,後面的複雜度就長不出來。這也是工程裡最不公平的一件事:好架構的回報,以「沒有故事」的形式發放——出事救火的人人看得見,讓火燒不起來的人沒有戰功。我後來當 EM,很大一部分工作就是學會看見、並獎勵這種「沒有故事」的人。