💳 第十四章:綠界金流 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 (檢查碼) 機制。
- 我們把「訂單編號、金額、商品名稱」等資訊,加上只有你和綠界知道的 HashKey 與 HashIV (機密金鑰)。
- 把它們全部揉在一起,經過一系列複雜的 URL Encode 與字元替換。
- 最後用 SHA-256 演算法壓扁成一段 64 碼的亂碼 (這就是 CheckMacValue)。
- 我們把這段亂碼連同金額一起送給綠界。
- 綠界收到後,用他們那邊的機密金鑰也算一次。如果算出來的亂碼跟你送來的一模一樣,綠界就承認這筆訂單;如果不一樣,就代表有人中途篡改了金額,綠界會直接報錯拒絕交易!
這就是資安防護的核心。而這個計算過程超級容易寫錯(只要大小寫不對、或者少替換一個符號,檢查碼就會錯誤)。 過去,工程師要花好幾天 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 字的金流實戰課,你徹底征服了過去讓無數新手撞牆的台灣魔王金流:綠界科技。
回顧我們打造印鈔機的關鍵:
- 永遠在後端算錢:
CheckMacValue是金流防偽的核心,一定要放在 Node.js (Astro API Route) 裡算,絕不能流到前端。 - 自動跳轉表單:我們沒有用花俏的 SPA Router 轉跳,而是直接寫一段純 HTML 表單並用 JS
submit()瞬間跳走,這解決了所有跨網域與金流阻擋的怪問題。 - Webhook 零信任原則:永遠不要相信從前端跳回來的使用者,只相信綠界 Server to Server 傳過來的
ReturnURL,而且每一次都要重新計算 CheckMacValue 來驗明正身。
掌握了這套打法,你不只可以賣露營地,你可以賣軟體、賣課程、賣顧問服務。 你已經擁有一台 24 小時為你收錢的自動化機器了。這就是全端工程師真正的價值所在! 恭喜畢業!