首頁 > 軟體

Python3 微信支付(小程式支付)V3介面的實現

2023-01-18 14:01:20

起因:

因公司專案需要網上充值功能,從而對接微信支付,目前也只對接了微信支付的小程式支付功能,在網上找到的都是對接微信支付V2版本介面,與我所對接的介面版本不一致,無法使用,特此記錄下微信支付完成功能,使用Django完成後端功能,此文章用於記錄使用,

以下程式碼僅供參考,如若直接商用出現任何後果請自行承擔,本人概不負責。

功能:

調起微信支付,微信回撥

程式碼:

1、準備工作:

mchid = "xxxxxx"                         # 商戶號
pay_key = "xxxxxx"                       # 商戶祕鑰V3 使用V3介面必須使用V3祕鑰
serial_num = "xxxxxx"                    # 證書序列號
 
# ======================前三個引數在微信支付中可找到===============================
# ============ 商戶號(mchid ) 在賬戶中心——商戶資訊——微信支付商戶號 (是純數位) ==================
# ============= 商戶祕鑰(pay_key) 在賬戶中心——API安全——APIv3祕鑰 (需手動設定) ===================
# ============= 證書序列號(serial_num) 在賬戶中心——API安全——API證書 (需手動申請,通過後會有串證書序列號),申請完成後需要把證書下載到專案中,便於使用 ===================
 
 
 
appid = "xxxxxx"                         # 微信小程式appid 
wx_secret ="xxxxxx"                      # 微信小程式祕鑰
# ============= 微信小程式appid 在產品中心——AppID賬號管理——新增關聯的AppID  ===================     
 
WX_Pay_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"
# ============= 微信支付呼叫地址,用於請求接收 預支付交易對談標識: prepay_id =================== 
 
 
WX_Notify_URL = "https://127.0.0.1:8000"  
# ============= 接收微信支付回撥地址,必須是https ===================         

2、調起微信支付(後端只能請求微信支付介面向微信支付官方獲取到預支付交易對談標識,並返回給前端,前端才能調起輸入密碼支付介面)

import json
import decimal
import traceback
 
import requests
from django.http import HttpResponse
 
 
def payment_view(request, *args, **kwargs):
    """
    微信支付(小程式)
    :param request:
    :param args:
    :param kwargs:
    :return:
    """
    try:
        reqdata = json.loads(request.body)
        # 前端引數
        jscode = reqdata["jscode"]  # 微信ID
        price = decimal.Decimal(reqdata["price"]).quantize(decimal.Decimal("0.00"))  # 充值金額,保留兩位小數
        nickname = reqdata["nickname"]  # 微信暱稱/支付寶名稱 前端獲取到返給後端做記錄,可要可不要的欄位
        paymode = reqdata["paymode"]  # 支付方式  1微信支付
        remark = reqdata["remark"]  # 支付內容描述
        
        # 根據jscode 獲取openID
        rets = requests.get(url = "https://api.weixin.qq.com/sns/jscode2session?" 
              "appid=%s&secret=%s&js_code=%s" 
              "&grant_type=authorization_code" % (appid,wx_secret, js_code), timeout=3, verify=False)
        if not rets:
            return HttpResponse(general_error_msg(msg="未獲取到微信資訊"))
 
        # 0.獲取支付的微信openid
        print(f"組織ID:{userinfo['orgid']}, jscode:{jscode}")
        wxuser = getappopenid(orgid, jscode)
        if wxuser:
            # session_key = wxuser["session_key"]
            openid = wxuser["openid"]
        else:
            return HttpResponse(general_error_msg(msg="未獲取到微信使用者資訊"))
 
        # 1.以交易日期生成交易號
        orderno = order_num()
        # 2.生成新交易記錄 paystatus 支付狀態  1成功 0待支付 -1支付失敗
        conorder.objects.create(orderno=orderno, openid=openid, openname=nickname,
                                paymode=paymode,goodstotalprice=price, paystatus=0, 
                                remark=remark,createtime=get_now_time(1))
        # 3.生成統一下單的報文body
        url = WX_Pay_URL
        body = {
            "appid": appid,
            "mchid": mchid,
            "description": remark,
            "out_trade_no": orderno,
            "notify_url": WX_Notify_URL + "/pay/notify",  # 後端接收回撥通知的介面
            "amount": {"total": int(price * 100), "currency": "CNY"},  # 正式上線price要*100,微信金額單位為分(必須整型)。
            "payer": {"openid": openid},
        }
        data = json.dumps(body)
 
        headers, random_str, time_stamps = make_headers_v3(mchid, serial_num, data=data, method='POST')
 
        # 10.傳送請求獲得prepay_id
        try:
            response = requests.post(url, data=data, headers=headers)  # 獲取預支付交易對談標識(prepay_id)
            print("預支付交易對談標識", response)
            if response.status_code == 200:
                wechatpay_serial, wechatpay_timestamp, wechatpay_nonce, wechatpay_signature, certificate, serial_no = check_wx_cert(
                    response, mchid, pay_key, serial_num)
                # 11.9簽名驗證
                if wechatpay_serial == serial_no:  # 應答簽名中的序列號同證書序列號應相同
                    print('serial_no match')
                    try:
                        data3 = f"{wechatpay_timestamp}n{wechatpay_nonce}n{response.text}n"
                        verify(data3, wechatpay_signature, certificate)
                        print('The signature is valid.')
                        # 12.生成調起支付API需要的引數並返回前端
                        res = {
                            'orderno': orderno,  # 訂單號
                            'timeStamp': time_stamps,
                            'nonceStr': random_str,
                            'package': 'prepay_id=' + response.json()['prepay_id'],
                            'signType': "RSA",
                            'paySign': get_sign(f"{appid}n{time_stamps}n{random_str}n{'prepay_id=' + response.json()['prepay_id']}n"),
                        }
                        return HttpResponse(success_msg(msg="下單成功", total=0, data=res))
                    except Exception as e:
                        log.error(f"證書序列號驗籤失敗{e}, {traceback.format_exc()}")
                        return HttpResponse(general_error_msg(msg="下單失敗"))
                else:
                    log.error(f"證書序列號比對失敗【請求頭中證書序列號:{wechatpay_serial};本地儲存證書序列號:{serial_no};】")
                    return HttpResponse(general_error_msg(msg="調起微信支付失敗!"))
            else:
                log.error(f"獲取預支付交易對談標識 介面報錯【params:{data};headers:{headers};response:{response.text}】")
                return HttpResponse(general_error_msg(msg="調起微信支付失敗!"))
        except Exception as e:
            log.error(f"呼叫微信支付介面超時【params:{data};headers:{headers};】:{e},{traceback.format_exc()}")
            return HttpResponse(general_error_msg(msg="微信支付超時!"))
    except Exception as e:
        log.error(f"微信支付介面報錯:{e},{traceback.format_exc()}")
        return HttpResponse(general_error_msg(msg="微信支付介面報錯!"))

3、相關方法

import base64
import random
import string
import time
import traceback
from datetime import datetime
 
import requests
from BaseMethods.log import log
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Cryptodome.Hash import SHA256
from sqlalchemy.util import b64encode
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
 
# 各包版本
# django-ratelimit==3.0.1
# SQLAlchemy~=1.4.44
# pycryptodome==3.16.0
# pycryptodomex==3.16.0
# cryptography~=38.0.4
# Django~=3.2.4
 
# 獲取唯一標識
def get_uuid(utype=0):
    """
    唯一碼
    :param utype:
    :return:
    """
    if utype == 0:
        return uuid.uuid1()
    elif utype == 1:
        return str(uuid.uuid1())
    elif utype == 2:
        return str(uuid.uuid1().hex)
    elif utype == 3:
        return str((uuid.uuid5(uuid.NAMESPACE_DNS, str(uuid.uuid1()) + str(random.random()))))
 
 
# 獲取當前時間
def get_now_time(type=0):
    """
    :param type: 型別0-5
    :return: yyyy-mm-dd HH:MM:SS;y-m-d H:M:S.f;y-m-d;ymdHMS;y年m月d日h時M分S秒
    """
    if type == 0:
        return datetime.datetime.now()
    elif type == 1:
        return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    elif type == 2:
        return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
    elif type == 3:
        return datetime.datetime.now().strftime("%Y-%m-%d")
    elif type == 4:
        return datetime.datetime.now().strftime("%Y%m%d%H%M%S")
    elif type == 5:
        locale.setlocale(locale.LC_CTYPE, 'chinese')
        timestr = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        t = time.strptime(timestr, "%Y-%m-%d %H:%M:%S")
        result = (time.strftime("%Y年%m月%d日%H時%M分%S秒", t))
        return result
    elif type == 6:
        return datetime.datetime.now().strftime("%Y%m%d")
 
 
