第8章:印鈔機始動!緑界科技 (ECPay) 決済連携とWebhook注文コールバック処理
ビジネスの世界には残酷な現実があります: 「お金を受け取れないウェブサイトは、アニメーションがどれだけ美しく、構造がどれだけ完璧でも、単なる『ポートフォリオ』でしかない」 「一度決済システムと連携し、24時間365日お金を受け入れられるようになれば、それは『デジタル資産 (Digital Asset)』へと進化する」
台湾でオンラインクレジットカード決済を構築する際、最も安定して市場シェアの高い老舗システムが**緑界科技 (ECPay)**です。
しかし、緑界の公式ドキュメントは新人エンジニアにとって難解な「天書」のようで、特に煩雑なSHA256によるハッシュ化とソート暗号化メカニズムが要求されます。
多くのエンジニアがこの段階で数週間悩んだ末に諦めてしまいます。しかしVibe Tutorのエンタープライズ級ソースコードでは、すべての落とし穴を事前に検証し、複雑な暗号化ロジックをクリーンなNext.js API Routesにカプセル化しています!
🎯 本章の目標
- 緑界決済の動作原理とセキュリティメカニズム(HashKeyが必要な理由)を理解する
- フロントエンド決済API (
/api/ecpay/checkout) の暗号化と自動リダイレクトフローを解析 - 非同期Webhookコールバックメカニズム (
/api/ecpay/return) をマスターし、注文漏れを防止
🛒 ステップ1:フロントエンドフォーム暗号化と送信 (Checkout Route)
ユーザーが価格設定ページで「💳 VIPパスを今すぐ購入」ボタンを押した瞬間、何が起こるのか?
フロントエンドのフォームデータ(courseId、price、tierを含む)はHTTP POSTで私たちのバックエンドAPI Route src/app/api/ecpay/checkout/route.tsに送信されます。
この重要なAPIでは、サーバーサイドで3つの主要タスクを実行する必要があります:
- ユーザーログイン状態の検証:未ログインの場合、支払いを許可しません。支払い後に、このVIPコースをどのアカウントに紐付けるかシステムが判断できないためです。
- 一意の注文番号生成 (MerchantTradeNo):重複しない文字列(例:
VIBE+ タイムスタンプ1710234567)を生成。これは将来の照合のための唯一の証明書です。 - 緑界に必要なパラメータのパッケージングと暗号化:商品名、金額、ReturnURL(コールバックURL)、そして最も重要な偽造防止チェックコードを含みます。
🛡️ 最大の難関:CheckMacValue(偽造防止チェックコード)の計算
なぜ金額を直接緑界に送信できないのか?暗号化せずに送信すると、技術知識のあるハッカーがパケットを傍受し、「9,999元」を「1元」に改ざんする可能性があります。すると緑界は1元を請求し、支払い成功と報告してしまいます。
この問題を解決するため、緑界ではすべてのパラメータと、あなたと緑界だけが知る専用パスワードHashKeyとHashIVを使用し、非常に厳格なルールでソートした上で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. URLエンコードし、小文字に変換(緑界が最もハマりやすい特殊な規定)
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)」します。
📡 ステップ2:緑界バックグラウンドコールバックメカニズム (Webhook / Return URL)
これは新人エンジニアが最も混乱しやすく、クレームが頻発するポイントです。 ユーザーが緑界でクレジットカード番号を入力し、支払いを確認して正常に引き落としが行われると、緑界は「2つの独立したアクション」を実行します:
- ClientRedirectURL(フロントエンド画面リダイレクト):ユーザーのブラウザ画面は設定した「支払い完了感謝ページ」にリダイレクトされます。 【致命的警告】:このページのロジックでデータベースに書き込んではいけません!ユーザーが支払い成功直後にネットワークが切断されたり、ブラウザを誤って閉じてしまう可能性があり、お金は引き落とされたのに権限が開放されない深刻なクレームが発生します。
- 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' },
});
}
🧪 ステップ3:緑界テスト用クレジットカードと環境変数設定の落とし穴
開発とテスト段階では、緑界は特別な「テスト用クレジットカード」を提供しており、実際の金銭を引き落とすことなく完全なカード決済とWebhookフローをテストできます。
💳 公式テスト用クレジットカード情報
決済時、クレジットカード支払いを選択し、以下を入力:
- カード番号:
4311-9522-2222-2222 - 有効期限:将来の任意の日付(例:
12/30) - セキュリティコード (CVV):
222 - SMS認証コード (OTP):SMS認証画面が表示された場合、任意の数字(例:
123456)を入力すれば通過します。
🚨 初心者が必ずハマる落とし穴:ReturnURLとLocalhost
Webhookを設定する際、システムは自動的にあなたのURLをReturnURLとして緑界に送信します。
ここに致命的な落とし穴があります:Vercelにデプロイ(本番環境)する場合、環境変数でURLをhttp://localhost:3000に設定してはいけません!
緑界のサーバーは外部ネットワークにあり、あなたの「ローカルマシン(localhost)」を見つけることは絶対にできません。これにより:
- 緑界の決済画面は「支払い成功」と表示
- しかしlocalhostへのWebhook送信が失敗
- データベースは支払い通知を受け取らず、ユーザーのコースが開放されないままとなり、深刻なクレームが発生!
解決策:
Vercelの環境変数 (Environment Variables) 設定で、コードがVercelの本番URL(例:https://vibe-tutor-web.vercel.app)を確実に取得できるようにします。これにより、緑界のWebhook信号が確実にあなたのデータベースに届きます!
✅ 本章のまとめ
これが最も古典的で、安定し、堅牢な非同期注文処理フローです。 この強力なWebhookメカニズムにより、ユーザーのスマホのバッテリー切れやネットワーク切断が発生しても、銀行のクレジットカード引き落としが成功していれば、緑界のサーバーが確実に注文成功情報をデータベースに送信し、ユーザーが夢見たコースへのアクセスを即座に開放します!
おめでとうございます!あなたの「印鈔機(紙幣印刷機)」は正式に組み立てられ、電源が入りました。 次の章では、UI/UXの「ゲーミフィケーション」設計心理学を通じて、ユーザーが1つのコースを購入した後、さらに次のコースを購入したくなる方法を探ります!