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


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

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

  參考: 官方文檔

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

 

一。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('程序異常'))

 

 

四。錯誤總結

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

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

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

data_dict = trans_xml_to_dict(request.body) 這個方式是可以拿到微信返回的所有回調參數,

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

 

五。 本文可能有些錯誤的地方,歡迎指出,同時有什么問題也歡迎提出。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM