一般的業務系統,狀態管理出錯頂多造成資料不一致,花時間修復就好;支付系統則不同,一個非法的狀態轉換,可能造成持卡人被重複扣款、退款金額錯誤、帳務無法對帳,甚至觸發監管機關的調查。

這就是為什麼金流支付產業的工程師對「狀態機」的要求,比其他後端工程師嚴格許多;不是寫個 if/else 判斷 status 就算數,而是要在 Domain 層、應用層、資料庫層三個層次都建立防護,讓非法轉換在物理上無法發生。

本文從信用卡交易出發,完整拆解一筆訂單從建立到結算的所有狀態,說明每條轉換背後的業務邏輯,並延伸到電子支付的狀態差異、非法轉換的防護設計,以及可以直接套用的 .NET 實作骨架。

本文是狀態機系列的第一篇,聚焦在信用卡訂單的完整狀態集合、每條合法轉換背後的業務觸發條件;三層防護的實作細節和電子支付的狀態機差異,則留到第二篇展開。


信用卡訂單的完整狀態集合

在開始探討轉換圖之前,我們先把每個狀態的語義定義清楚,這會比記憶狀態名稱更重要,因為面試官不會在意面試者使用了什麼英文單字,而是在意的是是否理解每個狀態代表的業務意涵。

INITIATED(已建立)

訂單剛建立,使用者已按下「確認付款」,系統已準備好發出授權請求,但尚未收到任何回應,這是所有訂單的起始狀態;在這個狀態下,資料庫應已記錄訂單金額、商品資訊、使用者 ID、時間戳記,但 authorization_codegateway_transaction_id 這些欄位尚為空值。

AUTHORIZED(已授權)

發卡行(Issuer)回傳核准碼(Approval Code),確認持卡人的卡片有效、額度足夠,此時系統應將授權碼、授權金額、授權時間存入資料庫;AUTHORIZED是一個容易產生誤解的狀態,其代表額度被凍結但資金尚未移動,持卡人的帳戶在這個瞬間沒有任何資金流出,只是有一筆「保留額度」;這就是為什麼飯店可以先凍結 5,000 元押金,退房後才根據實際消費金額請款。

CAPTURED(已請款)

商家確認可以請款(商品出貨、服務交付),發出 Capture 請求,這一步才是正式告訴銀行「我要這筆錢」;Capture 金額的關鍵規則是可以小於或等於授權金額,但不可以超過,超過授權金額的 Capture 請求會被收單行(Acquirer)拒絕,且可能觸發持卡人的詐欺警示。

SETTLED(已結算)

收單行完成日終批次清算後,資金在 T+1 或 T+2 工作天實際從發卡行撥到收單行、再撥到商家帳戶,這是資金真正移動的時刻。

FAILED(失敗)

授權請求被發卡行拒絕(額度不足、風控攔截、卡片過期等),或在授權過程中發生技術錯誤;FAILED 是一個終止狀態,代表失敗的訂單不應在原訂單上重試,而應建立新的訂單記錄,以保持完整的稽核軌跡。

VOIDED(已取消)

CAPTURED 之前商家主動取消授權,Void 操作的特點是:持卡人的凍結額度立即解除,且完全沒有資金移動,這和退款有根本的差異。

REFUNDED(已退款)

SETTLED 之後商家對持卡人執行退款,退款是反向資金流:錢從商家帳戶流回持卡人,通常需要 3 到 5 個工作天才能出現在持卡人的帳單上。

DISPUTED(爭議中)

持卡人向發卡行申訴,主張這筆交易有問題(未收到商品、遭到詐騙、對交易不認識等);發卡行審查後,會透過卡組織通知收單行,收單行再以 Webhook 通知商家系統;進入 DISPUTED 狀態後,對應金額可能被暫時凍結,商家需在限期內提交舉證資料。


合法的狀態轉換與業務觸發條件

INITIATED → AUTHORIZED

觸發條件:發卡行回傳成功的授權碼(Response Code 00);系統在這個轉換發生時,必須同時儲存以下資料:

authorization_code    // 發卡行核准碼,後續 Capture 必須帶上
authorized_amount     // 授權金額(可能和訂單金額不同)
authorized_at         // 授權時間戳(計算授權有效期的基準)
acquirer_reference    // 收單行參考號
card_bin              // 卡號前 8 碼(用於費率計算和對帳)

授權有效期的工程含義:授權通常有 7 天的有效期(部分產業如預訂、住宿可達 30 天),系統需要一支背景排程任務,定期掃描「AUTHORIZED 超過有效期且未 Capture」的訂單,主動發出 Void 請求並更新狀態,否則持卡人的額度會被無限期佔用。

INITIATED → FAILED

觸發條件:發卡行回傳非 00 的拒絕原因碼,或授權請求逾時。

常見的拒絕原因碼包含:51 額度不足、54 卡片過期、62 風控攔截、05 通用拒絕;值得注意的是 05 是最常見的「什麼都不說」拒絕碼,發卡行刻意不透露原因以防詐騙者測試,系統不應嘗試解讀它,而是應引導使用者聯繫發卡銀行。

AUTHORIZED → VOIDED

觸發條件:商家主動取消,或系統偵測到授權即將過期。

這條轉換有一個時間窗口的約束:Capture 發生之前才能 Void,一旦進入 CAPTURED 或之後的狀態,Void 就不再是合法操作;Void 的 API 呼叫通常非常簡單,帶上 authorization_code 即可,因為此時尚無資金移動,收單行端只需要解除額度保留。

