付款與 Webhook

追蹤 Checkout、Stripe 回呼、交易紀錄和權益授予,定位付款後未開通等問題。

付款排錯應追蹤同一筆業務的 Checkout ID、Stripe Event ID 和內部執行紀錄。不要為了重現問題而反覆實際付款,也不要只根據付款成功頁面,就判斷帳號已取得權限。

一、先核對連線與方案

設定作用常見錯誤
payment.provider目前的付款服務供應商設定與實際呼叫不一致
STRIPE_CONNECTION_ID應用程式內部的 Stripe 連線識別碼已有資料後任意更換,導致找不到原紀錄
STRIPE_SECRET_KEY呼叫 Stripe API測試、正式環境模式或帳戶不一致
STRIPE_WEBHOOK_SECRET驗證回呼簽章使用另一個 Endpoint 或 CLI 監聽器的 Secret
產品及方案 Key查找應用程式方案前端提交的 Key 已重新命名或不存在
方案 priceId對應 Stripe Price仍有預留值,或 Price 屬於其他模式

STRIPE_CONNECTION_ID 是專案自行定義的穩定識別碼,不是 Stripe 自動提供的 Price ID。測試與正式環境應分別設定,已有正式環境紀錄後,請勿任意更換。

npm run doctor 和 npm run doctor:remote 能提示缺少的變數,但不會驗證每個 Price 的歸屬和可購買狀態。方案顯示的價格也不會改變 Stripe 的實際收費。

二、無法建立 Checkout

在 Network 中檢查建立 Checkout 的請求,先區分參數驗證、登入要求、重複購買限制和 Stripe API 錯誤。

如果 Price 仍包含 _replace_me,請先在對應的 Stripe 模式中建立價格,再填入實際 ID。一次性方案與訂閱方案,也要使用適合各自付款方式的 Price。

出現 paymentsProductAlreadyPurchased 時,請檢查 allowRepurchase 和使用者既有的有效購買紀錄。不要刪除交易歷史,讓使用者再次付款。若業務需要允許重複購買,應調整方案規則,並測試購買後的權益處理。

重複購買判斷也不是並行鎖定機制。多個分頁可能在第一筆付款完成前建立多個 Checkout Session,測試時只保留一個有效流程。

三、本機收不到 Webhook

本機開發網址不能直接作為 Stripe 從公用網路投遞的目標。可以使用 Stripe CLI,將測試事件轉送至開發伺服器:

stripe login
stripe listen --forward-to http://127.0.0.1:5173/api/webhooks/stripe

請將連接埠替換為 npm run dev 實際監聽的連接埠,將監聽器輸出的簽章 Secret 填入本機 .env 的 STRIPE_WEBHOOK_SECRET,再重新啟動開發伺服器。這個值不能與主控台建立的 Endpoint Secret 混用,詳細說明請見 Stripe 本機監聽與回呼說明。

接著從應用程式頁面建立一筆測試 Checkout,再觀察 CLI 投遞、伺服器端記錄和本機資料庫。單獨觸發模擬付款事件,可能缺少應用程式建立的 Checkout、使用者關聯或方案對應關係,不能據此判定整個購買流程失敗。

專案也提供 webhook:stripe:dev 與 webhook:stripe:prod,它們會建立遠端 Webhook 目標,並將 Secret 寫回對應的環境檔案,並不是本機轉送器。使用這些指令碼時,需要提供可公開存取的網址。已有目標時,請先核對,不要反覆建立。

四、回呼簽章、路徑或設定錯誤

目前入口為:

POST {VITE_SITE_URL}/api/webhooks/stripe
現象檢查順序
404、405完整網址、HTTP 方法、目標 Worker 是否為目前版本
簽章驗證失敗Endpoint 或 CLI Secret、原始請求本文、Stripe-Signature 請求標頭
PAYMENT_NOT_CONFIGURED線上 Worker 的連線 ID、API Key、Webhook Secret
5xx伺服器端最早的例外、Stripe API、D1 和業務回呼處理
WEBHOOK_BUSY同一事件是否已有處理租用,查看時間和對應記錄

簽章驗證使用原始請求本文。不要先解析 JSON 再重新序列化,也不要為了排錯而關閉簽章驗證,請參閱 Stripe 簽章疑難排解。

修改本機 .env.production 後,仍需要更新部署。檔案已寫好,不代表 Worker 執行時的 Secret 已更新。網域變更時,也要檢查 Stripe 目標網址是否仍指向舊網域。

