第八章:印鈔機啟動!綠界科技 (ECPay) 金流串接與 Webhook 訂單回呼處理

在商業世界裡,有一個殘酷的現實: 如果一個網站不能收錢,不管它的動畫多漂亮、架構多完美,它充其量只能算是個「作品集」。 一旦它成功串接了金流,可以 24 小時不間斷地收錢入帳,它就正式升級為一台會下金蛋的「數位資產 (Digital Asset)」。

在台灣,要建立線上信用卡收付,最穩定、市佔率最高的老字號系統就是綠界科技 (ECPay)。 然而,綠界的官方文件對於新手工程師來說,往往像是一本晦澀難懂的天書。特別是它要求使用極度繁瑣的 SHA256 壓碼與排序加密機制。

許多工程師在這個關卡卡了幾個禮拜最後放棄。但在 Vibe Tutor 這套企業級原始碼中,我們已經幫你把所有的雷都踩過了,並將複雜的加密邏輯封裝成最乾淨俐落的 Next.js API Routes!

🎯 本章目標

  1. 理解綠界金流的運作原理與安全機制 (為什麼需要 HashKey?)。
  2. 解析前台結帳 API (/api/ecpay/checkout) 的加密與自動跳轉流程。
  3. 掌握非同步的 Webhook 回呼機制 (/api/ecpay/return),確保訂單不漏接。

🛒 步驟一:前台表單加密與送出 (Checkout Route)

當使用者在你的定價頁面上,興沖沖地按下「💳 立即購買 VIP 通行證」的瞬間,發生了什麼事? 前端的表單資料 (包含 courseId, price, tier) 會透過 HTTP POST 送到我們後端寫的 API Route:src/app/api/ecpay/checkout/route.ts

在這個關鍵的 API 中,我們必須在伺服器端完成三件大事:

  1. 驗證使用者是否登入:如果沒登入,就不准付款。因為付完錢後,系統會不知道要把這堂尊貴的 VIP 課程歸屬給哪一個帳號。
  2. 建立唯一訂單編號 (MerchantTradeNo):產生一組絕對不重複的字串(例如:VIBE + 時間戳記 1710234567),這是未來對帳的唯一憑證。
  3. 打包綠界需要的參數並加密:這包含商品名稱、金額、ReturnURL (回呼網址),以及最重要的防偽檢查碼。

🛡️ 核心大魔王:計算 CheckMacValue (防偽檢查碼)

為什麼不能直接把金額丟給綠界?因為如果傳輸過程沒有加密,懂點技術的駭客可以攔截封包,把「9,999 元」竄改成「1 元」,然後綠界就會傻傻地刷 1 塊錢,然後告訴你付款成功。

為了解決這個問題,綠界要求我們必須把所有的參數,加上只有你和綠界知道的專屬密碼 HashKeyHashIV,利用極其嚴格的規則排序並進行 SHA256 加密。

在我們的原始碼中,這段頭痛的加解密已經被完美封裝:

import crypto from 'crypto';

// 1. 將所有要送給綠界的參數,依照參數名稱的英文字母 A-Z 順序排列
const sortedKeys = Object.keys(params).sort();

// 2. 將密碼 (HashKey) 放在最前面,然後把參數串接起來
let checkValue = `HashKey=${process.env.ECPAY_HASH_KEY}`;
for (const key of sortedKeys) {
  checkValue += `&${key}=${params[key]}`;
}
// 把密碼 (HashIV) 放在最後面
checkValue += `&HashIV=${process.env.ECPAY_HASH_IV}`;

// 3. 進行 URLEncode 並轉換大小寫 (這是綠界最常讓人踩坑的特殊規定)
checkValue = encodeURIComponent(checkValue).toLowerCase();
checkValue = checkValue.replace(/%2d/g, '-').replace(/%5f/g, '_').replace(/%2e/g, '.').replace(/%21/g, '!');

// 4. 進行強大的 SHA256 雜湊加密,轉成大寫
const CheckMacValue = crypto.createHash('sha256').update(checkValue).digest('hex').toUpperCase();

// 最後,把這個算出來的 CheckMacValue 也塞進參數裡
params.CheckMacValue = CheckMacValue;

這段程式碼是在伺服器端 (Node.js) 執行的,所以你的 HashKey 絕對不會洩漏給前端的使用者。 算出檢查碼後,我們的 API 會回傳一個包含所有欄位的隱藏 HTML <form> 表單,並透過一段小腳本讓瀏覽器「自動提交 (Auto-Submit)」跳轉到綠界的刷卡網頁。


📡 步驟二:綠界背景回呼機制 (Webhook / Return URL)

這是一般新手工程師最容易搞混、也是引發客訴最頻繁的地方。 當使用者在綠界輸入完信用卡號,按下確認付款並成功扣款後,綠界會有「兩個獨立的動作」:

  1. ClientRedirectURL (前端畫面跳轉):使用者的瀏覽器畫面會跳轉回你設定的「付款成功感謝頁面」。 【致命警告】:絕對不能在這個頁面的邏輯中寫入資料庫!因為使用者可能在刷卡成功的瞬間,網路斷線或是手滑關閉了瀏覽器,導致他永遠沒進到感謝頁面,這樣他的錢被扣了,權限卻沒開通,引發嚴重的客訴。
  2. ReturnURL (伺服器背景 Webhook):這才是最穩定的機制!綠界的伺服器會在背景,默默地發送一個 HTTP POST 請求給你的伺服器,通知你「這筆訂單真的付錢了」。

這支負責接旨的 API 位於 src/app/api/ecpay/return/route.ts

export async function POST(request: Request) {
  // 解析綠界送來的表單資料
  const text = await request.text();
  const params = new URLSearchParams(text);
  
  // 1. 檢查綠界的狀態碼,'1' 代表信用卡付款成功
  const RtnCode = params.get('RtnCode');
  
  if (RtnCode === '1') {
    // 2. 我們在第一步結帳時,利用綠界提供的 CustomField 來偷藏使用者的 UID 與課程 ID
    const userId = params.get('CustomField1');
    const itemId = params.get('CustomField2'); 
    
    // 3. ⚠️ 資安防護:必須用相同的邏輯,驗證綠界傳來的 CheckMacValue,確保這個請求不是駭客偽造的!
    // (此處省略驗證邏輯,請參考實際原始碼)
    
    // 4. 以最高權限 (Service Role) 寫入 Supabase 資料庫
    const supabase = createClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.SUPABASE_SERVICE_ROLE_KEY! // 無視上一章提到的 RLS 防護牆的超級鑰匙
    );
    
    // 寫入購買紀錄,解鎖權限!
    await supabase.from('vt_purchases').insert({
      user_id: userId,
      item_id: itemId,
      item_type: 'course',
      price: parseInt(params.get('TradeAmt') || '0') // 紀錄實際收到的金額
    });
  }
  
  // 5. 【極度重要】不管處理成功與否,一定要回傳純文字 "1|OK" 給綠界
  // 如果你不回傳這個,綠界的系統會以為你的伺服器掛了,它會瘋狂地一直重試打你的 API!
  return new NextResponse('1|OK', {
    headers: { 'Content-Type': 'text/plain' },
  });
}

🧪 步驟三:使用綠界測試信用卡與環境變數設定坑

在開發與測試階段,綠界提供了一組特殊的「測試用信用卡」,讓你可以在不扣真實金錢的情況下跑完完整的刷卡與 Webhook 流程。

💳 官方測試信用卡資訊

結帳時,請選擇信用卡付款並輸入:

  • 信用卡號4311-9522-2222-2222
  • 有效年月:任何未來的日期(例如 12/30
  • 安全碼 (CVV)222
  • 手機驗證碼 (OTP):若跳出簡訊驗證畫面,請隨便輸入 123456 即可通過。

🚨 新手必踩大坑:ReturnURL 與 Localhost

在我們設定 Webhook 時,系統會自動將你的網址作為 ReturnURL 傳給綠界。 這裡有一個致命的雷區:當你把網站部署到 Vercel (正式上線) 時,千萬不能在環境變數把網址設為 http://localhost:3000

綠界的伺服器在外網,它絕對找不到你的「本地端電腦(localhost)」。這會導致:

  1. 綠界結帳畫面顯示「刷卡成功」。
  2. 但綠界發送 Webhook 到 localhost 失敗。
  3. 你的資料庫永遠不會收到付款通知,使用者的課程也不會解鎖,引發嚴重客訴!

解決方案: 在 Vercel 的環境變數 (Environment Variables) 設定中,務必確保程式碼能抓到 Vercel 的正式網址(例如 https://vibe-tutor-web.vercel.app),這樣綠界的 Webhook 訊號才能準確射進你的資料庫!

✅ 本章小結

這就是最經典、最穩定、最堅不可摧的非同步訂單處理流程。 這套強大的 Webhook 機制確保了:即使使用者的手機沒電關機、網路突然斷線,只要銀行的信用卡扣款成功,綠界的伺服器依然會不屈不撓地把訂單成功的資訊,安全地送到我們的資料庫中,瞬間幫使用者解鎖他夢寐以求的課程!

恭喜你!你的印鈔機已經正式組裝完畢並插上電源了。 在下一章,我們將探討如何透過 UI/UX 的「遊戲化 (Gamification)」設計心理學,讓使用者買完一堂課後,還想繼續買下一堂課!

解鎖完整教學內容

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