python - 對接微信支付(PC)和 注意點

注:本文僅提供 pc 端微信掃碼支付(模式一)的示例代碼。php

  關於對接過程當中遇到的問題總結在本文最下方。前端

  參考: 官方文檔數據庫

       https://blog.csdn.net/lm_is_dc/article/details/83312706api

 

一。wxpay_settings.py (配置基本參數和建立訂單時必要的方法,如 隨機生成字符串,加密簽名,生成支付二維碼等)
安全

# encoding: utf-8

import random
import os
import time
import requests
import hashlib
from random import Random

import qrcode
from bs4 import BeautifulSoup

from appname import settings

APP_ID = ""  # 公衆帳號appid
MCH_ID = ""  # 商戶號
API_KEY = ""  # 微信商戶平臺(pay.weixin.qq.com) -->帳戶設置 -->API安全 -->密鑰設置,設置完成後把密鑰複製到這裏
UFDODER_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder"  # url是微信下單api
NOTIFY_URL = "http://xxx/get_pay/callback/"  # 微信支付結果回調接口,須要你自定義
CREATE_IP = '111.111.11.11'  # 你服務器上的ip


def random_str(randomlength=8):
    """
    生成隨機字符串
    :param randomlength: 字符串長度
    :return:
    """
    str = ''
    chars = 'AaBbCcDdEeFfGgHhIiJjKbkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789'
    length = len(chars) - 1
    random = Random()

    print "randomlength : ", randomlength

    for i in range(randomlength):
        str += chars[random.randint(0, length)]
    print "str : ", str
    return str


def order_num(phone):
    """
    生成掃碼付款訂單號
    :param phone: 手機號
    :return:
    """
    local_time = time.strftime('%Y%m%d%H%M%S', time.localtime(time.time()))
    result = phone + 'T' + local_time + random_str(5)
    return result


def get_sign(data_dict, key):
    # 簽名函數,參數爲簽名的數據和密鑰
    params_list = sorted(data_dict.items(), key=lambda e: e[0], reverse=False)  # 參數字典倒排序爲列表
    params_str = "&".join(u"{}={}".format(k, v) for k, v in params_list) + '&key=' + key
    # 組織參數字符串並在末尾添加商戶交易密鑰
    md5 = hashlib.md5()  # 使用MD5加密模式
    md5.update(params_str.encode('utf-8'))  # 將參數字符串傳入
    sign = md5.hexdigest().upper()  # 完成加密並轉爲大寫
    return sign


def trans_dict_to_xml(data_dict):  # 定義字典轉XML的函數
    data_xml = []
    for k in sorted(data_dict.keys()):  # 遍歷字典排序後的key
        v = data_dict.get(k)  # 取出字典中key對應的value
        if k == 'detail' and not v.startswith('<![CDATA['):  # 添加XML標記
            v = '<![CDATA[{}]]>'.format(v)
        data_xml.append('<{key}>{value}</{key}>'.format(key=k, value=v))
    return '<xml>{}</xml>'.format(''.join(data_xml)).encode('utf-8')  # 返回XML,並轉成utf-8,解決中文的問題


def trans_xml_to_dict(data_xml):
    soup = BeautifulSoup(data_xml, features='xml')
    xml = soup.find('xml')  # 解析XML
    if not xml:
        return {}
    data_dict = dict([(item.name, item.text) for item in xml.find_all()])
    return data_dict

def wx_pay_unifiedorde(detail):
    """
    訪問微信支付統一下單接口
    :param detail:
    :return:
    """
    detail['sign'] = get_sign(detail, API_KEY)
    xml = trans_dict_to_xml(detail)  # 轉換字典爲XML
    response = requests.request('post', UFDODER_URL, data=xml)  # 以POST方式向微信公衆平臺服務器發起請求
    return response.content


def pay_fail(err_msg):
    """
    微信支付失敗
    :param err_msg: 失敗緣由
    :return:
    """
    data_dict = {'return_msg': err_msg, 'return_code': 'FAIL'}
    return trans_dict_to_xml(data_dict)


