藍新金流串接實作

前言

練習藍新金流,記錄實作步驟與需要的參數,參考 alpha camp 教材以及 藍新串接官方文件

邏輯

在串接上第三方支付平台需要取得交易參數

參數 中文 備註
MerchantID 商店代號 藍新金流商店代號
Version 串接程式版本
RespondType 回傳格式 Json 或 string
TimeStamp 時間戳
Amt 訂單金額
Email 付款人電子信箱
MerchantOrderNo 商店訂單編號
ItemDesc 商品資訊 鑑別訂單紀錄
LoginType 藍新金流會員 1 為需要登入,0 為不需要,對 client 的要求
ReturnURL 支付完成返還商店網址
NotifyURL 支付通知網址 每期授權結果通知
ClientBackURL 支付取消返回網址

資料會進行加密才進行傳送參數

參數 中文 備註
MerchantID 商品代號
TradInfo 交易資料 AES 加密
TradInSha 交易資料 SHA256 加密
Version 串接程式版本

前端頁面會先取得訂單付款頁面,取得訂單編號總金額金流參數加密過後資料(AES、SHA256)

教材顯示 MerchantIDTradeInfoTradeShaVersion,官方是以字串呈現,以及教材導向網址 PayGateWay(可更改命名是為表單給予要導向的網頁,教材命名這個名稱),在按出按鈕後傳送到付款頁面。

官方 ( HTML )

  • 正式: https://core.newebpay.com/MPG/period
  • 測試: https://ccore.newebpay.com/MPG/period
  • 新網址: https://ccore.newebpay.com/MPG/mpg_gateway
<form action="https://ccore.newebpay.com/MPG/mpg_gateway" method="POST">
<input type="text" name="MerchantID" value="MS35199" />
<input type="text" name="PostData_" value="357d8c54d0d" /> //加密後字串
<input type="submit" value="go" />
</form>

教材( 使用 handlebars )

<h3>準備為訂單編號: {{order.id}} 付款</h3>
<h4>總價 {{order.amount}}</h4>
<h4>總價 {{order.amount}}</h4>

<form name="Spgateway" action="{{tradeInfo.PayGateWay}}" method="POST">
MerchantID:
<input type="text" name="MerchantID" value="{{tradeInfo.MerchantID}}" /><br />
TradeInfo:
<input type="text" name="TradeInfo" value="{{tradeInfo.TradeInfo}}" /><br />
TradeSha:
<input type="text" name="TradeSha" value="{{tradeInfo.TradeSha}}" /><br />
Version:
<input type="text" name="Version" value="{{tradeInfo.Version}}" /><br />
<button type="submit" class="btn btn-primary">Payment</button>
</form>

步驟

  1. 付款頁面,如果有購物餐車,顯示購物車資料,在按付款後才會導向真正的付款頁面,這裡為 /order/:id/payment
  2. 串接金流 Get /order/:id/payment,取得訂單 data,使用 4 個 function , genDataChain 將物件轉換成字串、create_mpg_aes_encrypt 加密、create_mpg_sha_encrypt雜湊打包 TradeInfo進行交易、回傳後使用的 create_mpg_aes_decrypt解密,最後解密重新取得 TradeInfo 處理已取得的資料。

    • getTradeInfo - 取得 data
    • getTradeInfo - data 放入 mpg_aes_encrypt(data) 處理 , 結果再放入 mpg_sha_encrypt(mpg_aes_encrypt(data)) 處理
    • getTradeInfo - 得到的資料打包成 tradeInfo,包含上方提到的 MerchantID 、 TradInfo 放 mpg_aes_encrypt , TradInSha 放 mpg_sha_encrypt, Version 放版本。

    • 另外在多 payGeteWay,發送 API 的位置,還有 MerchantOrderNo,存到訂單做紀錄。

  3. 串接金流 Post /spgateway/callback

    • /spgateway/callback?from=NotifyURL,支付通知網址,電商端 req.query 會收到
    • spgatewayCallback: TradeInfo,交易完成後回傳的資料,作為電商核對確認付款
    • spgatewayCallback: create_mpg_aes_decrypt,使用解密函式,作為確認資料正確性,以及更新資料庫
    • spgatewayCallback: datacreate_mpg_aes_decrypt解密後
    • /spgateway/callback?from=ReturnURL,支付完成返還商店網址

實作

定義 route

router.get("/order/:id/payment", orderController.getPayment);
router.post("/spgateway/callback", orderController.spgatewayCallback);

定義參數

  • 臨時網址 ngrok 工具
const URL = ""; //本地 domain 不接受,使用 ngrok 工具做臨時網址,取得的網址放這
const MerchantID = ""; // 商店代號
const HashKey = ""; //API 金鑰
const HashIV = ""; //API 金鑰
const PayGateWay = "https://ccore.spgateway.com/MPG/mpg_gateway"; //付款網址
const ReturnURL = URL + "/spgateway/callback?from=ReturnURL"; //支付完成返還商店網址
const NotifyURL = URL + "/spgateway/callback?from=NotifyURL"; //支付通知網址
const ClientBackURL = URL + "/orders"; //支付取消返回網址

敏感資料安裝 dotenv 放在 .env

URL=
MERCHANT_ID=
HASH_KEY=
HASH_IV=

