金流與支付整合
台灣常見金流平台
💡 比喻:收銀台和銀行之間的橋梁 你的網站就像一家商店,客人要付錢時不會直接把鈔票塞進電腦螢幕。 金流平台就是那座「橋梁」——幫你把客人的錢從他的銀行帳戶, 安全地搬到你的銀行帳戶,中間還幫你處理信用卡、超商代碼等等。
金流平台比較
台灣主流金流平台比較:
┌──────────────┬──────────┬──────────┬──────────────┐
│ 平台名稱 │ 手續費 │ 撥款週期 │ 適合對象 │
├──────────────┼──────────┼──────────┼──────────────┤
│ 綠界 ECPay │ 2.75% │ 月結 │ 中小企業 │
│ 藍新 NewebPay│ 2.6% │ 月結 │ 中大型企業 │
│ LINE Pay │ 3% │ 月結 │ 行動支付 │
│ 街口支付 │ 2% │ 月結 │ 小店家 │
│ Apple Pay │ 需搭配 │ 依金流 │ iOS 用戶 │
│ PayPal │ 3.4% │ 即時 │ 跨國交易 │
└──────────────┴──────────┴──────────┴──────────────┘
金流串接架構
標準金流流程
金流串接完整流程:
┌─────────┐
│ 客 人 │
└────┬────┘
│ ① 按下結帳按鈕
┌────▼────┐
│ 你的網站 │
└────┬────┘
│ ② 建立訂單 + 產生付款參數
┌────▼────┐
│ 金流平台 │ (ECPay / 藍新)
└────┬────┘
│ ③ 顯示付款頁面(信用卡/ATM)
┌────▼────┐
│ 銀 行 │
└────┬────┘
│ ④ 扣款成功
┌────▼────┐
│ 金流平台 │
└────┬────┘
│ ⑤ 通知你的網站(回呼 URL)
┌────▼────┐
│ 你的網站 │ 更新訂單狀態
└─────────┘
ECPay SDK 整合
設定金流參數
// ECPay 設定模型 // 綠界金流設定
public class ECPaySettings // 金流設定類別
{
public string MerchantID { get; set; } = ""; // 特店編號
public string HashKey { get; set; } = ""; // 加密金鑰
public string HashIV { get; set; } = ""; // 加密向量
public string PaymentApiUrl { get; set; } = ""; // 付款 API 網址
public string ReturnUrl { get; set; } = ""; // 付款結果回傳網址
public string NotifyUrl { get; set; } = ""; // 付款通知回呼網址
}
// appsettings.json 設定範例 // 在設定檔中加入金流設定
// "ECPay": {
// "MerchantID": "3002607", // 測試特店編號
// "HashKey": "pwFHCqoQZGmho4w6", // 測試金鑰
// "HashIV": "EkRm7iFT261dpevs", // 測試向量
// "PaymentApiUrl": "https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5" // 測試 API
// }
建立付款請求
// ECPay 金流服務 // 處理綠界金流串接
public class ECPayService // 綠界金流服務類別
{
private readonly ECPaySettings _settings; // 金流設定
private readonly ILogger<ECPayService> _logger; // 日誌記錄器
// 建構函式 // 注入設定和日誌
public ECPayService( // 建構金流服務
IOptions<ECPaySettings> settings, // 注入設定
ILogger<ECPayService> logger) // 注入日誌
{
_settings = settings.Value; // 取得設定值
_logger = logger; // 儲存日誌記錄器
}
// 建立付款訂單 // 產生送往 ECPay 的表單參數
public Dictionary<string, string> CreatePayment( // 建立付款方法
string orderId, // 訂單編號
int amount, // 金額
string description) // 商品描述
{
// 組合付款參數 // ECPay 要求的欄位
var parameters = new Dictionary<string, string> // 建立參數字典
{
["MerchantID"] = _settings.MerchantID, // 特店編號
["MerchantTradeNo"] = orderId, // 特店交易編號
["MerchantTradeDate"] = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"), // 交易時間
["PaymentType"] = "aio", // 付款類型(全方位)
["TotalAmount"] = amount.ToString(), // 交易金額
["TradeDesc"] = description, // 交易描述
["ItemName"] = description, // 商品名稱
["ReturnURL"] = _settings.NotifyUrl, // 付款結果通知網址
["OrderResultURL"] = _settings.ReturnUrl, // 付款完成導回網址
["ChoosePayment"] = "ALL", // 付款方式(全部)
["EncryptType"] = "1" // 加密類型(SHA256)
};
// 計算檢查碼 // 防止參數被竄改
var checkMacValue = GenerateCheckMacValue(parameters); // 產生檢查碼
parameters["CheckMacValue"] = checkMacValue; // 加入檢查碼
_logger.LogInformation("建立 ECPay 付款:{OrderId}, ${Amount}", // 記錄付款
orderId, amount); // 傳入參數
return parameters; // 回傳付款參數
}
// 產生檢查碼 // SHA256 雜湊驗證
private string GenerateCheckMacValue( // 產生 CheckMacValue 方法
Dictionary<string, string> parameters) // 付款參數
{
// 依照 ECPay 規則排序 // 參數名稱字母排序
var sorted = parameters.OrderBy(p => p.Key); // 排序參數
// 組合字串 // HashKey + 參數 + HashIV
var raw = $"HashKey={_settings.HashKey}&"; // 開頭加 HashKey
raw += string.Join("&", sorted.Select(p => $"{p.Key}={p.Value}")); // 串接所有參數
raw += $"&HashIV={_settings.HashIV}"; // 結尾加 HashIV
// URL Encode 後轉小寫 // ECPay 規定
raw = System.Net.WebUtility.UrlEncode(raw)?.ToLower() ?? ""; // 編碼並轉小寫
// SHA256 雜湊 // 產生最終檢查碼
using var sha256 = System.Security.Cryptography.SHA256.Create(); // 建立 SHA256
var bytes = sha256.ComputeHash( // 計算雜湊
System.Text.Encoding.UTF8.GetBytes(raw)); // UTF-8 編碼
return BitConverter.ToString(bytes) // 轉為十六進位字串
.Replace("-", "").ToUpper(); // 去除破折號並轉大寫
}
// 驗證回呼 // 確認付款通知來自 ECPay
public bool VerifyCallback( // 驗證回呼方法
Dictionary<string, string> formData) // 表單資料
{
if (!formData.ContainsKey("CheckMacValue")) // 檢查是否有檢查碼
return false; // 沒有檢查碼就是偽造的
var receivedMac = formData["CheckMacValue"]; // 取得收到的檢查碼
var paramsWithoutMac = formData // 排除 CheckMacValue
.Where(p => p.Key != "CheckMacValue") // 過濾掉檢查碼
.ToDictionary(p => p.Key, p => p.Value); // 重建字典
var expectedMac = GenerateCheckMacValue(paramsWithoutMac); // 重新計算檢查碼
return receivedMac == expectedMac; // 比對是否一致
}
}
信用卡刷卡機串接
串口通訊刷卡
// 刷卡機串口通訊服務 // 透過 RS232 與刷卡機通訊
public class CardReaderService : IDisposable // 刷卡機服務
{
private readonly SerialPort _port; // 串口物件
private readonly ILogger<CardReaderService> _logger; // 日誌記錄器
// 建構函式 // 初始化刷卡機連線
public CardReaderService( // 建構刷卡機服務
ILogger<CardReaderService> logger) // 注入日誌
{
_logger = logger; // 儲存日誌記錄器
_port = new SerialPort // 建立串口
{
PortName = "/dev/ttyUSB1", // 刷卡機串口
BaudRate = 115200, // 鮑率 115200
DataBits = 8, // 資料位元
Parity = Parity.None, // 無同位元
StopBits = StopBits.One // 停止位元
};
}
// 發送刷卡請求 // 告訴刷卡機要刷多少錢
public async Task<CardTransactionResult> ProcessPaymentAsync( // 刷卡方法
decimal amount) // 刷卡金額
{
try // 嘗試刷卡
{
_port.Open(); // 開啟串口
// 組合刷卡指令 // 依刷卡機協定格式化
var command = FormatCommand(amount); // 格式化刷卡指令
_port.Write(command, 0, command.Length); // 發送指令
_logger.LogInformation("發送刷卡請求:${Amount}", amount); // 記錄金額
// 等待刷卡機回應 // 最多等 60 秒
var response = await WaitForResponseAsync( // 等待回應
TimeSpan.FromSeconds(60)); // 超時時間
return ParseResponse(response); // 解析回應結果
}
catch (Exception ex) // 捕捉例外
{
_logger.LogError(ex, "刷卡處理失敗"); // 記錄錯誤
return new CardTransactionResult // 回傳失敗結果
{
Success = false, // 標記失敗
ErrorMessage = ex.Message // 錯誤訊息
};
}
finally // 最終處理
{
if (_port.IsOpen) _port.Close(); // 確保關閉串口
}
}
private byte[] FormatCommand(decimal amount) => // 格式化指令方法
System.Text.Encoding.ASCII.GetBytes( // 轉為位元組陣列
$"SALE:{amount:F2}\r\n"); // 刷卡指令格式
private Task<byte[]> WaitForResponseAsync( // 等待回應方法
TimeSpan timeout) => // 超時時間
Task.FromResult(Array.Empty<byte>()); // 回傳空陣列(待實作)
private CardTransactionResult ParseResponse( // 解析回應方法
byte[] response) => // 回應位元組
new() { Success = true }; // 回傳結果(待實作)
public void Dispose() => _port.Dispose(); // 釋放串口資源
}
// 刷卡交易結果 // 記錄刷卡結果
public class CardTransactionResult // 交易結果類別
{
public bool Success { get; set; } // 是否成功
public string? AuthCode { get; set; } // 授權碼
public string? CardNumber { get; set; } // 卡號末四碼
public string? ErrorMessage { get; set; } // 錯誤訊息
}
QR Code 行動支付
產生 QR Code
// QR Code 支付服務 // 產生行動支付用的 QR Code
public class QrPaymentService // QR 支付服務類別
{
private readonly ILogger<QrPaymentService> _logger; // 日誌記錄器
// 建構函式 // 注入日誌
public QrPaymentService( // 建構 QR 支付服務
ILogger<QrPaymentService> logger) // 注入 Logger
{
_logger = logger; // 儲存日誌記錄器
}
// 產生 LINE Pay QR Code 網址 // 客人掃碼付款
public string GenerateLinePayUrl( // 產生 LINE Pay 網址方法
string orderId, // 訂單編號
int amount) // 金額
{
// LINE Pay API 付款請求 // 取得付款網址
var paymentUrl = $"https://sandbox-api-pay.line.me/v3/payments/request"; // API 端點
_logger.LogInformation( // 記錄產生 QR Code
"產生 LINE Pay QR:訂單 {OrderId}, ${Amount}", // 格式化訊息
orderId, amount); // 傳入參數
return paymentUrl; // 回傳付款網址
}
}
電子發票串接
電子發票服務
// 電子發票服務 // 串接財政部電子發票 API
public class InvoiceService // 電子發票服務類別
{
private readonly HttpClient _httpClient; // HTTP 客戶端
private readonly ILogger<InvoiceService> _logger; // 日誌記錄器
// 發票資料模型 // 開立發票所需資訊
public class InvoiceRequest // 發票請求類別
{
public string BuyerIdentifier { get; set; } = ""; // 買方統編(空白為個人)
public string BuyerName { get; set; } = ""; // 買方名稱
public List<InvoiceItem> Items { get; set; } = new(); // 商品明細
public int TotalAmount { get; set; } // 總金額
public string CarrierType { get; set; } = ""; // 載具類型
public string CarrierNum { get; set; } = ""; // 載具號碼
}
// 發票商品明細 // 每一筆商品資訊
public class InvoiceItem // 發票商品類別
{
public string Name { get; set; } = ""; // 商品名稱
public int Quantity { get; set; } // 數量
public decimal UnitPrice { get; set; } // 單價
public decimal Amount { get; set; } // 小計
}
// 建構函式 // 注入 HttpClient
public InvoiceService( // 建構發票服務
HttpClient httpClient, // 注入 HTTP 客戶端
ILogger<InvoiceService> logger) // 注入日誌
{
_httpClient = httpClient; // 儲存 HTTP 客戶端
_logger = logger; // 儲存日誌記錄器
}
// 開立電子發票 // 呼叫財政部 API
public async Task<string?> IssueInvoiceAsync( // 開立發票方法
InvoiceRequest request) // 發票請求
{
_logger.LogInformation( // 記錄開立發票
"開立電子發票:{Amount} 元", request.TotalAmount); // 傳入金額
// 呼叫財政部 API // 取得發票號碼
// 實際實作需依照財政部規格 // 這裡是框架示範
var invoiceNumber = $"AB-{DateTime.Now:yyyyMMdd}-{Guid.NewGuid():N}"; // 模擬發票號碼
return invoiceNumber; // 回傳發票號碼
}
}
退款流程處理
// 退款服務 // 處理各種退款情境
public class RefundService // 退款服務類別
{
private readonly ECPayService _ecpay; // 綠界金流服務
private readonly ILogger<RefundService> _logger; // 日誌記錄器
// 建構函式 // 注入金流服務
public RefundService( // 建構退款服務
ECPayService ecpay, // 注入綠界服務
ILogger<RefundService> logger) // 注入日誌
{
_ecpay = ecpay; // 儲存金流服務
_logger = logger; // 儲存日誌記錄器
}
// 處理退款 // 依付款方式執行退款
public async Task<RefundResult> ProcessRefundAsync( // 退款方法
string orderId, // 訂單編號
decimal amount, // 退款金額
string reason) // 退款原因
{
_logger.LogInformation( // 記錄退款請求
"處理退款:訂單 {OrderId}, ${Amount}, 原因:{Reason}", // 格式化訊息
orderId, amount, reason); // 傳入參數
// 驗證退款金額 // 不能超過原始金額
if (amount <= 0) // 金額必須大於零
{
return new RefundResult // 回傳失敗
{
Success = false, // 標記失敗
Message = "退款金額必須大於零" // 錯誤訊息
};
}
// 呼叫金流平台退款 API // 實際執行退款
await Task.CompletedTask; // 非同步退款(待實作)
return new RefundResult // 回傳成功
{
Success = true, // 標記成功
Message = "退款處理中", // 成功訊息
RefundId = Guid.NewGuid().ToString() // 退款編號
};
}
}
// 退款結果 // 退款處理回傳
public class RefundResult // 退款結果類別
{
public bool Success { get; set; } // 是否成功
public string Message { get; set; } = ""; // 結果訊息
public string? RefundId { get; set; } // 退款編號
}
金流安全
防重複付款
// 冪等性付款服務 // 防止重複扣款
public class IdempotentPaymentService // 冪等付款服務類別
{
private readonly IDistributedCache _cache; // 分散式快取
private readonly ILogger<IdempotentPaymentService> _logger; // 日誌記錄器
// 建構函式 // 注入快取和日誌
public IdempotentPaymentService( // 建構冪等付款服務
IDistributedCache cache, // 注入分散式快取
ILogger<IdempotentPaymentService> logger) // 注入日誌
{
_cache = cache; // 儲存快取
_logger = logger; // 儲存日誌記錄器
}
// 處理付款(防重複) // 同一筆訂單只會扣一次
public async Task<PaymentResult> ProcessPaymentAsync( // 防重複付款方法
string idempotencyKey, // 冪等鍵(通常用訂單編號)
Func<Task<PaymentResult>> paymentAction) // 實際付款動作
{
// 檢查是否已處理過 // 從快取查詢
var cached = await _cache.GetStringAsync(idempotencyKey); // 查詢快取
if (cached != null) // 如果已經處理過
{
_logger.LogWarning("偵測到重複付款:{Key}", idempotencyKey); // 記錄重複
return System.Text.Json.JsonSerializer // 反序列化快取結果
.Deserialize<PaymentResult>(cached)!; // 回傳之前的結果
}
// 執行付款 // 第一次處理
var result = await paymentAction(); // 執行實際付款動作
// 快取結果(24小時) // 防止重複
await _cache.SetStringAsync(idempotencyKey, // 儲存到快取
System.Text.Json.JsonSerializer.Serialize(result), // 序列化結果
new DistributedCacheEntryOptions // 快取選項
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24) // 24 小時後過期
});
return result; // 回傳付款結果
}
}
// 付款結果 // 付款處理回傳
public class PaymentResult // 付款結果類別
{
public bool Success { get; set; } // 是否成功
public string? TransactionId { get; set; } // 交易編號
public string? Message { get; set; } // 結果訊息
}
對帳系統設計
// 對帳服務 // 每日自動對帳
public class ReconciliationService // 對帳服務類別
{
private readonly ILogger<ReconciliationService> _logger; // 日誌記錄器
// 建構函式 // 注入日誌
public ReconciliationService( // 建構對帳服務
ILogger<ReconciliationService> logger) // 注入 Logger
{
_logger = logger; // 儲存日誌記錄器
}
// 每日對帳 // 比對系統訂單和金流平台紀錄
public async Task<ReconciliationReport> DailyReconcileAsync( // 每日對帳方法
DateTime date) // 對帳日期
{
_logger.LogInformation("開始對帳:{Date:yyyy-MM-dd}", date); // 記錄開始
var report = new ReconciliationReport // 建立對帳報告
{
Date = date, // 對帳日期
TotalOrders = 0, // 系統訂單數
TotalPayments = 0, // 金流平台筆數
MatchedCount = 0, // 對帳成功筆數
MismatchedCount = 0 // 對帳失敗筆數
};
// 實際對帳邏輯 // 比對系統和金流資料
await Task.CompletedTask; // 非同步對帳(待實作)
return report; // 回傳對帳報告
}
}
// 對帳報告 // 對帳結果統計
public class ReconciliationReport // 對帳報告類別
{
public DateTime Date { get; set; } // 對帳日期
public int TotalOrders { get; set; } // 系統訂單總數
public int TotalPayments { get; set; } // 金流筆數
public int MatchedCount { get; set; } // 成功比對筆數
public int MismatchedCount { get; set; } // 失敗比對筆數
}
🤔 我這樣寫為什麼會錯?
❌ 錯誤:金流 HashKey 寫在程式碼裡
// 錯誤寫法 // 把機密金鑰寫死在程式碼裡
public class BadPaymentService // 錯誤的金流服務
{
private const string HashKey = "pwFHCqoQZGmho4w6"; // 金鑰寫死在程式碼 ← 超危險!
private const string HashIV = "EkRm7iFT261dpevs"; // 向量也寫死 ← 會被 Git 追蹤到!
}
✅ 正確:使用環境變數或 Secret Manager
// 正確寫法 // 金鑰放在安全的地方
public class SecurePaymentService // 安全的金流服務
{
private readonly string _hashKey; // 金鑰(不寫死)
public SecurePaymentService( // 建構函式
IConfiguration config) // 注入設定
{
// 從環境變數或 User Secrets 讀取 // 不會出現在原始碼
_hashKey = config["ECPay:HashKey"] // 從設定讀取金鑰
?? throw new InvalidOperationException( // 找不到就拋例外
"ECPay:HashKey 未設定"); // 錯誤訊息
}
}
❌ 錯誤:沒有驗證回呼來源
// 錯誤寫法 // 任何人都能偽造付款成功通知
[HttpPost("payment/callback")] // 付款回呼端點
public IActionResult PaymentCallback( // 回呼方法
[FromForm] Dictionary<string, string> data) // 表單資料
{
// 直接信任回呼資料 ← 沒有驗證! // 任何人都能偽造
var orderId = data["MerchantTradeNo"]; // 直接取訂單編號
UpdateOrderStatus(orderId, "paid"); // 直接更新為已付款
return Ok(); // 回傳成功
}
private void UpdateOrderStatus(string id, string s) { } // 更新訂單狀態
✅ 正確:驗證 CheckMacValue
// 正確寫法 // 驗證回呼是否來自金流平台
[HttpPost("payment/callback")] // 付款回呼端點
public IActionResult PaymentCallback( // 回呼方法
[FromForm] Dictionary<string, string> data) // 表單資料
{
// 先驗證 CheckMacValue // 確認是 ECPay 發的
if (!_ecpayService.VerifyCallback(data)) // 驗證檢查碼
{
_logger.LogWarning("收到偽造的付款回呼!"); // 記錄可疑行為
return BadRequest("驗證失敗"); // 拒絕偽造請求
}
// 驗證通過才更新 // 確保資料真實性
var orderId = data["MerchantTradeNo"]; // 取得訂單編號
UpdateOrderStatus(orderId, "paid"); // 安全地更新狀態
return Ok("1|OK"); // ECPay 要求回傳此格式
}
private ECPayService _ecpayService = null!; // 金流服務
private ILogger _logger2 = null!; // 日誌記錄器
private void UpdateOrderStatus(string id, string s) { } // 更新訂單