06 Python Flask 教學10 所有文章

【Flask 教學】Python 綠界金流 API 信用卡串接

ecpay_photo

詳細記錄了當時使用 Flask 串接綠界金流 API 的過程,目前 Python 串接綠界支付的文章不多,希望此篇對您有幫助!

  • 撰寫此篇時間為 2020-2-14,綠界 API 文件版本 V 5.1.38
  • 資料庫使用 flask_sqlalchemy (2.4.0) – ORM
  • 語言 Python (3.7.2) – flask (1.1.1)

此篇完整程式碼放置於 GitHub,歡迎 git clone 使用。

本篇參考的綠界支付官方文件如下:

大綱

本篇分成四個部分:

  1. 環境參數:將正式站和測試站的環境參數寫於此處
  2. 跳轉至綠界前:建立訂單後跳轉至 ECpay 頁面
  3. 綠界回傳:ReturnURL 綠界 Server 端回傳 (POST)
  4. 綠界回傳:OrderResultURL 綠界 Client 端 (POST)

第二部分對應到流程第 2 點,建立訂單
第三部分對應到流程第 13 點,背景接收付款結果 ReturnURL
第四部分對應到流程第 15 點,顯示結果畫面 OrderResultURL

第一部分:環境參數設定

方便用於正式和測試站的 Hashkey 參數轉換,以及檢查碼 [CheckMacValue] 的驗證計算

關於綠界檢查碼 [CheckMacValue] 的驗證方式:

  1. 將傳遞參數依照字母排序
  2. 參數最前面加上 HashKey、最後面加上 HashIV
  3. 進行 URL encode
  4. 轉為小寫
  5. 以 SHA256 加密方式來產生雜凑值
  6. 再轉大寫產生 CheckMacValue
# 環境參數
class Params:
    def __init__(self):
        web_type = ‘test’
        if web_type == ‘offical’:
            # 正式環境
            self.params = {
                'MerchantID': 'ID隱藏',
                'HashKey': 'Key 隱藏',
                'HashIV': 'IV 隱藏',
                'action_url':
                'https://payment.ecpay.com.tw/Cashier/AioCheckOut/V5'
            }
        else:
            # 測試環境
            self.params = {
                'MerchantID':
                '2000132',
                'HashKey':
                '5294y06JbISpM5x9',
                'HashIV':
                'v77hoKGq4kWxNNIS',
                'action_url':
                'https://payment-stage.ecpay.com.tw/Cashier/AioCheckOut/V5'
            }

    @classmethod
    def get_params(cls):
        return cls().params

    # 驗證綠界傳送的檢查碼 check_mac_value 值是否正確
    @classmethod
    def get_mac_value(cls, get_request_form):

        params = dict(get_request_form)
        if params.get(‘CheckMacValue’):
            params.pop(‘CheckMacValue’)

        ordered_params = collections.OrderedDict(
            sorted(params.items(), key=lambda k: k[0].lower()))

        HahKy = cls().params['HashKey']
        HashIV = cls().params['HashIV']

        encoding_lst = []
        encoding_lst.append('HashKey=%s&' % HahKy)
        encoding_lst.append(''.join([
            '{}={}&'.format(key, value)
            for key, value in ordered_params.items()
        ]))
        encoding_lst.append('HashIV=%s' % HashIV)

        safe_characters = '-_.!*()'

        encoding_str = ''.join(encoding_lst)
        encoding_str = quote_plus(str(encoding_str),
                                  safe=safe_characters).lower()

        check_mac_value = ''
        check_mac_value = hashlib.sha256(
            encoding_str.encode('utf-8')).hexdigest().upper()

        return check_mac_value

第二部分:跳轉至綠界前

  1. 先從 session 取得使用者id (uid),以及購物車頁面 POST 的收件人資訊
  2. 建立訂單資訊
  3. 將交易商品 & 金額寫進參數,並呼叫綠界 SDK,跳轉至綠界
