金流串接實戰:冪等、對帳與 Webhook 的那些坑
後端工程師,整合過多家金流(信用卡、第三方支付)的下單、退款與對帳流程。
金流是那種「平常沒事、出事就是錢」的系統。我整合過好幾家金流商,從信用卡到第三方支付,最深的體會是:金流串接真正的難度不在「打 API 送出付款」,而在處理各種「我不確定到底成功了沒」的中間狀態。這篇講幾個你一定會遇到的坑。
冪等是金流的第一原則
使用者在結帳頁狂點「付款」、手機網路斷線後重試、前端 timeout 但後端其實成功了——這些都會讓同一筆訂單的付款請求送出多次。如果你沒做冪等,使用者就會被重複扣款,然後你就會收到客訴和退款工單。
我的作法:
- 每筆付款請求在進來時就帶一個 idempotency key(通常用訂單編號,或前端產生的 UUID)
- 後端在建立付款前,先用這個 key 嘗試寫入一筆「付款意圖」記錄,並對 key 做唯一索引
- 如果 key 已存在,直接回傳上一次的結果,不再向金流商送第二次
關鍵在於「先佔位、再執行」:唯一索引的衝突就是你的併發保護。不要用「先查有沒有、沒有才寫」這種 check-then-act,那在併發下一定會破。
Webhook 一定會晚到、重送、亂序
金流商的付款結果常常是非同步的:你送出請求拿到「處理中」,真正的結果透過 webhook 回拋。而 webhook 有三個你必須假設一定會發生的特性:
- 會重送:同一個事件可能收到好幾次
- 會晚到:有時候使用者都已經在結果頁等了,webhook 還沒到
- 可能亂序:付款成功和退款的通知順序不保證
對應的設計原則:
- Webhook handler 必須冪等,用金流商給的交易序號去重
- 不要相信 webhook 的「順序」,要用狀態機判斷合法的狀態轉移(例如已退款的單收到「付款成功」要忽略)
- Webhook 收到後先落地再處理:先把原始通知存起來回 200,再非同步處理。不要在 handler 裡做一堆事讓金流商等到 timeout,它會把你的成功通知判定為失敗而重送
還有一個血淚教訓:一定要驗簽。Webhook 端點是公開的,任何人都能打。沒驗簽等於讓別人偽造「付款成功」通知。
主動查詢,不要只依賴 webhook
Webhook 會掉。網路抖動、你的服務剛好在部署、對方系統異常,通知就這樣不見了。如果你的付款狀態完全依賴 webhook,就會有訂單永遠卡在「處理中」。
所以一定要有一個主動對帳機制:對於停在中間狀態超過 N 分鐘的訂單,定期主動去金流商查詢真實狀態,把狀態補正。Webhook 是「快」,主動查詢是「保證最終一致」,兩個都要有。
每日對帳:抓出帳不平的單
就算上面都做了,你還是需要每天跟金流商的對帳檔比對一次。原因很簡單:你系統認為的成交,和金流商實際請款的金額、筆數,中間任何一個環節的 bug 都可能造成落差。
對帳要比對的至少有:
- 筆數是否一致
- 每筆金額是否一致
- 你有、對方沒有的單(可能你誤判成功)
- 對方有、你沒有的單(可能 webhook 全掉了)
把這個做成每日自動跑、有差異就告警,你才能在「使用者發現」之前先發現問題。
金額用整數
跟撮合引擎一樣,金流的金額也是用最小單位的整數來存(例如台幣的「分」,或直接用 decimal 型別),絕對不要用 float。對帳時一分錢的誤差都是大事。
小結
金流系統的可靠性不是來自「正常流程寫得多漂亮」,而是來自你對異常流程的處理:
- 冪等用唯一索引「先佔位」
- Webhook 假設它會重送、晚到、亂序,且一定要驗簽
- Webhook 之外要有主動查詢補狀態
- 每日對帳是最後一道防線
做金流久了會養成一種職業病:看到任何外部呼叫,都會先問「如果這裡斷掉、重送、回應遺失,會怎樣?」這個習慣後來讓我寫任何分散式系統都受用。