加密用 function,最後會將 getTradeInfo 放到 getPayment 。經由 getPayment 呼叫 getTradeInfo 傳入 AmtDescemail,進入 getTradeInfo 之後

  • genDataChain放在 create_mpg_aes_encrypt() 中幫忙 data 改為字串資料
  • data 作為參數放到 create_mpg_aes_encrypt(TradeInfo) 宣告成 mpg_aes_encrypt
  • mpg_aes_encrypt 放到 create_mpg_sha_encrypt(TradeInfo) 宣告成 mpg_sha_encrypt
  • 宣告 tradeInfo 打包 mpg_aes_encrypt 、 mpg_aes_encrypt 等資料回傳作為傳送交易參數

使用 getTradeInfo 時,我們會定義 data 其中會設定 MerchantOrderNo 並用 Date.now() 設定編號,未來在 打包 tradeInfo 也會設定 MerchantOrderNo 將資料從 data 傳入,在做交易的時候,取得回傳交易資料解碼後一樣會有 MerchantOrderNo,我們會當作 order 欄位 sn 搜尋依據,更改是否已經付款。

const crypto = require("crypto"); // 加密
const nodemailer = require("nodemailer"); // 寄送 mail

// 放入 create_mpg_aes_encrypt 將交易資訊轉成字串,以便加密使用
function genDataChain(TradeInfo) {
let results = [];
for (let kv of Object.entries(TradeInfo)) {
results.push(`${kv[0]}=${kv[1]}`);
}
return results.join("&");
}

function create_mpg_aes_encrypt(TradeInfo) {
let encrypt = crypto.createCipheriv("aes256", HashKey, HashIV);
let enc = encrypt.update(genDataChain(TradeInfo), "utf8", "hex");
return enc + encrypt.final("hex");
}

function create_mpg_sha_encrypt(TradeInfo) {
let sha = crypto.createHash("sha256");
let plainText = `HashKey=${HashKey}&${TradeInfo}&HashIV=${HashIV}`;

return sha
.update(plainText)
.digest("hex")
.toUpperCase();
}

// 交易完成後回傳資料使用的反向解密
function create_mpg_aes_decrypt(TradeInfo) {
let decrypt = crypto.createDecipheriv("aes256", HashKey, HashIV);
decrypt.setAutoPadding(false);
let text = decrypt.update(TradeInfo, "hex", "utf8");
let plainText = text + decrypt.final("utf8");
let result = plainText.replace(/[\x00-\x20]+/g, "");
return result;
}

function getTradeInfo(Amt, Desc, email) {
console.log("===== getTradeInfo =====");
console.log(Amt, Desc, email);
console.log("==========");

data = {
MerchantID: MerchantID, // 商店代號
RespondType: "JSON", // 回傳格式
TimeStamp: Date.now(), // 時間戳記
Version: 1.5, // 串接程式版本
MerchantOrderNo: Date.now(), // 商店訂單編號
LoginType: 0, // 智付通會員
OrderComment: "OrderComment", // 商店備註
Amt: Amt, // 訂單金額
ItemDesc: Desc, // 產品名稱
Email: email, // 付款人電子信箱
ReturnURL: ReturnURL, // 支付完成返回商店網址
NotifyURL: NotifyURL, // 支付通知網址/每期授權結果通知
ClientBackURL: ClientBackURL // 支付取消返回商店網址
};

console.log("===== getTradeInfo: data =====");
console.log(data);

mpg_aes_encrypt = create_mpg_aes_encrypt(data);
mpg_sha_encrypt = create_mpg_sha_encrypt(mpg_aes_encrypt);

console.log("===== getTradeInfo: mpg_aes_encrypt, mpg_sha_encrypt =====");
console.log(mpg_aes_encrypt);
console.log(mpg_sha_encrypt);

tradeInfo = {
MerchantID: MerchantID, // 商店代號
TradeInfo: mpg_aes_encrypt, // 加密後參數
TradeSha: mpg_sha_encrypt,
Version: 1.5, // 串接程式版本
PayGateWay: PayGateWay,
MerchantOrderNo: data.MerchantOrderNo
};

console.log("===== getTradeInfo: tradeInfo =====");
console.log(tradeInfo);

return tradeInfo;
}

GET /order/:id/payment

getPayment: (req, res) => {
console.log("===== getPayment =====");
console.log(req.params.id);
console.log("==========");
return Order.findByPk(req.params.id, {}).then(order => {
order.update({
...req.body,
sn: tradeInfo.MerchantOrderNo, //從購物餐車到取得付款頁面時,將 sn 更新,確立訂單成立。
}).then(order => {
res.render('payment', {order, tradeInfo})
})
})
},
};

Post /spgateway/callback

在 return 的結果中,req.body.TradeInfoResult,會有紀錄 TradeNo 編號,我們使用這個交易編號,作為我們在資料庫 order 中撈資料的依據

spgatewayCallback: (req, res) => {
console.log("===== spgatewayCallback =====");
console.log(req.method); // 總共四次,回傳前 post 3 次,確認電商網站是否正常。
console.log(req.query); // 回傳 { from: NotifyURL},第四次回傳 { from: ReturnURL}
console.log(req.body); // 回傳的 object 解碼使用
console.log("==========");

console.log("===== spgatewayCallback: TradeInfo =====");
console.log(req.body.TradeInfo);

const data = JSON.parse(create_mpg_aes_decrypt(req.body.TradeInfo))
console.log("===== spgatewayCallback: create_mpg_aes_decrypt、data =====");
console.log(data);

return Order.findAll({
where: { sn: data["Result"]["MerchantOrderNo"] }
}).then(orders => {
orders[0]
.update({
...req.body,
payment_status: 1 //在解密資料後修改付款狀態為真
})
.then(order => {
return res.redirect("/orders");
});
});
};