def create_qrcode(username, url):
    """
    生成掃碼支付二維碼
    :param username: 用戶名
    :param url: 支付路由
    :return:
    """
    get_path = "%s/%s" % (settings.MEDIA_ROOT, "QRcode")    # 建立文件夾路徑
    img_path = "%s/%s" % (settings.MEDIA_ROOT, "QRcode/")   # 建立文件路徑

    img = qrcode.make(url)  # 建立支付二維碼片
    # 你存放二維碼的地址 uploads/media
    img_url = img_path + username + '.png'     # 建立文件

    if not os.path.exists(get_path):
        os.makedirs(get_path)
    img.save(img_url)

    ret_url = settings.MEDIA_URL + 'QRcode/'  + username + '.png'  # 前端展現路徑

    return ret_url

 

二。URL服務器

    url(r'^create_order/$', CreateOrderViews.as_view()),    # 建立訂單和生成二維碼
    url(r'^get_pay/$', Wxpay_QRccode.as_view()),    # 微信支付二維碼展現頁 wx
    url(r'^get_pay/callback/$', Wxpay_ModelOne_pay.as_view()),    # 支付回調接口 wx

 

三。Views微信

class CreateOrderViews(APIView):
    """
        建立訂單和支付二維碼
    """

    # 生成訂單(省略部分操做..)
    order_obj = models.Order.objects.create()

    # 調用微信支付 (下面部分參數的詳細說明請看第一步驟)
    paydict = {
        'appid': APP_ID,  
        'mch_id': MCH_ID,
        'nonce_str': random_str(), 
        'product_id': order_obj.id,  # 商品id,可自定義
        'time_stamp': int(time.time()), 
    }
    paydict['sign'] = get_sign(paydict, API_KEY)
    url = "weixin://wxpay/bizpayurl?appid=%s&mch_id=%s&nonce_str=%s&product_id=%s&time_stamp=%s&sign=%s" \
          % (paydict['appid'], paydict['mch_id'], paydict['nonce_str'], paydict['product_id'],
             paydict['time_stamp'], paydict['sign'])

    # 能夠直接在微信中點擊該url,若是有錯誤,微信會彈出提示框,若是是掃碼,若是失敗,什麼提示都沒有,不利於調試
    # 建立二維碼
    img_url = create_qrcode(user_obj.username, url) # 調用生成二維碼的方法(具體看第一步 wxpay_settings.py 的create_qrcode方法)
    order_obj.wx_pay_path = img_url # 將支付二維碼路徑存儲在訂單表中
    order_obj.save()    # 保存訂單
    pay_url = "/order/get_pay/?order_id=%s" % (order_obj.id)
    return JSONResponse({"pay_url": pay_url})


class Wxpay_QRccode(APIView):
    """
    返回二維碼圖片路徑接口
    """
    def get(self, request, *args, **kwargs):
        username = request.session.get('username')
        if username:
            order_id = request.GET.get("order_id")
            order_obj = Order.objects.filter(id=order_id, create_user__username=username,
                                             status=Order.STATUS_DEFAULT).first()

            return JSONResponse({"pay_img": order_obj.wx_pay_path}) # 從數據庫中獲取圖片路徑
        return JSONResponse({"error": "xxx"})