# 重構系統jargon類,用於處理時間格式報錯問題
class DateEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return obj.strftime('%Y-%m-%d %H:%M:%S')
        elif isinstance(obj, datetime.date):
            return obj.strftime("%Y-%m-%d")
        elif isinstance(obj, Decimal):
            return float(obj)
        elif isinstance(obj, bytes):
            return str(obj, encoding='utf-8')
        elif isinstance(obj, uuid.UUID):
            return str(obj)
        elif isinstance(obj, datetime.time):
            return obj.strftime('%H:%M')
        elif isinstance(obj, datetime.timedelta):
            return str(obj)
        else:
            return json.JSONEncoder.default(self, obj)
 
 
 
 
def decrypt(nonce, ciphertext, associated_data, pay_key):
    """
    AES解密
    :param nonce:
    :param ciphertext:
    :param associated_data:
    :param pay_key:
    :return:
    """
    key = pay_key
    key_bytes = str.encode(key)
    nonce_bytes = str.encode(nonce)
    ad_bytes = str.encode(associated_data)
    data = base64.b64decode(ciphertext)
    aesgcm = AESGCM(key_bytes)
    return aesgcm.decrypt(nonce_bytes, data, ad_bytes)
 
 
def order_num():
    """
    生成訂單號
    :return:
    """
    # 下單時間的年月日毫秒12+亂數8位元
    now_time = datetime.now()
    result = str(now_time.year) + str(now_time.month) + str(now_time.day) + str(now_time.microsecond) + str(
        random.randrange(10000000, 99999999))
    return result
 
 
def get_sign(sign_str):
    """
    定義生成簽名的函數
    :param sign_str:
    :return:
    """
    try:
        with open(r'static/cret/apiclient_key.pem') as f:
            private_key = f.read()
        rsa_key = RSA.importKey(private_key)
        signer = pkcs1_15.new(rsa_key)
        digest = SHA256.new(sign_str.encode('utf-8'))
        # sign = b64encode(signer.sign(digest)).decode('utf-8')
        sign = b64encode(signer.sign(digest))
        return sign
    except Exception as e:
        log.error("生成簽名的函數方法報錯【func:get_sign;sign_str:%s】:%s ==> %s" % (sign_str, e, traceback.format_exc()))
 
 
def check_wx_cert(response, mchid, pay_key, serial_no):
    """
    微信平臺證書
    :param response: 請求微信支付平臺所對應的的介面返回的響應值
    :param mchid: 商戶號
    :param pay_key: 商戶號祕鑰
    :param serial_no: 證書序列號
    :return:
    """
    wechatpay_serial, wechatpay_timestamp, wechatpay_nonce, wechatpay_signature, certificate = None, None, None, None, None
    try:
        # 11.應答簽名驗證
        wechatpay_serial = response.headers['Wechatpay-Serial']  # 獲取HTTP頭部中包括回撥報文的證書序列號
        wechatpay_signature = response.headers['Wechatpay-Signature']  # 獲取HTTP頭部中包括回撥報文的簽名
        wechatpay_timestamp = response.headers['Wechatpay-Timestamp']  # 獲取HTTP頭部中包括回撥報文的時間戳
        wechatpay_nonce = response.headers['Wechatpay-Nonce']  # 獲取HTTP頭部中包括回撥報文的隨機串
        # 11.1.獲取微信平臺證書 (等於又把前面的跑一遍,實際上應是獲得一次證書就存起來,不用每次都重新獲取一次)
        url2 = "https://api.mch.weixin.qq.com/v3/certificates"
        # 11.2.生成證書請求隨機串
        random_str2 = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
        # 11.3.生成證書請求時間戳
        time_stamps2 = str(int(time.time()))
        # 11.4.生成請求證書的簽名串
        data2 = ""
        sign_str2 = f"GETn{'/v3/certificates'}n{time_stamps2}n{random_str2}n{data2}n"
        # 11.5.生成簽名
        sign2 = get_sign(sign_str2)
        # 11.6.生成HTTP請求頭
        headers2 = {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "Authorization": 'WECHATPAY2-SHA256-RSA2048 '
                             + f'mchid="{mchid}",nonce_str="{random_str2}",signature="{sign2}",timestamp="{time_stamps2}",serial_no="{serial_no}"'
        }
        # 11.7.傳送請求獲得證書
        response2 = requests.get(url2, headers=headers2)  # 只需要請求頭
        cert = response2.json()
 
        # 11.8.證書解密
        nonce = cert["data"][0]['encrypt_certificate']['nonce']
        ciphertext = cert["data"][0]['encrypt_certificate']['ciphertext']
        associated_data = cert["data"][0]['encrypt_certificate']['associated_data']
        serial_no = cert["data"][0]['serial_no']
        certificate = decrypt(nonce, ciphertext, associated_data, pay_key)
    except Exception as e:
        log.error(f"微信平臺證書驗證報錯:{e};{traceback.format_exc()}")
    return wechatpay_serial, wechatpay_timestamp, wechatpay_nonce, wechatpay_signature, certificate, serial_no
 
 
