歡迎來到第二十四天!金流串接最怕上線切換時因環境變數或簽章計算錯誤導致「付了錢卻沒拿到點數」的掉單事故。
// ECPayConfig.kt
data class ECPayConfig(
val merchantID: String,
val hashKey: String,
val hashIV: String,
val apiUrl: String,
val returnUrl: String
) {
companion object {
fun fromEnv(isProduction: Boolean): ECPayConfig {
return if (isProduction) {
ECPayConfig(
merchantID = System.getenv("ECPAY_PROD_MERCHANT_ID")!!,
hashKey = System.getenv("ECPAY_PROD_HASH_KEY")!!,
hashIV = System.getenv("ECPAY_PROD_HASH_IV")!!,
apiUrl = "https://payment.ecpay.com.tw/Cashier/AioCheckOut/V5",
returnUrl = "https://api.yourstore.com/api/v1/payment/ecpay_webhook"
)
} else {
ECPayConfig(
merchantID = "2000132", // 沙盒測試特店編號
hashKey = "5294y06J2Lu523ge",
hashIV = "v77hoKGq4kWxNNIS",
apiUrl = "https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5",
returnUrl = "https://dev-api.yourstore.com/api/v1/payment/ecpay_webhook"
)
}
}
}
}
當 Webhook 發生異常時,稽核日誌是唯一的除錯依據:
// PaymentAuditLogTable.kt
object PaymentAuditLogTable : Table("payment_audit_logs") {
val id = uuid("id").default(UUID.randomUUID())
val tradeNo = varchar("trade_no", 64)
val rawPayload = text("raw_payload")
val isSignatureValid = boolean("is_signature_valid")
val processedStatus = varchar("processed_status", 32)
val createdAt = long("created_at").default(System.currentTimeMillis())
override val primaryKey = PrimaryKey(id)
}
Tech Tip:
- URL-Encoding 大小寫字母:綠界 HMAC 驗證要求 URL-Encode 後的字串小寫,但
-_.!*()符號必須保持特定轉義,否則 CheckMacValue 永遠對不上!- IP 白名單與防火牆:確認雲端防火牆放行綠界通知伺服器 IP 段。
- 全鏈路稽核日誌:不管簽章是否成功,第一個步驟永遠先將 Raw Payload 寫入
payment_audit_logs!