AUTHORIZED → CAPTURED

觸發條件:商家確認可以請款,發出 Capture 請求,除了帶上 authorization_code,還需要指定實際的請款金額;如前所述,這個金額可以小於但不可超過授權金額。

部分場景支援多次 Capture:例如一張訂單分三批出貨,每批出貨時各 Capture 一次;這個場景的狀態機設計會更複雜,訂單本身可能需要一個「部分 Captured」的中間狀態,或是拆分成多個子訂單分別追蹤。

CAPTURED → SETTLED

觸發條件:收單行完成日終批次清算,資金實際撥款至商家帳戶。

這個轉換通常是由收單行的 Webhook 觸發,而不是系統主動查詢;在設計上需要接收結算通知的 Webhook endpoint,在確認簽名後更新訂單狀態並記錄實際入帳金額(這個金額會比訂單金額少,因為已扣除 MDR 手續費)。

SETTLED → REFUNDED

觸發條件:商家主動退款,或消費者申請退款,這條轉換有一個常被誤解的設計決策:退款不應覆蓋原訂單,而應在獨立的 refunds 表建立新記錄

這樣設計的原因有三個。第一,支援部分退款:一筆 NT$1,000 的訂單可以先退 NT$300,之後再退 NT$200,如果直接修改訂單狀態就沒辦法追蹤每次退款的金額和時間。第二,保持稽核軌跡:原訂單的 SETTLED 狀態清楚記錄了這筆交易曾經成功結算,退款是之後發生的獨立事件。第三,對帳需要:對帳時需要同時看到「這筆訂單結算了多少」和「這筆訂單退款了多少」,兩張表分開查詢比一個混合狀態清楚得多。這個資料表的完整設計會在第三篇展開。

SETTLED → DISPUTED

觸發條件:持卡人向發卡行申訴,觸發 Chargeback 流程。

這條轉換不是商家主動發起的,而是被動接收;收單行會透過 Webhook 通知系統,此時需要做幾件事:將訂單狀態更新為 DISPUTED、凍結對應退款操作(避免同時進行退款和爭議處理)、立即拉出訂單的所有相關資料(付款憑證、物流記錄、客服紀錄),準備在期限內提交給收單行。


非法狀態轉換:面試常出現的考點

CAPTURED 之後不能 Void

原因:Capture 之後,這筆交易已進入收單行的批次清算佇列,「保留額度」的概念已不再適用;此時 Void 等同對空操作,但可能在帳務系統造成不一致,因為系統記錄了 Void,但收單行端已在處理 Capture。

適當做法:如果 Capture 之後需要退錢,唯一合法路徑是等結算完成後執行 Refund。

SETTLED 之後不能 Void

原因:結算後資金已實際在銀行間移動,Void 這個動作在技術上已無法撤回任何資金流動。

適當做法:同上,只能走 Refund 流程。

Capture 金額不能超過授權金額

原因:發卡行在授權時只同意了一個特定的金額,超過這個金額的請款沒有得到授權,收單行會直接拒絕,且可能觸發持卡人的詐欺警示通知。

適當做法:如果實際金額超過原授權金額(例如消費者加購了商品),需要先 Void 原授權,再以新的金額重新走一次授權流程。

同一筆授權不能重複 Capture(非分批出貨場景)

原因:重複 Capture 意味著同一筆授權碼被使用了兩次,直接造成持卡人被重複扣款,是支付系統中很嚴重的 bug 之一。

防護方式:在資料庫的 orders 表對 authorization_code 欄位建立唯一約束(UNIQUE constraint),讓重複 Capture 在資料庫層就被擋下。這個約束的具體實作會在第二篇的三層防護架構中展開。

FAILED 是終止狀態,不能繼續轉換

原因:失敗的訂單代表這次授權嘗試結束了;如果允許從 FAILED 重新授權,稽核軌跡會變得難以解讀,因為無法從訂單記錄看出「這筆訂單第一次失敗了、第二次成功」,這在商業上應該是兩個獨立的交易事件。

適當做法:每次重試授權都應建立新的訂單記錄,並在新訂單上記錄「重試自哪一筆訂單」的關聯。


信用卡訂單狀態機的完整骨架

  • 狀態的語義比名稱重要AUTHORIZED 最核心的含義不是「授權成功」,而是「額度已凍結、資金尚未移動」,理解這個語義才能設計出正確的 Void vs Refund 分流邏輯。
  • 每條合法轉換都有明確的觸發條件和需要儲存的資料:從 authorization_codesettled_date,這些欄位不是隨意設計的,都是為了支撐後續的對帳和爭議處理。
  • 終止狀態不可再轉換FAILEDVOIDEDREFUNDED 是旅程的終點,試圖從終止狀態繼續轉換是常見的設計錯誤,也是面試時容易被抓到破綻的地方。
  • 非法轉換和合法轉換一樣重要:能說出五種常見的非法轉換以及背後的業務原因,是資深工程師和一般工程師在面試中最明顯的分野。

下一篇會說明電子支付的狀態機和信用卡有哪些關鍵差異,以及如何在 Domain 層、資料庫層、併發控制三個層次,把這篇定義的規則真正落實成程式碼層級的防護。


本文為個人學習筆記,持續更新中。