def verify(check_data, signature, certificate):
    """
    驗籤函數
    :param check_data:
    :param signature:
    :param certificate:
    :return:
    """
    key = RSA.importKey(certificate)  # 這裡直接用瞭解密後的證書,但沒有去匯出公鑰,似乎也是可以的。怎麼導公鑰還沒搞懂。
    verifier = pkcs1_15.new(key)
    hash_obj = SHA256.new(check_data.encode('utf8'))
    return verifier.verify(hash_obj, base64.b64decode(signature))
 
 
def make_headers_v3(mchid, serial_num, data='', method='GET'):
    """
    定義微信支付請求介面中請求頭認證
    :param mchid: 商戶ID
    :param serial_num: 證書序列號
    :param data: 請求體內容
    :param method: 請求方法
    :return: headers(請求頭)
    """
    # 4.定義生成簽名的函數 get_sign(sign_str)
    # 5.生成請求隨機串
    random_str = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))
    # 6.生成請求時間戳
    time_stamps = str(int(time.time()))
    # 7.生成簽名串
    sign_str = f"{method}n{'/v3/pay/transactions/jsapi'}n{time_stamps}n{random_str}n{data}n"
    # 8.生成簽名
    sign = get_sign(sign_str)
    # 9.生成HTTP請求頭
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'WECHATPAY2-SHA256-RSA2048 '
                         + f'mchid="{mchid}",nonce_str="{random_str}",signature="{sign}",timestamp="{time_stamps}",serial_no="{serial_num}"'
    }
    return headers, random_str, time_stamps

4、微信回撥

import decimal
import json
import traceback
 
