I Dream, AI Works.

7-11

模組四|免費金流系統

沒有金流的網站不是店面,而金流這一關在 2026 年已經可以零成本接起來。

讓成交真實發生,0 元以外的成本只有手續費


模組四能做到什麼

recur.tw(台灣市場,主要金流)
→ 台灣本地支付平台,支援:
  信用卡 / ATM 轉帳 / 超商付款(7-11 / 全家)
→ 台幣直接收款,對帳清楚
→ 無需國際支付知識,申請流程台灣化
→ 適合:所有以台灣受眾為主的 OPC

Lemon Squeezy(國際市場,輔助金流)
→ Merchant of Record:稅務全交給 Lemon Squeezy 處理
→ 支援全球信用卡 / PayPal
→ 免月費,只收手續費(5% + 50 cents/筆)
→ 適合:有國際學員,或同時經營英文市場

Webhook 自動解鎖(兩套金流共用邏輯)
→ 付款成功 → Webhook 通知你的網站
→ 自動在 Purchase 資料表新增記錄
→ 自動更新 user.role
→ 自動發送購買確認 + 課程歡迎 Email
→ 學員刷新頁面,課程解鎖完成

最佳實務

台灣 OPC 從 recur.tw 出發,有國際受眾再加 Lemon Squeezy。

為什麼 recur.tw 是台灣首選:
→ 台灣學員習慣超商付款和 ATM 轉帳,不只是信用卡
→ 台幣結算,對帳和報稅直覺
→ 台灣客服,出問題找得到人
→ 沒有「國際匯率損失」,學員看到的價格就是你收到的金額

什麼時候加入 Lemon Squeezy:
→ 開始有非台灣學員詢問購買
→ 想進入英文市場
→ 需要 Merchant of Record(省去稅務處理)

Checkout 頁面讓學員自己選付款方式。

不要強迫只能用一種。 Landing Page 的 CTA 提供兩個選項: 「台灣付款(信用卡 / ATM / 超商)」和「國際信用卡」。 系統根據選擇路由到對應的金流。

Webhook 邏輯兩套金流共用一個模式。

recur.tw 和 Lemon Squeezy 的 Webhook 處理邏輯完全一樣: 驗簽名 → Idempotency 檢查 → 寫 Purchase → 更新 role → 發 Email。 只有 payload 格式不同,其他邏輯抽成共用函數。

Webhook 必須驗證簽名,不能省略。

任何人都可以呼叫你的 Webhook endpoint。 沒有驗證簽名 = 有人可以偽造「付款成功」通知 → 白給課程。

付款成功 Email 要在 Webhook 裡發,不在前端發。

前端可能會有用戶沒等 redirect 就關掉視窗。 Webhook 是後端通知,可靠性高,在這裡發 Email 才能保證一定發出去。


前置條件

【主要:recur.tw 設定】
□ 申請 recur.tw 帳號:https://recur.tw
  → 完成商家審核(需要身份證件或公司資料)
  → 在後台建立「商品」(對應你的 L2 和 L3 課程)
  → 取得 API Key 和 Secret
  → 設定 Webhook URL:https://你的網域.com/api/webhooks/recur
  → 記下 Webhook Secret(用於驗簽名)

【輔助:Lemon Squeezy 設定(可選)】
□ 申請 Lemon Squeezy 帳號(免費):https://lemonsqueezy.com
  → 建立 Store,建立 Product
  → 取得 API Key 和 Webhook Secret
  → 設定 Webhook URL:https://你的網域.com/api/webhooks/lemonsqueezy

【必要條件】
□ 模組三(會員系統)必須先完成
  (Webhook 需要把 role 更新到 user 資料表)
□ 網站已部署到 Vercel(Webhook 需要可公開存取的 HTTPS URL)

Claude Code 指令

我的專案是 Next.js App Router + Payload CMS 3.x + Tailwind CSS + TypeScript。
模組一到三已完成。模組三使用 Better Auth,有 user / Purchase 資料表。

請幫我建立模組四:雙軌金流系統。
主要金流:recur.tw(台灣市場)
輔助金流:Lemon Squeezy(國際市場)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【A】環境設定
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

安裝 Lemon Squeezy SDK(recur.tw 用原生 fetch 串接):
npm install @lemonsqueezy/lemonsqueezy.js

在 .env.local 加入:

# recur.tw(主要)
RECUR_API_KEY=(你的 recur.tw API Key)
RECUR_API_SECRET=(你的 recur.tw API Secret)
RECUR_WEBHOOK_SECRET=(recur.tw Webhook 驗簽用的 Secret)

# Lemon Squeezy(輔助)
LEMONSQUEEZY_API_KEY=(你的 Lemon Squeezy API Key)
LEMONSQUEEZY_STORE_ID=(你的 Store ID)
LEMONSQUEEZY_WEBHOOK_SECRET=(Lemon Squeezy Webhook Secret)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【B】共用的付款後處理邏輯
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

建立 lib/payment-handler.ts,抽出兩套金流共用的邏輯:

匯出 handlePaymentSuccess(params) 函數:
接收:
{
  orderId: string        // 金流平台的訂單 ID
  userId: string         // 購買者的 user ID
  courseSlug: string     // 購買的課程
  amount: number         // 實際付款金額(台幣)
  userEmail: string      // 購買者 Email
  provider: 'recur' | 'lemonsqueezy'
}

執行步驟:
1. Idempotency 檢查
   → 查詢 Purchase 資料表,是否已有 orderId 的記錄
   → 如果存在:直接 return(不重複處理)

2. 新增 Purchase 記錄
   → insert {userId, courseSlug, orderId, amount, status: 'completed',
             provider, purchasedAt: new Date()}