五、從事件帳本確認處理位置

在正確的業務資料庫 DB 中,以唯讀方式查詢目標 Stripe Event。請將範例識別碼替換成本次事件 ID:

SELECT id, provider, connection_id, event_id, event_type,
       status, created_at, updated_at
FROM webhook_events
WHERE provider = 'stripe'
  AND event_id = 'evt_replace_me'
ORDER BY id DESC
LIMIT 10;

同一事件也需要搭配 connection_id 判斷歸屬。不要將完整的 raw_body 匯出至共用記錄,它包含業務和使用者資訊。

結果含義與下一步
沒有紀錄檢查是否投遞至正確環境、是否通過簽章驗證,以及是否支援該事件類型
processing查看租用資訊、更新時間和目前執行記錄,不能只憑狀態就判定仍在處理
failed在受控環境查看 last_error,定位失敗的處理步驟
succeeded繼續核對交易和權益,不代表所有後續非同步工作都已完成

目前 webhook_events 沒有 attempt_count 欄位。投遞次數請查看 Stripe 投遞歷史,Command 嘗試次數則查看 command_execution,兩者並非同一個計數。

不支援的事件會寫入 payment.webhook.ignored 記錄並傳回成功,不會進入這份帳本。已處理成功的重複事件,也會直接傳回成功,因此不能只憑 2xx 就證明新增了一筆交易。

六、已付款,卻沒有取得角色或權益

依以下順序,找出最後一個成功步驟:

Stripe 付款狀態
→ 對應回呼投遞與簽章驗證
→ checkout_sessions / transactions / transaction_items
→ 方案與 Price 對應關係
→ event_execution / command_execution
→ user_role_capability / user_entitlement_capability

一次性付款和訂閱使用的業務事件不同,不能要求每筆訂閱都必須找到 PaymentSucceededEvent。訂閱還應檢查 subscriptions、subscription_items,以及訂閱建立、更新和結束事件。

請優先檢查以下情況:

  • 回呼中的 Price ID 未對應至目前的產品方案,無法產生預期的授予輸入。
  • 自訂後的方案未設定需要授予的角色或權益。
  • 付款紀錄已更新,但 Event 建立或 Command 執行失敗。
  • 權益已儲存,但頁面檢查的是另一個能力 Key、使用者或來源。
  • 使用者完成 Checkout 頁面操作,但付款方式仍處於非同步確認階段。

背景執行紀錄的查詢和狀態說明,請見背景工作與統計。確認根因前,不要手動新增交易或直接授予帳號權限,否則會掩蓋故障,並破壞後續撤銷所需的關聯。

七、重複投遞、忙碌狀態與重播

Stripe 可能重複投遞,事件順序也不應視為業務發生順序,請參閱 Stripe Webhook 投遞行為。

範本依供應商、連線 ID 和 Event ID 辨識事件。成功紀錄會直接傳回成功,有效處理租用仍被占用時,則傳回非 2xx,讓供應商稍後重試。目前 Stripe 處理租用的最長有效時間為三分鐘。到期後,由後續投遞嘗試重新取得處理權,不是背景計時器一到時間就一定會恢復。

重播前,請確認設定已修正,並檢查交易、權益和外部副作用是否已發生。等冪邏輯會略過已成功的事件,重播該事件不會自動修正另一個獨立失敗的非同步 Command。

不要刪除事件帳本、將狀態改為未處理,或偽造新的事件 ID 來強制重新執行。若處理結果與外部狀態不一致,應先完成對帳,再透過對應業務層設計修正方式。

八、帳單入口與退款

帳單入口需要使用者對應的 payment_customers 紀錄,Customer 必須屬於目前連線與模式。接著檢查 Stripe 入口設定和返回網址,不能使用另一環境的 Customer 進行測試。

目前退款處理會儲存退款紀錄,應用程式回呼會記錄 refund.updated,但不會自動撤銷一次性購買授予的角色或權益。退款成功後仍可存取,不一定是回呼失敗,也可能是尚未實作退款後的產品規則。訂閱終止流程則應另外檢查對應的訂閱事件。

修正後的驗證

使用測試模式,從應用程式建立一筆完整購買,確認交易、事件、權限和頁面行為一致。再驗證重複投遞不會重複授予權益,以及未購買的使用者仍無法存取受限功能。正式環境問題的恢復,以原業務紀錄對帳為準,不要求使用者再次付款來證明修正有效。