在進入到實作前,我們先來理解下關於的歐付寶(O'Pay)金流的運作原理。
假設我們在一個有付費服務的系統裡面導入金流,其流程大概會像是:
其動作:
在動作2
中,我們可以藉由發送個API至歐付寶那邊,假設在該API底下設定了ReturnURL
值及OrderResultURL
值,就會間接設定動作5
及動作7
所連接到的URL。
若我們有在動作2
中設定了ReturnURL
值,代表歐付寶那邊會對這個API進行POST
,並將些付款結果的值帶進去request中來讓我們Server收到,這時我們只需要再回傳一個成功收到的狀態1|OK
來做response即可。
同樣的,如果在動作2
中設定了OrderResultURL
值,代表歐付寶那邊也會對這個API進行POST
,並將些付款結果的值帶進去request中來讓我們Server收到,這時我們就會在response中回傳HTML
檔案至指定的URL,來讓使用者看到付款結果。
註記:
- ReturnURL值會對應到歐付寶的
付款結果通知
。- OrderResultURL值會在使用者在付款結束後,將使用者的瀏覽器畫面導向該URL所指定的頁面。
這部分的細節可參考:官方的金流介接文件
.
├── app.js
├── bin
│ └── www
├── controllers
│ ├── get_controller.js
│ └── modify_controller.js
├── package.json
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes
│ ├── payment.js
│ └── users.js
├── views
├── payment_action.ejs
├── payment_result.ejs
└── payment.ejs
├── .env
└── .gitignore
先下載歐付寶所提供的SDK,它能用來提供
並將它的opay_payment_nodejs
資料夾放置node_modules
的目錄中。
再來,我們先去觀察該SDK
的專案中conf
資料夾的payment_conf.xml
檔案。它看起來會長這樣:
<?xml version="1.0" encoding="utf-8"?>
<Conf>
<OperatingMode>Test</OperatingMode> <!--Test or Production-->
<MercProfile>Stage_Account</MercProfile>
<IsProjectContractor>N</IsProjectContractor>
<MerchantInfo>
<MInfo name="Production_Account">
<MerchantID></MerchantID>
<HashKey></HashKey>
<HashIV></HashIV>
</MInfo>
<MInfo name="Stage_Account">
<MerchantID>2000132</MerchantID>
<HashKey>5294y06JbISpM5x9</HashKey>
<HashIV>v77hoKGq4kWxNNIS</HashIV>
</MInfo>
</MerchantInfo>
<IgnorePayment>
<!--<Method>Credit</Method>-->
<!--<Method>WebATM</Method>-->
<!--<Method>ATM</Method>-->
<!--<Method>CVS</Method>-->
<!--<Method>Tenpay</Method>-->
<!--<Method>TopUpUsed</Method>-->
</IgnorePayment>
</Conf>
註記:這部分僅說明與測試環境有關的參數。
- OperatingMode: 指目前的情況該SDK是屬於測試環境還是上線環境,會隨著選擇的不同而轉往相對應的歐付寶API。
- MercProfile: 隨著
MerchantInfo
而改變,測試環境就是Stage_Account
ˊ而上線環境則是Production_Account
。- MerchantInfo: 為
MercProfile
對應到的部分,其測試環境的參數它也幫我們輸入在內。這部分官方詳細的文件在這。
我們選擇使用信用卡付款
且不做發票的方式來實作,而這部分在官方的git中,有釋出範例可參考。如果讀者有仔細看,可以發現到這個範例到最後會產生一個HTML擋,也就是會間接產生出歐付寶官方的付款畫面的HTML。
/**
* Created by ying.wu on 2017/6/27.
*/
const opay_payment = require('../lib/opay_payment.js');
//參數值為[PLEASE MODIFY]者,請在每次測試時給予獨特值
//若要測試非必帶參數請將base_param內註解的參數依需求取消註解 //
let base_param = {
MerchantTradeNo: 'PLEASE MODIFY', //請帶20碼uid, ex: f0a0d7e9fae1bb72bc93
MerchantTradeDate: 'PLEASE MODIFY', //ex: 2017/02/13 15:45:30
TotalAmount: '100',
TradeDesc: '測試交易描述',
ItemName: '測試商品等',
ReturnURL: 'http://192.168.0.1',
// ChooseSubPayment: '',
// OrderResultURL: 'http://192.168.0.1/payment_result',
// NeedExtraPaidInfo: '1',
// ClientBackURL: 'https://www.google.com',
// ItemURL: 'http://item.test.tw',
// Remark: '交易備註',
// HoldTradeAMT: '1',
// StoreID: '',
// UseRedeem: ''
};
let create = new opay_payment();
let htm = create.payment_client.aio_check_out_credit_onetime(parameters = base_param);
console.log(htm);
由上面所給的範例得知,帳單ID的生成會是20碼
。我們可以在用戶點選完產品的時候,再轉到結帳頁面時偷偷塞入帳單ID。至controllers
資料夾的get_controller.js
中寫入:
module.exports = class GetPayment {
payUid(req, res) {
let uid = randomValue(10, 99) + "1234567890234567" + randomValue(10, 99);
res.render('payment', { uid: uid });
}
}
並在該檔案底下增加randomValue
的生成function:
const randomValue = function (min, max) {
return Math.round(Math.random() * (max - min) + min);
}
註記:這部分的生成規則每個店家或是工程師都有自己的一套邏輯,而筆者展示的生成規則並不嚴謹,讀者參考就好。
我們來修改該文件,變成賣企鵝玩偶
。至controllers
資料夾的get_controller.js
中寫入:
//串連至歐付寶的金流服務(使用歐付寶的SDK)
payAction(req, res) {
let uid = req.query.uid;
let base_param = {
MerchantTradeNo: uid, //請帶20碼uid, ex: f0a0d7e9fae1bb72bc93
MerchantTradeDate: onTimeValue(), //ex: 2017/02/13 15:45:30
TotalAmount: '100',
TradeDesc: '企鵝玩偶 一隻',
ItemName: '企鵝玩偶 300元 X 1',
ReturnURL: '', // 付款結果通知URL
OrderResultURL: '', // 在使用者在付款結束後,將使用者的瀏覽器畫面導向該URL所指定的URL
EncryptType: 1,
// ItemURL: 'http://item.test.tw',
Remark: '該服務繳費成立時,恕不接受退款。',
// HoldTradeAMT: '1',
// StoreID: '',
// UseRedeem: ''
};
let create = new opay();
let parameters = {};
let invoice = {};
try {
let htm = create.payment_client.aio_check_out_credit_onetime(parameters = base_param);
res.render('payment_action', {
result: htm
})
} catch (err) {
// console.log(err);
let error = {
status: '500',
stack: ""
}
res.render('error', {
message: err,
error: error
})
}
}
}
並在該檔案底下增加onTimeValue
的生成function:
//example: 2017/08/09 20:34:02
const onTimeValue = function () {
var date = new Date();
var mm = date.getMonth() + 1;
var dd = date.getDate();
var hh = date.getHours();
var mi = date.getMinutes();
var ss = date.getSeconds();
return [date.getFullYear(), "/" +
(mm > 9 ? '' : '0') + mm, "/" +
(dd > 9 ? '' : '0') + dd, " " +
(hh > 9 ? '' : '0') + hh, ":" +
(mi > 9 ? '' : '0') + mi, ":" +
(ss > 9 ? '' : '0') + ss
].join('');
};
註記:若讀者想要跟著實作,那麼
ReturnURL
及OrderResultURL
需輸入實際網域的URL,若我們在這邊輸入localhost那麼整個運作是會卡住的。試想假設我們輸入localhost,每當歐付寶來呼叫這URL時,不就都會連到歐付寶自己的網址嗎?
再來,寫入ReturnURL
及OrderResultURL
會對應到的內容。至controllers
資料夾的modify_controller.js
中寫入:
module.exports = class ModifyPayment {
//銜接歐付寶的Return_URL回來的資料,並確定付款是否成功,若成功則進行改變用戶資料動作。
paymentResult(req, res) {
var rtnCode = req.body.RtnCode;
var simulatePaid = req.body.SimulatePaid;
var merchantID = req.body.MerchantID;
var merchantTradeNo = req.body.MerchantTradeNo;
var storeID = req.body.StoreID;
var rtnMsg = req.body.RtnMsg;
// var tradeNo = req.body.TradeNo;
var tradeAmt = req.body.TradeAmt;
// var payAmt = req.body.PayAmt;
var paymentDate = req.body.PaymentDate;
var paymentType = req.body.PaymentType;
// var paymentTypeChargeFee = req.body.PaymentTypeChargeFee;
let paymentInfo = {
merchantID: merchantID,
merchantTradeNo: merchantTradeNo,
storeID: storeID,
rtnMsg: rtnMsg,
paymentDate: paymentDate,
paymentType: paymentType,
tradeAmt: tradeAmt
}
//(添加simulatePaid模擬付款的判斷 1為模擬付款 0 為正式付款)
//測試環境
if (rtnCode === "1" && simulatePaid === "1") {
// 這部分可與資料庫做互動
res.write("1|OK");
res.end();
}
//正式環境
// else if (rtnCode === "1" && simulatePaid === "0") {
// 這部分可與資料庫做互動
// }
else {
res.write("0|err");
res.end();
}
}
//銜接歐付寶的OrderResultURL
paymentActionResult(req, res) {
var merchantID = req.body.MerchantID; //會員編號
var merchantTradeNo = req.body.MerchantTradeNo; //交易編號
var storeID = req.body.StoreID; //商店編號
var rtnMsg = req.body.RtnMsg; //交易訊息
var paymentDate = req.body.PaymentDate; //付款時間
var paymentType = req.body.PaymentType; //付款方式
var tradeAmt = req.body.TradeAmt; //交易金額
let result = {
member: {
merchantID: merchantID,
merchantTradeNo: merchantTradeNo,
storeID: storeID,
rtnMsg: rtnMsg,
paymentDate: paymentDate,
paymentType: paymentType,
tradeAmt: tradeAmt
}
}
// console.log("result: " + JSON.stringify(result));
res.render(
'payment_result', {
result: result
}
)
}
}
至routes
資料夾的payment.js
檔案中寫入:
var express = require('express');
var router = express.Router();
const GetPayment = require('../controllers/payment/get_controller');
const ModifyPayment = require('../controllers/payment/modify_controller');
getPayment = new GetPayment();
modifyPayment = new ModifyPayment();
// 用戶進入付款頁面所呼叫的API
router.get('/payment', getPayment.payUid);
// 用戶在付款頁面按下結帳的API
router.get('/paymentaction', getPayment.payAction);
// 銜接歐付寶的Return_URL回來的資料
router.post('/payment', modifyPayment.paymentResult);
// 銜接歐付寶的OrderResultURL
router.post('/paymentactionresult', modifyPayment.paymentActionResult);
module.exports = router;
由於這部分的程式並沒有辦法進行測試,因為ReturnURL
及OrderResultURL
需輸入實際網域的URL。所以這部分就直接拿筆者先前已經做好的練習畫面來展示。
首先,我們到payment.ejs的畫面。
在點選結帳
按鈕後,會進入到歐付寶的付款系統:
這時,也可以看到我們的商品有顯示在它們系統上面。接著我們使用歐付寶所提供的測試付款時需要用到測試帳號密碼來登入:
帳號: stageuser001
密碼: test1234
之後,輸入在歐付寶串接教學上提供的測試用信用卡卡號。
測試用信用卡卡號:4311-9522-2222-2222
卡片有效期限:(只要大於現在時間即可)
安全碼:222
之後按下一步,會跳出個確認視窗:
當按下確認交易後,就會跳到我們當初在信用卡付款
那API所設定OrderResultURL
中。
註記:這部分若沒使用歐付寶所提供的測試用信用卡卡號,那麼這邊的交易狀態會是
失敗
的情況。
各家金流的運用其實絕大多數都大同小異,與第三方登入相同,當我們嘗試要做嫁接時,還是得花點心思去研究下對方的API文件。同時,還得考量到要怎麼跟我們的系統做一個合併使用,其流程設計如何,符不符合需求...等。
還有在上述有提到訂單編號生成的規則,由於訂單編號不用想一定會是唯一值,但要怎麼讓這個唯一值不重複,也考驗著工程師的能耐。
接續,我們將進入到最後的進階實作「爬蟲
」。