支付狀態機設計與落地方案

支付業務往往涉及創建訂單、發起支付、等待第三方回調、退款、關閉等多個環節。由於環節多、異常場景複雜,如果僅靠一個簡單的 status 字段隨意更新,很容易出現邏輯混亂、漏單或無法審計等問題。爲此,許多支付系統會採用狀態機(State Machine)來管理支付訂單在不同階段的狀態轉移,並配合狀態變更記錄表(或稱歷史表)進行留痕,以便後續審計和問題排查。

一、爲什麼需要支付狀態機

業務複雜度

支付流程包含多個環節,從「創建訂單」到「支付成功 / 失敗」再到「退款 / 關閉」都有可能發生各種中間狀態與異常場景,比如用戶遲遲不付款、第三方回調丟失、部分退款等。

可維護性與可審計性

可維護性:狀態機能將「狀態」與「事件」的對應關係明確列出,避免團隊成員隨意修改訂單狀態,減少溝通與排查成本。

可審計性:支付屬於資金流,監管或合規要求通常較高,需要對每一步狀態變更進行留痕記錄。

異常場景處理

• 第三方回調可能延遲或失敗; • 用戶在支付中途取消訂單; • 退款過程需要多次與第三方確認。

使用狀態機可以讓這些異常場景的處理更加有序,並保持系統內狀態的一致性。

綜上,狀態機對於中大型支付系統而言,幾乎是必不可少的設計工具。

二、常見的支付狀態

在實際業務中,支付狀態的定義並不完全固定。下面列出一個常見且相對完整的狀態集合,可根據業務需求增減或合併:

1.CREATED(已創建)

• 訂單在支付中心已生成,但尚未正式發起支付。

2.PENDING(待支付)

• 已向第三方發起支付,或生成支付鏈接 / 二維碼供用戶支付,尚未收到最終支付結果。

3.PROCESSING(支付中 / 處理中)

• 部分渠道會返回「處理中」狀態,表示第三方需要一定時間完成扣款確認。 • 有的業務會直接把「待支付」和「支付中」合併成一個狀態。

4.SUCCESS(支付成功)

• 收到第三方支付成功回調或主動查詢到支付成功,訂單已完成支付。

5.FAIL(支付失敗)

• 第三方支付明確失敗,或用戶超時未付款等。進入失敗狀態後通常無法再進行支付。

6.REFUNDING(退款中)

• 對於已支付成功的訂單,用戶或系統發起退款後,等待第三方退款結果時的狀態。

7.REFUNDED(退款成功)

• 第三方確認退款成功。如果是部分退款,需要在訂單或退款表中記錄已退金額和剩餘可退金額。

8.CLOSED(已關閉 / 已取消)

• 訂單在支付成功前被取消,如用戶主動取消或系統因超時而關閉等。 • 關閉後不可再進行支付或退款。

這些狀態幾乎涵蓋了常見的支付生命週期。如果業務場景不需要「PROCESSING」或「REFUNDING」,可以進行簡化;若需要更加精細的退款流程,也可進一步細化「部分退款」「多次退款」等狀態。

三、典型的狀態流轉示例

狀態機的核心在於「當前狀態 + 事件 -> 下一個狀態」。下表是一個簡化示例,展示了常見的事件觸發和狀態變化:

Rjxj2W

以上爲通用示例,實際業務會根據「部分退款」「多次退款」「多渠道回調」等情況進行更精細的設計。

四、狀態變更記錄表的設計

4.1 爲什麼需要單獨記錄表

  1. 審計與追溯
    資金相關業務往往需要留痕,以便在事後可以查看每一次狀態變化發生的時間、原因、操作者等。
  2. 問題排查
    用戶投訴或系統出現故障時,可以通過狀態歷史表還原訂單的完整生命週期,快速定位問題。
  3. 統計分析
    可以基於狀態變更表,統計訂單在各狀態停留的時間分佈、失敗率、退款率等,爲業務優化提供數據支撐。

4.2 表結構示例

常見的「狀態變更記錄表」(也可稱爲 payment_status_history 或 payment_order_history 等)大致可設計如下:

CREATE TABLE payment_status_history (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_id BIGINT NOT NULL,          -- 關聯 PaymentOrder 的主鍵或訂單號
    from_status VARCHAR(50) NOT NULL,  -- 變更前狀態
    to_status VARCHAR(50) NOT NULL,    -- 變更後狀態
    event VARCHAR(100) NOT NULL,       -- 觸發此次變更的事件 (PaymentSuccess, CloseOrder 等)
    operator VARCHAR(50),             -- 操作者 (系統, 用戶ID, 定時任務等)
    remark VARCHAR(255),              -- 備註或額外信息 (失敗原因, 第三方返回碼等)
    create_time DATETIME NOT NULL      -- 變更發生時間
);

多條記錄:一個訂單從創建到完成,可能多次變更狀態,每次都插入一條新的記錄,而不是隻有一條。

字段含義

order_id:用來區分屬於哪個訂單; •from_status / to_status:記錄本次變更的起點和終點; •event:具體事件名稱(如 PaymentSuccess 或 CloseOrder); •operator:記錄是誰或什麼系統觸發了本次變更; •remark:可以寫入失敗原因、第三方返回碼等輔助信息; •create_time:變更的發生時間戳。

五、如何在項目中落地

5.1 手寫狀態機

在多數項目中,最常見的做法是手動編寫一份「狀態機映射」或「狀態流轉表」:

public enum PaymentStatus {
    CREATED, PENDING, PROCESSING, SUCCESS, FAIL, REFUNDING, REFUNDED, CLOSED;
}
public enum PaymentEvent {
    CreateOrder, InitiatePayment, PaymentSuccess, PaymentFail, CloseOrder, 
    RefundRequest, RefundSuccess, RefundFail;
}

然後在代碼中使用映射或 if-else / switch 邏輯,控制「當前狀態 + 事件 -> 下一個狀態」的規則。每次狀態更新時:

  1. 查詢訂單當前狀態;
  2. 判斷是否允許觸發對應事件;
  3. 如果允許,則更新狀態爲目標狀態;
  4. 插入一條狀態變更記錄到 payment_status_history

示例

@Transactional
public void updateOrderStatus(Long orderId, PaymentEvent event, String operator) {
    PaymentOrder order = paymentOrderRepository.findById(orderId)
        .orElseThrow(() -> new RuntimeException("Order not found"));
    PaymentStatus current = order.getStatus();
    PaymentStatus next = stateMachine.nextState(current, event); 
    // 通過映射或方法,判斷下一個狀態
    if (next != current) {
        // 更新訂單主表
        order.setStatus(next);
        paymentOrderRepository.save(order);
        // 插入狀態變更記錄
        PaymentStatusHistory history = new PaymentStatusHistory();
        history.setOrderId(orderId);
        history.setFromStatus(current.name());
        history.setToStatus(next.name());
        history.setEvent(event.name());
        history.setOperator(operator);
        history.setRemark("可放第三方回調信息等");
        history.setCreateTime(LocalDateTime.now());
        paymentStatusHistoryRepository.save(history);
    }
}

5.2 使用 Spring StateMachine

•Spring 提供了 Spring Statemachine[1] 庫,可以更系統化地管理複雜的狀態、事件、轉移; • 支持分層狀態機、並行狀態機等高級功能,也可以配置監聽器在狀態變更時自動寫入數據庫; • 適用於狀態過多、流程極其複雜或需要可視化管理的場景; • 如果團隊不熟悉該框架且業務需求不算太複雜,手寫狀態機往往已經足夠。

六、關鍵關注點

冪等性

• 支付回調可能多次觸發,需要確保重複回調不會導致重複更新或錯誤更新。可以在數據庫層面做冪等校驗,如若訂單狀態已是 SUCCESS,再次收到成功回調則忽略。

異常場景

• 第三方回調丟失:訂單可能一直停留在 PENDING 狀態,需要定期主動查詢第三方; • 超時關閉:若用戶長時間未支付,訂單可自動從 CREATED / PENDING 轉爲 CLOSED; • 退款失敗:若第三方退款失敗,需要回到 SUCCESS 狀態並可再次發起退款。

部分退款

• 如果允許部分退款,需要額外記錄「已退金額」「剩餘可退金額」等信息; • 狀態機也需支持「部分退款成功」「多次退款」等更復雜的場景。

數據一致性

• 通常使用數據庫事務保證訂單表與狀態變更表的同步更新; • 大規模系統可採用消息隊列或分佈式事務方案。

對賬與統計

• 完整的支付系統還需要對賬邏輯(對比第三方交易流水),並將狀態變更表的數據用於審計與統計分析。

七、總結

狀態機設計

• 列出核心狀態(CREATED、PENDING、SUCCESS、FAIL、REFUNDING、REFUNDED、CLOSED 等)和對應事件; • 明確「當前狀態 + 事件 -> 下一個狀態」的規則,保證每次狀態變更都有明確觸發。

狀態變更記錄表

• 建議使用「多條記錄」的方式保存狀態流轉歷史,每次變更都插入一條; • 表中至少包含「訂單標識、原狀態、新狀態、觸發事件、操作人、時間、備註」等核心字段; • 方便後續審計、問題排查與統計分析。

關鍵落地點

• 確保冪等異常場景處理; • 考慮部分退款多次退款等業務需求; • 根據團隊熟悉程度,選擇手寫狀態機Spring StateMachine; • 在大規模合規要求高的場景中,要特別重視審計和數據一致性。

通過以上設計,一個支付系統就能夠在單體微服務架構中實現對訂單全生命週期的有效管理,保持狀態清晰、有序,滿足合規與審計需求,並在異常場景下依舊具備較好的可維護性和可追溯性。

References

[1] Spring Statemachine: https://spring.io/projects/spring-statemachine

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/-pzuyEEzlD02apWOd83Y1w