from django.http import HttpResponse
 
 
def notify_view(request, *args, **kwargs):
    """
    支付完成之後的通知(微信官方返回的資料)
    :param request:
    :param args:
    :param kwargs:
    :return:
    """
    try:
        # 1.獲得支付通知的引數
        body = request.body
        data = bytes.decode(body, 'utf-8')
        newdata = json.loads(data)
        # newdata = {
        #     "id": "9d40acfd-13cb-5175-a5aa-6c421f794952",
        #     "create_time": "2023-01-06T15:12:49+08:00",
        #     "resource_type": "encrypt-resource",
        #     "event_type": "TRANSACTION.SUCCESS",
        #     "summary": "xe6x94xafxe4xbbx98xe6x88x90xe5x8ax9f",
        #     "resource": {
        #         "original_type":
        #         "transaction",
        #         "algorithm": "AEAD_AES_256_GCM",
        #         "ciphertext": "UF5gLXfe8qBv9qxQsf+/Mb6as+vbIhUS8Dm25qGIJIIdXTorUUjqZH1+"
        #                       "jMQxkxma/Gn9bOxeAoQWPEuIoJ2pB328Iv90jmHTrouoP3L60mjNgGJS8d3H8i1zAPBXCpP4mgvgRANWsw4pAWj1lFM5BZr4aP+"
        #                       "pNMc5TdwreGBG3rO9sbCLXsSRfW8pVZ7IfPnhPDTOWP3P1k5ikHedcRt4/HP69oDBEe5RSsD93wO/"
        #                       "lrIwycStVHyecBaliwpVMRnNnRCXqhlalNJ3NJ6jcgy32fP1J+L90ntwGyqMmZUS71P5TN1H0iH5rXNpRY9IF3pvN+"
        #                       "lei5IS86wEoVXkmEsPcJrHaabn7rghxuZoqwuauMIiMwBLllnEmgXfAbJA4FJy+"
        #                       "OLhZPrMWMkkiNCLcL069QlvhLXYi/0V9PQVTnvtA5RLarj26s4WSqTZ2I5VGHbTqSIZvZYK3F275KEbQsemYETl18xwZ+"
        #                       "WAuSrYaSKN/pKykK37vUGtT3FeIoJup2c6M8Ghull3OcVmqCOsgvU7/pNjl1rLKEJB6t/X9avcHv+feikwQBtBmd/b2qCeSrEpM7US",
        #         "associated_data": "transaction",
        #         "nonce": "cKEdw8eV9Bh0"
        #     }
        # }
        nonce = newdata['resource']['nonce']
        ciphertext = newdata['resource']['ciphertext']
        associated_data = newdata['resource']['associated_data']
 
        try:
           payment = decrypt(nonce, ciphertext, associated_data, pay_key)
           break
        except Exception as e:
           print(e)
        if not payment:
            return HttpResponse({"code": "FAIL", "message": "失敗"}, status=400)
        payment = eval(payment.decode('utf-8'))
        # payment = {
        #     "mchid": "xxxx",
        #     "appid": "xxxx",
        #     "out_trade_no": "20231654836163523608",
        #     "transaction_id": "4200001646202301065425000524",
        #     "trade_type": "JSAPI",
        #     "trade_state": "SUCCESS",
        #     "trade_state_desc": "xe6x94xafxe4xbbx98xe6x88x90xe5x8ax9f",
        #     "bank_type": "OTHERS",
        #     "attach": "",
        #     "success_time": "2023-01-06T15:12:49+08:00",
        #     "payer": {
        #         "openid": "xxxxx"
        #     },
        #     "amount": {
        #         "total": 1,
        #         "payer_total": 1,
        #         "currency": "CNY",
        #         "payer_currency": "CNY"
        #     }
        # }
        orderno = payment['out_trade_no']
        zf_status = True if payment["trade_type"] == "SUCCESS" else False
        if zf_status:
            money = decimal.Decimal(int(payment["amount"]["payer_total"]) / 100).quantize(decimal.Decimal("0.00"))
        else:
            money = decimal.Decimal(0.0).quantize(decimal.Decimal("0.00"))
        # 7.回撥報文簽名驗證
        # 同第一篇簽名驗證的程式碼
        wechatpay_serial, wechatpay_timestamp, wechatpay_nonce, wechatpay_signature, certificate = check_wx_cert(request, mchid, pay_key, serial_num)
        if wechatpay_serial == serial_num:  # 應答簽名中的序列號同證書序列號應相同
            # 8.獲得回撥報文中交易號後修改已支付訂單狀態
            res = conorder.objects.filter(orderno=orderno, paystatus=-1).first()
            if res:
                res.paystatus = 1
                res.save()
            else:
                res.paystatus = -1
                res.save()
            # 9.專案業務邏輯
            return HttpResponse({"code": "SUCCESS", "message": "成功"})
        else:
            log.error(f"證書序列號比對失敗【請求頭中證書序列號:{wechatpay_serial};本地儲存證書序列號:{serial_num};】")
            return HttpResponse({"code": "FAIL", "message": "失敗"}, status=400)
    except Exception as e:
        log.error(f"微信回撥介面報錯:{e},{traceback.format_exc()}")
        return HttpResponse({"code": "FAIL", "message": "失敗"}, status=400)

5、參考文章:

在此非常感謝博主,文章連結如下:https://zhuanlan.zhihu.com/p/402449405

到此這篇關於Python3 微信支付(小程式支付)V3介面的實現的文章就介紹到這了,更多相關Python3 微信支付V3介面內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com