# 建立訂單後跳轉至 ECpay 頁面
@payment.route(‘/to_ecpay’, methods=[‘POST’])
def ecpay():

    # 從 session 中取得 uid
    uid = session.get(‘uid’)
    host_name = request.host_url

    # 取得 POST 的收件人資訊
    trade_name = request.values[‘name’]
    trade_phone = request.values[‘phone’]
    county = request.values[‘county’]
    district = request.values[‘district’]
    zipcode = request.values[‘zipcode’]
    address = request.values[‘address’]

    # 利用 uid 查詢資料庫,購物車商品 & 價錢
    carts = sql.AddToCar.query.filter_by(uid=uid, state='Y')
    total_product_price = 0
    total_product_name = ''

    for cart in carts:
        price = cart.product.price
        quan = cart.quantity
        product_name = cart.product.name
        total_product_price += price * quan
        total_product_name += product_name + '#'

    # 建立交易編號 tid
    date = time.time()
    tid = str(date) + 'Uid' + str(uid)
    status = '未刷卡'

    # 新增 Transaction 訂單資料
    T = sql.Transaction(uid, tid, trade_name, trade_phone, address,
                        total_product_price, status, county, district, zipcode)
    db.session.add(T)
    db.session.commit()

    params = Params.get_params()

    # 設定傳送給綠界參數
    order_params = {
        'MerchantTradeNo': datetime.now().strftime("NO%Y%m%d%H%M%S"),
        'StoreID': '',
        'MerchantTradeDate': datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
        'PaymentType': 'aio',
        'TotalAmount': total_product_price,
        ‘TradeDesc’: ‘ToolsFactory’,
        ‘ItemName’: total_product_name,
        ‘ReturnURL’: host_name + ‘payment/receive_result’,
        ‘ChoosePayment’: ‘Credit’,
        ‘ClientBackURL’: host_name + ‘payment/trad_result’,
        ‘Remark’: ‘交易備註’,
        ‘ChooseSubPayment’: ‘’,
        ‘OrderResultURL’: host_name + ‘payment/trad_result’,
        ‘NeedExtraPaidInfo’: ‘Y’,
        ‘DeviceSource’: ‘’,
        ‘IgnorePayment’: ‘’,
        ‘PlatformID’: ‘’,
        ‘InvoiceMark’: ’N’,
        'CustomField1': str(tid),
        'CustomField2': '',
        'CustomField3': '',
        'CustomField4': '',
        'EncryptType': 1,
    }

    extend_params_1 = {
        'BindingCard': 0,
        'MerchantMemberID': '',
    }

    extend_params_2 = {
        'Redeem': 'N',
        'UnionPay': 0,
    }

    inv_params = {
        # 'RelateNumber': 'Tea0001', # 特店自訂編號
        # 'CustomerID': 'TEA_0000001', # 客戶編號
        # 'CustomerIdentifier': '53348111', # 統一編號
        # 'CustomerName': '客戶名稱',
        # 'CustomerAddr': '客戶地址',
        # 'CustomerPhone': '0912345678', # 客戶手機號碼
        # 'CustomerEmail': '[email protected]',
        # 'ClearanceMark': '2', # 通關方式
        # 'TaxType': '1', # 課稅類別
        # 'CarruerType': '', # 載具類別
        # 'CarruerNum': '', # 載具編號
        # 'Donation': '1', # 捐贈註記
        # 'LoveCode': '168001', # 捐贈碼
        # 'Print': '1',
        # 'InvoiceItemName': '測試商品1|測試商品2',
        # 'InvoiceItemCount': '2|3',
        # 'InvoiceItemWord': '個|包',
        # 'InvoiceItemPrice': '35|10',
        # 'InvoiceItemTaxType': '1|1',
        # 'InvoiceRemark': '測試商品1的說明|測試商品2的說明',
          # 'DelayDay': '0', # 延遲天數
        # 'InvType': '07', # 字軌類別
    }

    ecpay_payment_sdk = module.ECPayPaymentSdk(MerchantID=params['MerchantID'],
                                               HashKey=params['HashKey'],
                                               HashIV=params['HashIV'])

    # 合併延伸參數
    order_params.update(extend_params_1)
    order_params.update(extend_params_2)

    # 合併發票參數
    order_params.update(inv_params)

    try:
        # 產生綠界訂單所需參數
        final_order_params = ecpay_payment_sdk.create_order(order_params)

        # 產生 html 的 form 格式
        action_url = params['action_url']
        html = ecpay_payment_sdk.gen_html_post_form(action_url,final_order_params)
        return html

    except Exception as error:
        print(‘An exception happened: ‘ + str(error))

第三部分:綠界回傳交易資訊

