💳 第十四章:綠界金流 ECPay 串接實戰

恭喜你完成了不遠露營度假山莊的前台展示與 Supabase 資料庫串接! 但是,一個沒有收銀機的網站,永遠無法為你帶來真實的收入。 想像一下:客戶在網站上選好了日期、挑好了「星空神殿帳」,結果最後一步卻是「請加我們的 Line 客服,匯款後通知我們」... 這在 2024 年是完全無法接受的使用者體驗,你會因此流失 50% 以上的衝動購物客!

在台灣,最普及、也是最適合獨立開發者與 SaaS 創業者的金流服務就是 綠界科技 (ECPay)。 只要串好綠界,客戶就可以在你的網站上直接刷信用卡、使用 Apple Pay、街口支付,甚至去 7-11 掃條碼付錢!

這堂高達 6000 字的終極實戰課,我們將用 Vibe Coding 的方式,帶你一步步拆解傳統工程師最害怕的「CheckMacValue 雜湊加密演算法」,並將綠界金流完美整合進我們的 not-far-web 專案中!


🔐 為什麼綠界金流這麼難串?

在開始寫程式之前,我們必須先了解金流的本質。 當我們要把客戶轉跳到綠界的刷卡頁面時,我們不能只是在網址後面加上 ?price=1500。 因為如果我們這麼做,聰明的駭客只要在瀏覽器上把 ?price=1500 改成 ?price=1,他就可以用 1 塊錢買走你價值 1500 元的服務!

為了解決這個問題,綠界發明了 CheckMacValue (檢查碼) 機制。

  1. 我們把「訂單編號、金額、商品名稱」等資訊,加上只有你和綠界知道的 HashKeyHashIV (機密金鑰)。
  2. 把它們全部揉在一起,經過一系列複雜的 URL Encode 與字元替換。
  3. 最後用 SHA-256 演算法壓扁成一段 64 碼的亂碼 (這就是 CheckMacValue)。
  4. 我們把這段亂碼連同金額一起送給綠界。
  5. 綠界收到後,用他們那邊的機密金鑰也算一次。如果算出來的亂碼跟你送來的一模一樣,綠界就承認這筆訂單;如果不一樣,就代表有人中途篡改了金額,綠界會直接報錯拒絕交易!

這就是資安防護的核心。而這個計算過程超級容易寫錯(只要大小寫不對、或者少替換一個符號,檢查碼就會錯誤)。 過去,工程師要花好幾天 Debug 這個演算法。現在,我們有 AI!


⚙️ 實戰 1:申請綠界測試帳號與準備環境變數

在寫程式之前,請先準備好你的「收銀機鑰匙」。 如果你還沒有去申請綠界會員,我們可以使用綠界提供的公版測試帳號

請在你的 not-far-web 專案根目錄建立(或打開) .env 檔案,加入以下設定:

# 綠界測試環境 API 端點
ECPAY_ENDPOINT="https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5"
# 綠界公版測試特店編號
ECPAY_MERCHANT_ID="3002607"
# 綠界公版測試 HashKey (絕對不可外洩)
ECPAY_HASH_KEY="pwFHCqoQZGmho4w6"
# 綠界公版測試 HashIV (絕對不可外洩)
ECPAY_HASH_IV="EkRm7iFT261dpevs"

# 開發環境的本機網址 (如果你的 Astro 跑在 4321 port)
ECPAY_RETURN_URL="https://localhost:4321/api/ecpay/return"
ECPAY_CLIENT_BACK_URL="https://localhost:4321/booking-success"

🧙‍♂️ 實戰 2:讓 AI 寫出綠界專用的加密演算法

現在,我們要寫一個後端 API。 注意:金流加密絕對不能在前端 (瀏覽器) 做! 如果你在前端做,駭客只要打開 F12 開發者工具,就能把你的 HashKey 看光光。我們必須在 Astro 的伺服器端 (SSR API Route) 來處理這件事。

💡 Vibe Prompt 實戰 1:生成 CheckMacValue 演算法

不要自己手刻加密邏輯,讓 Claude 3.5 Sonnet 或 GPT-4o 來幫你!

[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:

我正在使用 Node.js / TypeScript 串接台灣綠界金流 (ECPay)。 請幫我寫一個函數 generateCheckMacValue(params: Record<string, string>, hashKey: string, hashIV: string): string。 需求: 1. 依照綠界官方規格,先將 keys 以字母順序排序。 2. 組合成 HashKey=xxx&Key1=Value1&Key2=Value2...&HashIV=xxx。 3. 使用 encodeURIComponent 轉碼後全部轉小寫。 4. 進行綠界專屬的特殊字元替換 (取代 %2d, %5f, %2e, %21, %2a, %28, %29, %20 為對應符號)。 5. 使用 crypto 套件進行 SHA256 雜湊,最後轉為全大寫回傳。

🤖 AI 生成的完美加密核心:

import crypto from 'crypto';

function generateCheckMacValue(params: Record<string, string>, hashKey: string, hashIV: string): string {
  // 1. 依據英文字母順序排序 (A-Z)
  const sortedKeys = Object.keys(params).sort();
  
  // 2. 組合字串,頭尾加上 HashKey 與 HashIV
  let str = `HashKey=${hashKey}`;
  for (const key of sortedKeys) {
    str += `&${key}=${params[key]}`;
  }
  str += `&HashIV=${hashIV}`;

  // 3. 進行 URL Encode 並轉小寫
  let encoded = encodeURIComponent(str).toLowerCase();
  
  // 4. 綠界最惱人的 C# url encode 特殊字元相容替換
  // 這是無數工程師熬夜踩雷才找出來的對應表!
  encoded = encoded
    .replace(/%2d/g, '-')
    .replace(/%5f/g, '_')
    .replace(/%2e/g, '.')
    .replace(/%21/g, '!')
    .replace(/%2a/g, '*')
    .replace(/%28/g, '(')
    .replace(/%29/g, ')')
    .replace(/%20/g, '+')
    .replace(/'/g, '%27')
    .replace(/~/g, '%7e');

  // 5. 使用 SHA-256 進行不可逆加密,並轉為大寫
  return crypto.createHash('sha256').update(encoded).digest('hex').toUpperCase();
}

這段程式碼價值連城!如果你少加了一行 .replace(/%20/g, '+'),只要你的商品名稱裡面包含「空白」,綠界就會瘋狂報錯。而 AI 直接幫我們把這段雷區避開了。


🚀 實戰 3:建立結帳跳轉 API (Astro API Route)

有了加密核心,我們現在要在 not-far-web 中建立一支結帳 API。 當使用者在購物車點擊「確認結帳」時,前端會把訂單資訊 POST 給這支 API,API 算好檢查碼後,會吐出一個「帶有自動送出腳本的 HTML 表單」,將使用者送到綠界。

建立檔案:src/pages/api/ecpay/create.ts

💡 Vibe Prompt 實戰 2:撰寫結帳 API

[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:

在 Astro 框架中,我要建立一支 GET / POST API Route (/api/ecpay/create)。 請使用剛才的 generateCheckMacValue 函數。 流程: 1. 接收前端傳來的 courseId 與 price。 2. 產生一筆時間戳訂單號 MerchantTradeNo。 3. 讀取 env 中的 ECPAY_MERCHANT_ID, HASH_KEY, HASH_IV。 4. 整理所有綠界必填參數 (MerchantID, MerchantTradeDate, PaymentType='aio', TotalAmount, TradeDesc, ItemName, ReturnURL, ChoosePayment='Credit', EncryptType='1', ClientBackURL)。 5. 計算出 CheckMacValue 後,動態生成一段包含隱藏 input 的 HTML 程式碼,並在 HTML 中加入一段 JavaScript (document.getElementById('form').submit()) 讓畫面自動跳轉。 6. 回傳這段 HTML 給瀏覽器 (Content-Type: text/html)。

🤖 AI 的無縫跳轉引擎:

import type { APIRoute } from 'astro';
import crypto from 'crypto';

// (此處省略上方的 generateCheckMacValue 函數...)

export const POST: APIRoute = async ({ request }) => {
  const formData = await request.formData();
  const campName = formData.get('campName') as string || '不遠露營度假山莊線上預訂';
  const price = formData.get('price') as string || '1500';

  // 產生時間戳訂單號 (不能超過 20 碼)
  const timestamp = new Date().getTime().toString().slice(-6);
  const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
  const orderNo = `CAMP${timestamp}${random}`;

  const merchantId = import.meta.env.ECPAY_MERCHANT_ID;
  const hashKey = import.meta.env.ECPAY_HASH_KEY;
  const hashIv = import.meta.env.ECPAY_HASH_IV;
  const endpoint = import.meta.env.ECPAY_ENDPOINT;

  // 取得台灣時間並格式化為 yyyy/MM/dd HH:mm:ss
  const twTime = new Date(new Date().getTime() + 8 * 60 * 60 * 1000);
  const pad = (n: number) => n.toString().padStart(2, '0');
  const tradeDate = `${twTime.getUTCFullYear()}/${pad(twTime.getUTCMonth() + 1)}/${pad(twTime.getUTCDate())} ${pad(twTime.getUTCHours())}:${pad(twTime.getUTCMinutes())}:${pad(twTime.getUTCSeconds())}`;

  const params: Record<string, string> = {
    MerchantID: merchantId,
    MerchantTradeNo: orderNo,
    MerchantTradeDate: tradeDate,
    PaymentType: 'aio',
    TotalAmount: price.toString(),
    TradeDesc: '不遠露營度假山莊線上預訂',
    ItemName: campName,
    ReturnURL: import.meta.env.ECPAY_RETURN_URL, // 這是給綠界 Server 呼叫的
    ChoosePayment: 'Credit', // 只開放信用卡結帳
    EncryptType: '1',
    ClientBackURL: import.meta.env.ECPAY_CLIENT_BACK_URL, // 這是給客戶結帳後點擊返回的
  };

  // 算出終極密碼
  params.CheckMacValue = generateCheckMacValue(params, hashKey, hashIv);

  // 把所有參數變成隱藏的 input 表單
  const inputs = Object.entries(params).map(([key, val]) => 
    `<input type="hidden" name="${key}" value="${val}" />`
  ).join('\n');

  // 製造一個「載入中」畫面,並在背後瞬間把表單往綠界送
  const html = `
    <!DOCTYPE html>
    <html lang="zh-TW">
    <head>
      <meta charset="UTF-8">
      <title>準備進入綠界安全加密頻道</title>
      <style>
        body { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; background: #0f172a; color: white; font-family: sans-serif; }
        .spinner { border: 4px solid rgba(255,255,255,0.1); border-top: 4px solid #10b981; border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite; margin-bottom: 20px;}
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
      </style>
    </head>
    <body>
      <div class="spinner"></div>
      <h2>連線安全加密伺服器中...</h2>
      <form id="ecpay-form" action="${endpoint}" method="POST">
        ${inputs}
      </form>
      <script>
        // 為了讓使用者看到酷炫的載入畫面,我們故意延遲 0.8 秒再轉跳
        setTimeout(() => {
          document.getElementById('ecpay-form').submit();
        }, 800);
      </script>
    </body>
    </html>
  `;

  return new Response(html, {
    headers: { 'Content-Type': 'text/html; charset=utf-8' },
  });
};

📡 實戰 4:防護偽造的 Server Callback (Webhooks)

很多人以為,客戶刷卡成功後,綠界把客戶導回我們的網站 (ClientBackURL) 就算付款成功了。 大錯特錯!這是超級危險的做法! 因為駭客可以自己捏造一個網址 https://你的網站.com/booking-success?order=123 自己點擊,你的系統如果光靠這個就出貨,你明天就破產了。

真正安全的做法是接收綠界的 ReturnURL (Server Callback)。 綠界的伺服器會主動發送一個 HTTP POST 請求到你的伺服器(就像 Line Bot 的 Webhook 一樣)。這個請求裡面會包含 RtnCode=1 (代表刷卡成功),並且也會帶有一組 CheckMacValue! 你必須把綠界傳來的參數,用同樣的演算法再算一次,如果算出來的密碼跟綠界傳來的密碼一樣,這才代表這筆資料真的是綠界傳來的,而不是駭客偽造的!

建立檔案:src/pages/api/ecpay/return.ts

💡 Vibe Prompt 實戰 3:實作極度安全的 Webhook 驗證

[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:

我需要一支處理綠界 ReturnURL 的 Astro API (/api/ecpay/return)。 1. 接收 application/x-www-form-urlencoded 格式的 POST 請求。 2. 將所有參數提取出來,先將傳入的 CheckMacValue 取出並從物件中刪除。 3. 使用剩下的參數,呼叫我們寫好的 generateCheckMacValue 重新計算一次。 4. 比對重新計算的值是否等於傳入的 CheckMacValue。如果不等,代表遭人偽造,回傳 '0|Error'。 5. 如果驗證通過,檢查 RtnCode 是否為 '1'。如果是,請印出 "訂單付款成功!" (實戰中這裡會把狀態寫入 Supabase)。 6. 最終必須回傳 '1|OK' 給綠界。

🤖 AI 的銅牆鐵壁驗證站:

import type { APIRoute } from 'astro';
// (省略 generateCheckMacValue 導入)

export const POST: APIRoute = async ({ request }) => {
  try {
    const formData = await request.formData();
    const data: Record<string, string> = {};
    formData.forEach((value, key) => {
      data[key] = value.toString();
    });

    console.log("=== 收到綠界金流 Webhook ===");
    console.log("訂單編號:", data.MerchantTradeNo);
    console.log("交易狀態代碼:", data.RtnCode);

    // 1. 取出並分離綠界傳來的 CheckMacValue
    const receivedMac = data.CheckMacValue;
    delete data.CheckMacValue; // 刪除後才能重新計算

    // 2. 重新計算檢查碼
    const hashKey = import.meta.env.ECPAY_HASH_KEY;
    const hashIv = import.meta.env.ECPAY_HASH_IV;
    const calculatedMac = generateCheckMacValue(data, hashKey, hashIv);

    // 3. 防偽造比對
    if (calculatedMac !== receivedMac) {
      console.error("🚨 嚴重警告:檢查碼驗證失敗!可能是偽造的請求!");
      return new Response('0|Error', { status: 400 });
    }

    console.log("✅ 檢查碼驗證通過!這筆資料確實來自綠界。");

    // 4. 判斷是否付款成功
    if (data.RtnCode === '1') {
      console.log(`💰 訂單 ${data.MerchantTradeNo} 刷卡成功!金額: ${data.TradeAmt}`);
      // 這裡應該呼叫 Supabase:
      // await supabase.from('orders').update({ status: 'PAID' }).eq('order_no', data.MerchantTradeNo);
    } else {
      console.log(`❌ 訂單 ${data.MerchantTradeNo} 付款失敗。原因: ${data.RtnMsg}`);
    }

    // 5. 必須回傳 1|OK 給綠界,否則綠界會認為你沒收到,一直重新發送浪費資源
    return new Response('1|OK', {
      headers: { 'Content-Type': 'text/plain' },
    });
  } catch (error) {
    console.error('ECPay Return Error:', error);
    return new Response('0|Error', { status: 500 });
  }
};

💻 實戰 5:串接前端,讓按鈕活起來!

最後一哩路,我們要把網頁上的「立即預訂」按鈕,變成發送資料給我們 API 的觸發器。

只要把你的按鈕包在一個 <form> 裡面,一切就搞定了!沒有複雜的 AJAX,沒有 CORS 問題,這就是網頁開發最原始、最迷人的暴力美學!

<!-- 在你的 React/Astro 組件中 -->
<form action="/api/ecpay/create" method="POST">
  <!-- 把你要買的商品跟金額藏在隱藏欄位 -->
  <input type="hidden" name="campName" value="星空神殿帳 週末包場" />
  <input type="hidden" name="price" value="5800" />
  
  <button 
    type="submit" 
    class="w-full bg-emerald-500 hover:bg-emerald-600 text-white font-bold py-4 rounded-xl transition-all shadow-lg shadow-emerald-500/30"
    onclick="this.innerHTML='加密連線中...'; this.classList.add('opacity-50');"
  >
    信用卡結帳 (TWD 5,800)
  </button>
</form>

✅ 本章總結與自動化心法

這堂長達 6000 字的金流實戰課,你徹底征服了過去讓無數新手撞牆的台灣魔王金流:綠界科技。

回顧我們打造印鈔機的關鍵:

  1. 永遠在後端算錢CheckMacValue 是金流防偽的核心,一定要放在 Node.js (Astro API Route) 裡算,絕不能流到前端。
  2. 自動跳轉表單:我們沒有用花俏的 SPA Router 轉跳,而是直接寫一段純 HTML 表單並用 JS submit() 瞬間跳走,這解決了所有跨網域與金流阻擋的怪問題。
  3. Webhook 零信任原則:永遠不要相信從前端跳回來的使用者,只相信綠界 Server to Server 傳過來的 ReturnURL,而且每一次都要重新計算 CheckMacValue 來驗明正身。

掌握了這套打法,你不只可以賣露營地,你可以賣軟體、賣課程、賣顧問服務。 你已經擁有一台 24 小時為你收錢的自動化機器了。這就是全端工程師真正的價值所在! 恭喜畢業!

解鎖完整教學內容

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