3. 更新 user role
   → 查詢 Payload Course collection 取得 level(l2 / l3)
   → 更新 Better Auth user 資料表的 role
   → 規則:只升級,不降級(已有 l3 的不改為 l2)

4. 發送購買確認 Email(使用 Resend)
   → 主旨:「🎉 購買成功!你的課程已解鎖」
   → 內容:
     感謝購買 [課程名稱]
     立即開始學習:https://你的網域.com/learn/[courseSlug]
     如有問題請 Email 至:你的客服信箱

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【C】recur.tw 整合(主要金流)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

建立 lib/recur.ts:

請先閱讀 recur.tw 的官方 API 文件(https://recur.tw/docs)。
根據文件實作以下函數:

createPaymentOrder(params) 函數:
接收:
{
  courseSlug: string
  courseName: string
  amount: number        // 台幣金額
  userId: string
  userEmail: string
  returnUrl: string     // 付款成功後 redirect 的 URL
}

執行:
→ 呼叫 recur.tw 建立訂單的 API endpoint
→ 在訂單的 custom metadata 帶入 { userId, courseSlug }
→ 回傳 recur.tw 給你的付款頁面 URL

建立 POST /api/webhooks/recur route:

Step 1:驗證 Webhook 簽名
→ 根據 recur.tw 文件說明的簽名方式驗證
→ 不符合:回傳 401

Step 2:解析 Webhook body
→ 根據 recur.tw 文件確認付款成功的事件格式
→ 取得:orderId / status / amount / userEmail / custom metadata(userId, courseSlug)

Step 3:只處理「付款成功」狀態的通知
→ 其他狀態:回傳 200(不處理)

Step 4:呼叫 handlePaymentSuccess()
→ 傳入從 Webhook 解析到的所有參數
→ provider: 'recur'

Step 5:回傳 200

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【D】Lemon Squeezy 整合(輔助金流)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

建立 lib/lemonsqueezy.ts:

匯出 getLSCheckoutUrl(variantId, userEmail, userId, courseSlug) 函數:
→ 用 SDK 建立 Checkout URL
→ 帶入 custom_data:{ userId, courseSlug }
→ 設定 redirect_url = /dashboard
→ 預填 Email = userEmail
→ 回傳 checkoutUrl

建立 POST /api/webhooks/lemonsqueezy route:

Step 1:驗證 Webhook 簽名
→ 從 headers 取得 X-Signature
→ 用 LEMONSQUEEZY_WEBHOOK_SECRET 計算 HMAC SHA256 比對

Step 2:只處理 event_name = order_created + status = paid

Step 3:從 data.attributes 取得
→ order_number(orderId)
→ total(除以 100 得台幣,若為美金需換算)
→ user_email
→ custom_data.userId + custom_data.courseSlug

Step 4:呼叫 handlePaymentSuccess()
→ provider: 'lemonsqueezy'

Step 5:回傳 200

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【E】Checkout API Route(統一入口)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

建立 POST /api/checkout route:

接收:
{
  courseSlug: string
  provider: 'recur' | 'lemonsqueezy'
  // recur.tw 需要:
  recurProductId?: string
  // Lemon Squeezy 需要:
  lsVariantId?: string
}

執行:
1. 用 Better Auth 取得 session,確認已登入
   → 未登入:回傳 401

2. 檢查 Purchase 資料表,確認未購買過
   → 已購買:回傳 { error: '你已購買此課程' }

3. 根據 provider 路由:
   → recur:呼叫 createPaymentOrder(),回傳 { checkoutUrl }
   → lemonsqueezy:呼叫 getLSCheckoutUrl(),回傳 { checkoutUrl }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【F】Landing Page CTA 按鈕更新
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

更新 /courses/[slug] 的購買按鈕區塊:

顯示兩個付款選項(L2 / L3 課程才顯示,L1 免費不需要):

主要按鈕(recur.tw):
→ 文字:「立即購買(信用卡 / ATM / 超商)」
→ 點擊:呼叫 POST /api/checkout,provider: 'recur'
→ 取得 checkoutUrl 後 redirect

輔助按鈕(Lemon Squeezy,樣式較小):
→ 文字:「國際信用卡付款」
→ 點擊:呼叫 POST /api/checkout,provider: 'lemonsqueezy'
→ 取得 checkoutUrl 後 redirect

兩個按鈕共用 loading 狀態:
→ 任一點擊後,兩個按鈕都顯示 disabled

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【G】本機測試設定
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Webhook 需要可公開存取的 HTTPS URL,用 localtunnel 處理:

npx localtunnel --port 3000
→ 取得臨時 URL,設定到 recur.tw 和 Lemon Squeezy 的 Webhook 設定

提供以下測試工具:
1. 模擬 recur.tw Webhook 的 curl 指令(用 RECUR_WEBHOOK_SECRET 產生正確簽名)
2. 模擬 Lemon Squeezy Webhook 的 curl 指令
3. 查詢 Purchase 資料表是否正確寫入的 SQL

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

技術規格:
- 兩個 Webhook route 都要設定 export const runtime = 'nodejs'
- handlePaymentSuccess 是純函數,方便單獨測試
- 所有金流相關操作保留 console.log(方便 debug)
- recur.tw API 整合部分:如果文件有不清楚的地方,
  先留下 TODO 註解標明需要確認的欄位,不要猜測 API 格式
- 完成後提供完整測試清單:
  1. recur.tw 沙盒測試付款流程
  2. Lemon Squeezy Test mode 測試付款流程
  3. 確認 Purchase 記錄寫入
  4. 確認 user.role 更新
  5. 確認購買確認 Email 發出
  6. 確認課程頁面正確解鎖

草稿中 · Writing in Public