iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 29
0
Modern Web

Node JS-Back end見聞錄系列 第 29

Node.js-Backend見聞錄(28):進階實作-關於金流

Node.js-Backend見聞錄(28):進階實作-關於金流-以歐付寶為例

前言

在進入到實作前,我們先來理解下關於的歐付寶(O'Pay)金流的運作原理。

運行原理

假設我們在一個有付費服務的系統裡面導入金流,其流程大概會像是:

其動作:

  1. Client端將訂單資料傳送給Server。
  2. Server端將訂單資料傳送給O'Pay。
  3. Client端輸入進行付款動作。
  4. O'Pay跟Bank做交易的授權。
  5. O'Pay將付款結果送給Server
  6. 成功收到款項時,與DB資料庫做互動。
  7. 將付款結果傳送給Server端看。

動作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

開始實作

Opay Node.js SDK

先下載歐付寶所提供的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

由上面所給的範例得知,帳單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('');
};

註記:若讀者想要跟著實作,那麼ReturnURLOrderResultURL需輸入實際網域的URL,若我們在這邊輸入localhost那麼整個運作是會卡住的。試想假設我們輸入localhost,每當歐付寶來呼叫這URL時,不就都會連到歐付寶自己的網址嗎?

ReturnURL & OrderResultURL

再來,寫入ReturnURLOrderResultURL會對應到的內容。至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
            }
        )
    }
}

rotues

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;

測試

由於這部分的程式並沒有辦法進行測試,因為ReturnURLOrderResultURL需輸入實際網域的URL。所以這部分就直接拿筆者先前已經做好的練習畫面來展示。

首先,我們到payment.ejs的畫面。

在點選結帳按鈕後,會進入到歐付寶的付款系統:

這時,也可以看到我們的商品有顯示在它們系統上面。接著我們使用歐付寶所提供的測試付款時需要用到測試帳號密碼來登入:

帳號: stageuser001
密碼: test1234

之後,輸入在歐付寶串接教學上提供的測試用信用卡卡號。

測試用信用卡卡號:4311-9522-2222-2222
卡片有效期限:(只要大於現在時間即可)
安全碼:222

之後按下一步,會跳出個確認視窗:

當按下確認交易後,就會跳到我們當初在信用卡付款那API所設定OrderResultURL中。

註記:這部分若沒使用歐付寶所提供的測試用信用卡卡號,那麼這邊的交易狀態會是失敗的情況。

小結

各家金流的運用其實絕大多數都大同小異,與第三方登入相同,當我們嘗試要做嫁接時,還是得花點心思去研究下對方的API文件。同時,還得考量到要怎麼跟我們的系統做一個合併使用,其流程設計如何,符不符合需求...等。

還有在上述有提到訂單編號生成的規則,由於訂單編號不用想一定會是唯一值,但要怎麼讓這個唯一值不重複,也考驗著工程師的能耐。

接續,我們將進入到最後的進階實作「爬蟲」。

參考資料

歐付寶金流介接文件

歐付寶串接教學

歐付寶金流-Node.js SDK


上一篇
Node.js-Backend見聞錄(27):進階實作-關於第三方登入(二)
下一篇
Node.js-Backend見聞錄(29):進階實作-關於爬蟲-以7–11店家資料為例
系列文
Node JS-Back end見聞錄31

尚未有邦友留言

立即登入留言