@method_decorator(csrf_exempt, name='dispatch')
class Wxpay_ModelOne_pay(APIView):
    """
    使用微信掃一掃掃描二維碼,微信系統會自動回調此路由,Post請求
    """
    def post(self, request, *args, **kwargs):
        """
        掃描二維碼後,微信系統回調的地址處理
        微信傳來的參數格式經trans_xml_to_dict()轉成字典
        {'openid': 'xxx',
         'is_subscribe': 'Y',
         'mch_id': 'xxx',
         'nonce_str': 'xxx',
         'sign': 'xxx',
         'product_id': 'xxx',
         'appid': 'xxx'}

        :param request:
        :param args:
        :param kwargs:
        :return:
        """
        try:
            data_dict = trans_xml_to_dict(request.body)  # 回調數據轉字典
            sign = data_dict.pop('sign')  # 取出簽名
            key = API_KEY  # 商戶交易密鑰
            back_sign = get_sign(data_dict, key)  # 計算簽名

            if data_dict.get('product_id'):     # 第一次微信掃碼請求,確認是否有該訂單(並確認訂單狀態等是否正確)
                order_obj = Order.objects.filter(id=data_dict.get('product_id'), status=Order.STATUS_DEFAULT).first()
                if not order_obj:
                    return HttpResponse(pay_fail('交易信息有誤,未找到該訂單'))

            elif data_dict.get('return_code') == "SUCCESS":     # 此處是支付成功後的回調,第二次請求(用於修改訂單狀態,保存openid等)
                order_obj = Order.objects.filter(vx_out_trade_no=data_dict.get('out_trade_no'),
                                                 status=Order.STATUS_DEFAULT).first()
                with transaction.atomic():  # 開啓事務操做
                    order_obj.status = Order.STATUS_PAYED
                    order_obj.pay_mode = Order.WEIXIN
                    order_obj.save()
                return HttpResponse("SUCCESS")  # 最終記得返回 SUCCESS,不然微信會重複訪問該接口,並返回支付結果。

            else:
                return HttpResponse(pay_fail('支付狀態返回錯誤!'))

            if sign == back_sign:  # 驗證簽名是否與回調簽名相同(第一次請求)
                params = {
                    'appid': APP_ID,  # APPID
                    'mch_id': MCH_ID,  # 商戶號
                    'nonce_str': random_str(16),  # 隨機字符串
                    'out_trade_no': order_num(data_dict.get('product_id')),  # 訂單編號
                    'total_fee': int(Decimal(order_obj.total_fee) * 100),  # 付款金額,單位是分,必須是整數
                    'spbill_create_ip': CREATE_IP,  # 發送請求服務器的IP地址
                    'body': 'xxx',  # 商品描述
                    'detail': 'xxx',  # 商品詳情
                    'notify_url': NOTIFY_URL,  # 微信支付結果回調接口
                    'trade_type': 'NATIVE',  # 掃碼支付類型
                }
                # 調用微信統一下單支付接口url
                notify_result = wx_pay_unifiedorde(params)

                if data_dict.get('product_id'):
                    # 保存微信訂單(用於後續修改訂單狀態)
                    order_obj = Order.objects.filter(id=data_dict.get('product_id'),
                                                     status=Order.STATUS_DEFAULT).first()
                    order_obj.vx_out_trade_no = params.get('out_trade_no')
                    order_obj.save()

                return HttpResponse(notify_result)
            return HttpResponse(pay_fail('交易信息有誤,請從新掃碼'))
        except Exception as e:
            return HttpResponse(pay_fail('程序異常'))

 

 

四。錯誤總結session

1. 開發中第一個錯誤就是掃碼時的服務器繁忙(好像是叫這個錯誤來着。),解決方案:去微信公衆號後臺設置服務器地址。app

2. 參數錯誤的話就請檢查下是否和微信公衆號裏面的匹配。微信公衆平臺

3. 參數回調問題:NOTIFY_URL 這個參數填異步回調地址(必須是服務器地址,微信公衆號後臺設置),

data_dict = trans_xml_to_dict(request.body) 這個方式是能夠拿到微信返回的全部回調參數,

其實這裏會被請求兩次,第一次是掃碼請求,這裏主要是給讓用戶看到支付前微信生成商品信息(商品價格等),
第二次是掃碼後的支付成功/失敗的回調請求,這裏主要就是對數據庫的操做了(訂單狀態,商品屬性等),
data_dict.get('return_code') == "SUCCESS" ,return_code參數是隻有回調纔會傳,因此根據這個判斷須要作什麼操做。

 

五。 本文可能有些錯誤的地方,歡迎指出,同時有什麼問題也歡迎提出。
相關文章
相關標籤/搜索