第三章:滴水不漏的 Webhook 處理與資料庫同步

在上一章,我們學會了把使用者送到 Stripe 結帳頁面。 但你必須記住金流界最重要的一句話:「前端畫面上的『付款成功』,永遠都不能當作真實的付款依據。」

使用者可能在刷卡成功的瞬間,網路斷線或是立刻關閉了瀏覽器分頁,導致他根本沒抵達你的 success_url 頁面。如果你把寫入資料庫的邏輯放在前端頁面,那使用者就會面臨「卡被刷了,但沒拿到商品」的悲慘客訴。

唯一可靠的方法,是使用 Webhook。這就像是 Stripe 的伺服器在背景偷偷打一通電話給你的伺服器:「嘿!我剛剛收到錢了,這筆訂單的明細是...」。這通電話不受使用者瀏覽器關閉的影響,是最穩定且安全的對帳機制。


🔒 1. Webhook 簽章 (Signature):防駭客的核心

既然 Webhook 是 Stripe 發送 HTTP POST 給我們的 API,那如果駭客偽造了一個長得很像 Stripe 的請求發給我們,假裝他付了 100 萬美金,怎麼辦?

Stripe 的解決方案是:Webhook 簽章 (Signature)。 當你在 Stripe Dashboard 設定 Webhook URL 時,Stripe 會發給你一把專屬的 Webhook Secret (以 whsec_ 開頭)。 Stripe 每次發請求給你時,都會在 HTTP Headers 裡面放一個用這把 Secret 算出來的「簽章」。我們收到請求時,必須用同一把 Secret 來驗證這個簽章,如果對不起來,就代表這是駭客偽造的,直接拒絕!


💻 2. 實作 Stripe Webhook API

在 Next.js App Router 中,我們建立這支 API:src/app/api/stripe/webhook/route.ts

import { NextResponse } from 'next/server';
import Stripe from 'stripe';
import { createClient } from '@supabase/supabase-js'; // 注意這裡要用 Admin 權限寫入

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
});

// 金流 Webhook 非常特殊,我們必須接收「原始的位元組 (Raw Body)」才能正確驗證簽章
// 所以在 Next.js 中,我們要把預設的 JSON 解析關掉 (不過在 App Router 中,我們直接讀 request.text() 即可)

export async function POST(request: Request) {
  const payload = await request.text();
  const sig = request.headers.get('stripe-signature');

  let event: Stripe.Event;

  try {
    // 1. 核心防禦:驗證簽章!這行如果通過,保證是真實的 Stripe 發出的請求
    event = stripe.webhooks.constructEvent(
      payload,
      sig!,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err: any) {
    console.error('⚠️ Webhook 簽章驗證失敗.', err.message);
    return NextResponse.json({ error: 'Webhook signature verification failed' }, { status: 400 });
  }

  // 2. 初始化高權限的資料庫連線 (因為 Webhook 是背景執行,沒有當前使用者的 Cookie)
  const supabaseAdmin = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!
  );

  // 3. 根據事件類型進行處理
  try {
    switch (event.type) {
      // 事件 A:結帳流程完成 (適用於單次付款或訂閱制建立的第一筆)
      case 'checkout.session.completed': {
        const session = event.data.object as Stripe.Checkout.Session;
        
        // 抓出我們在上一章偷藏的 userId
        const userId = session.metadata?.userId;
        
        if (userId) {
          console.log(`💰 付款成功!使用者: ${userId}, 金額: ${session.amount_total}`);
          
          // 寫入資料庫,正式發放權限!
          await supabaseAdmin.from('vt_purchases').insert({
            user_id: userId,
            item_id: 'vip-all-access', // 假設這是全站通行證
            order_no: session.id,
            amount: session.amount_total ? session.amount_total / 100 : 0 // Stripe 金額是以最小單位(分)計算的,要除以 100
          });
        }
        break;
      }

      // 事件 B:訂閱制的次月扣款成功 ( Recurring Payment )
      case 'invoice.paid': {
        const invoice = event.data.object as Stripe.Invoice;
        // 如果你需要實作每個月自動續約的功能,就在這裡更新使用者的訂閱到期日!
        break;
      }

      // 事件 C:訂閱制扣款失敗 (信用卡過期或餘額不足)
      case 'invoice.payment_failed': {
        const invoice = event.data.object as Stripe.Invoice;
        // 在這裡將使用者的 VIP 權限暫停,並寄信通知他更新信用卡
        break;
      }

      default:
        console.log(`Unhandled event type ${event.type}`);
    }

    // 4. 【極度重要】一定要回傳 200 OK 給 Stripe
    // 如果不回傳,Stripe 會以為你的伺服器壞了,然後在接下來的三天內瘋狂重試打你的 API
    return NextResponse.json({ received: true });

  } catch (err) {
    console.error('處理 Webhook 邏輯時發生錯誤:', err);
    return NextResponse.json({ error: 'Webhook handler failed' }, { status: 500 });
  }
}

🚧 3. 如何在 Localhost 測試 Webhook?

在前面的綠界章節,我們講過「大雷區」:綠界的伺服器打不到你的 localhost。 那現在我們在開發 Stripe 階段,總不可能每次寫完一行 Code 都要部署到 Vercel 才能測試吧?

Stripe 展現了它身為全球最佳開發者體驗公司的實力,它提供了一個官方工具:Stripe CLI

  1. 在你的電腦安裝 Stripe CLI
  2. 在終端機登入你的 Stripe 帳號:stripe login
  3. 執行指令,把 Stripe 的 Webhook 穿透內網直接轉發到你的 localhost:
    stripe listen --forward-to localhost:3000/api/stripe/webhook
    
  4. 執行這行指令後,終端機會顯示一行: Your webhook signing secret is whsec_xxxxxxxx... 這就是你本地開發用的 STRIPE_WEBHOOK_SECRET,把它貼進你的 .env.local

現在,當你在本地端點擊結帳頁面刷卡時,Stripe 的雲端會透過 CLI 把成功的 Webhook 訊號,穿越層層防火牆,精準地打到你電腦上的 API!這種頂級的開發體驗,就是為什麼全世界的開發者都如此熱愛 Stripe。

🏆 結語

恭喜你完成了 Stripe 國際金流的串接! 你現在擁有了一個具備「Apple Pay 快速結帳」、「多幣別收單」、「安全 Webhook 對帳」與「訂閱制支援」的國際級 SaaS 基礎建設。

軟體沒有國界,你的程式碼也不應該有。準備好接單,把你的軟體賣給全世界吧!🚀

解鎖完整教學內容

本章為付費內容。加入專案即可解鎖超過 5000 字的深度解析,包含 10 個以上神級 Prompt 與真實 Source Code 範例!