ECpay 回傳結果有三種:

  1. ReturnURL : 綠界 Server端 POST 付款完成通知至指定網址
    • 因資安關係限制 443 port & 合法的 domain 才能正確接收成功
  2. OrderResultURL: 綠界 Client端 POST,回傳付款結果至指定網址
  3. ClientBackURL : 綠界 Client端 GET,使用者點選按鈕後返回連結
    • 如果有同時使用 OrderResultURL,使用者在付款成功後,會直接跳轉到 OrderResultURL 指定頁面 (POST)
    • 如果只有使用 ClientBackURL,使用者在付款成功後,會停留在綠界支付的交易成功頁面 ( 適用於方便 debug )

一. 設定 ReturnURL 接收頁面

  1. 限制於 443 port & 合法的 domain 才能正確接收成功,如果是測試 locahost 並沒有 https 的話,建議可以使用 ngrox 來獲得 https 進行測試
  2. 如有接收成功,官方要求回應 ‘1|OK’
# ReturnURL: 綠界 Server 端回傳 (POST)
@csrf.exempt
@payment.route(‘/receive_result’, methods=[‘POST’])
def end_return():
    result = request.form[‘RtnMsg’]
    tid = request.form[‘CustomField1’]
    trade_detail = sql.Transaction.query.filter_by(tid=tid).first()
    trade_detail.status = ‘交易成功 sever post’
    db.session.add(trade_detail)
    db.session.commit()

    return ‘1|OK’

二. 設定 OrderResultURL 接收頁面

  1. 先檢驗綠界傳送的 check_mac_value 是否為正確
  2. 判斷回傳資訊,交易成功則修改修資料庫並且跳轉至交易成功頁面
  3. 判斷回傳資訊,失敗則跳轉至失敗頁面
# OrderResultURL: 綠界 Client 端 (POST)
@csrf.exempt
@payment.route(‘/trad_result’, methods=[‘GET’, ‘POST’])
def end_page():

    if request.method == ‘GET’:
        return redirect(url_for(‘index’))

    if request.method == ‘POST’:
        check_mac_value = Params.get_mac_value(request.form)

        if request.form[‘CheckMacValue’] != check_mac_value:
            Return ‘請聯繫管理員’

        # 接收 ECpay 刷卡回傳資訊
        result = request.form[‘RtnMsg’]
        tid = request.form['CustomField1']
        trade_detail = sql.Transaction.query.filter_by(tid=tid).first()

        # 取得交易使用者資訊
        uid = trade_detail.uid

        trade_client_detail = {
            'name': trade_detail.trade_name,
            'phone': trade_detail.trade_phone,
            ‘county’: trade_detail.trade_county,
            ‘district’: trade_detail.trade_district,
            ‘zipcode’: trade_detail.trade_zipcode,
            ‘trade_address’: trade_detail.trade_address
        }

        # 判斷成功
        if result == ‘Succeeded’:
            trade_detail.status = ‘待處理’
            commit_list = []

            # 移除 AddToCar (狀態:Y 修改成 N)
            carts = sql.AddToCar.query.filter_by(uid=uid, state=‘Y’)
            for cart in carts:
                price = cart.product.price
                quan = cart.quantity
                cart.state = 'N'
                # 新增 Transaction_detail 訂單細項資料
                Td = sql.Transaction_detail(tid, cart.product.pid, quan, price)
                commit_list.append(Td)
                commit_list.append(cart)

            db.session.add_all(commit_list)
            db.session.commit()

            # 讀取訂單細項資料
            trade_detail_items = sql.Transaction_detail.query.filter_by(
                tid=tid)

            return render_template('/payment/trade_success.html',
                                   shopping_list=trade_detail_items,
                                   total=trade_detail.total_value)

        # 判斷失敗
        else:
            carts = sql.AddToCar.query.filter_by(uid=uid, state='Y')
            trade_detail = sql.Transaction.query.filter_by(tid=tid).first()

            return render_template('/payment/trade_fail.html',
                                   shopping_list=carts,
                                   total=trade_detail.total_value,
                                   trade_client_detail=trade_client_detail)

最後:

此篇完整程式碼放置於 GitHub,歡迎 git clone 使用。

關於更多 Python Flask 教學的延伸閱讀:

▍關於 Flask 教學系列目錄:

▍其他 Flask 相關教學:

有關Max行銷誌的最新文章,都會發佈在Max的Facebook粉絲專頁,如果想看最新更新,還請您按讚或是追蹤唷!

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *