藍新金流串接實作

前言

練習藍新金流,記錄實作步驟與需要的參數,參考 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)。

教材顯示 MerchantID、 TradeInfo、 TradeSha、 Version,官方是以字串呈現,以及教材導向網址 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: data,create_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 傳入 Amt、 Desc、 email,進入 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.TradeInfo 的 Result,會有紀錄 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");
});
});
};