Python3 微信支付(小程序支付)V3接口的實(shí)現(xiàn)
起因:
因公司項(xiàng)目需要網(wǎng)上充值功能,從而對接微信支付,目前也只對接了微信支付的小程序支付功能,在網(wǎng)上找到的都是對接微信支付V2版本接口,與我所對接的接口版本不一致,無法使用,特此記錄下微信支付完成功能,使用Django完成后端功能,此文章用于記錄使用,
以下代碼僅供參考,如若直接商用出現(xiàn)任何后果請自行承擔(dān),本人概不負(fù)責(zé)。
功能:
調(diào)起微信支付,微信回調(diào)
代碼:
1、準(zhǔn)備工作:
mchid = "xxxxxx" # 商戶號 pay_key = "xxxxxx" # 商戶秘鑰V3 使用V3接口必須使用V3秘鑰 serial_num = "xxxxxx" # 證書序列號 # ======================前三個(gè)參數(shù)在微信支付中可找到=============================== # ============ 商戶號(mchid ) 在賬戶中心——商戶信息——微信支付商戶號 (是純數(shù)字) ================== # ============= 商戶秘鑰(pay_key) 在賬戶中心——API安全——APIv3秘鑰 (需手動(dòng)設(shè)置) =================== # ============= 證書序列號(serial_num) 在賬戶中心——API安全——API證書 (需手動(dòng)申請,通過后會(huì)有串證書序列號),申請完成后需要把證書下載到項(xiàng)目中,便于使用 =================== appid = "xxxxxx" # 微信小程序appid wx_secret ="xxxxxx" # 微信小程序秘鑰 # ============= 微信小程序appid 在產(chǎn)品中心——AppID賬號管理——添加關(guān)聯(lián)的AppID =================== WX_Pay_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi" # ============= 微信支付調(diào)用地址,用于請求接收 預(yù)支付交易會(huì)話標(biāo)識: prepay_id =================== WX_Notify_URL = "https://127.0.0.1:8000" # ============= 接收微信支付回調(diào)地址,必須是https ===================
2、調(diào)起微信支付(后端只能請求微信支付接口向微信支付官方獲取到預(yù)支付交易會(huì)話標(biāo)識,并返回給前端,前端才能調(diào)起輸入密碼支付界面)
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) # 前端參數(shù) jscode = reqdata["jscode"] # 微信ID price = decimal.Decimal(reqdata["price"]).quantize(decimal.Decimal("0.00")) # 充值金額,保留兩位小數(shù) nickname = reqdata["nickname"] # 微信昵稱/支付寶名稱 前端獲取到返給后端做記錄,可要可不要的字段 paymode = reqdata["paymode"] # 支付方式 1微信支付 remark = reqdata["remark"] # 支付內(nèi)容描述 # 根據(jù)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 支付狀態(tài) 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.生成統(tǒng)一下單的報(bào)文body url = WX_Pay_URL body = { "appid": appid, "mchid": mchid, "description": remark, "out_trade_no": orderno, "notify_url": WX_Notify_URL + "/pay/notify", # 后端接收回調(diào)通知的接口 "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.發(fā)送請求獲得prepay_id try: response = requests.post(url, data=data, headers=headers) # 獲取預(yù)支付交易會(huì)話標(biāo)識(prepay_id) print("預(yù)支付交易會(huì)話標(biāo)識", 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簽名驗(yàn)證 if wechatpay_serial == serial_no: # 應(yīng)答簽名中的序列號同證書序列號應(yīng)相同 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.生成調(diào)起支付API需要的參數(shù)并返回前端 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"證書序列號驗(yàn)簽失敗{e}, {traceback.format_exc()}") return HttpResponse(general_error_msg(msg="下單失敗")) else: log.error(f"證書序列號比對失敗【請求頭中證書序列號:{wechatpay_serial};本地存儲證書序列號:{serial_no};】") return HttpResponse(general_error_msg(msg="調(diào)起微信支付失敗!")) else: log.error(f"獲取預(yù)支付交易會(huì)話標(biāo)識 接口報(bào)錯(cuò)【params:{data};headers:{headers};response:{response.text}】") return HttpResponse(general_error_msg(msg="調(diào)起微信支付失敗!")) except Exception as e: log.error(f"調(diào)用微信支付接口超時(shí)【params:{data};headers:{headers};】:{e},{traceback.format_exc()}") return HttpResponse(general_error_msg(msg="微信支付超時(shí)!")) except Exception as e: log.error(f"微信支付接口報(bào)錯(cuò):{e},{traceback.format_exc()}") return HttpResponse(general_error_msg(msg="微信支付接口報(bào)錯(cuò)!"))
3、相關(guān)方法
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 # 獲取唯一標(biāo)識 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())))) # 獲取當(dāng)前時(shí)間 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時(shí)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時(shí)%M分%S秒", t)) return result elif type == 6: return datetime.datetime.now().strftime("%Y%m%d") # 重構(gòu)系統(tǒng)jargon類,用于處理時(shí)間格式報(bào)錯(cuò)問題 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: """ # 下單時(shí)間的年月日毫秒12+隨機(jī)數(shù)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): """ 定義生成簽名的函數(shù) :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("生成簽名的函數(shù)方法報(bào)錯(cuò)【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: 請求微信支付平臺所對應(yīng)的的接口返回的響應(yīng)值 :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.應(yīng)答簽名驗(yàn)證 wechatpay_serial = response.headers['Wechatpay-Serial'] # 獲取HTTP頭部中包括回調(diào)報(bào)文的證書序列號 wechatpay_signature = response.headers['Wechatpay-Signature'] # 獲取HTTP頭部中包括回調(diào)報(bào)文的簽名 wechatpay_timestamp = response.headers['Wechatpay-Timestamp'] # 獲取HTTP頭部中包括回調(diào)報(bào)文的時(shí)間戳 wechatpay_nonce = response.headers['Wechatpay-Nonce'] # 獲取HTTP頭部中包括回調(diào)報(bào)文的隨機(jī)串 # 11.1.獲取微信平臺證書 (等于又把前面的跑一遍,實(shí)際上應(yīng)是獲得一次證書就存起來,不用每次都重新獲取一次) url2 = "https://api.mch.weixin.qq.com/v3/certificates" # 11.2.生成證書請求隨機(jī)串 random_str2 = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32)) # 11.3.生成證書請求時(shí)間戳 time_stamps2 = str(int(time.time())) # 11.4.生成請求證書的簽名串 data2 = "" sign_str2 = f"GET\n{'/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.發(fā)送請求獲得證書 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"微信平臺證書驗(yàn)證報(bào)錯(cuò):{e};{traceback.format_exc()}") return wechatpay_serial, wechatpay_timestamp, wechatpay_nonce, wechatpay_signature, certificate, serial_no def verify(check_data, signature, certificate): """ 驗(yàn)簽函數(shù) :param check_data: :param signature: :param certificate: :return: """ key = RSA.importKey(certificate) # 這里直接用了解密后的證書,但沒有去導(dǎo)出公鑰,似乎也是可以的。怎么導(dǎo)公鑰還沒搞懂。 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'): """ 定義微信支付請求接口中請求頭認(rèn)證 :param mchid: 商戶ID :param serial_num: 證書序列號 :param data: 請求體內(nèi)容 :param method: 請求方法 :return: headers(請求頭) """ # 4.定義生成簽名的函數(shù) get_sign(sign_str) # 5.生成請求隨機(jī)串 random_str = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32)) # 6.生成請求時(shí)間戳 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、微信回調(diào)
import decimal import json import traceback from django.http import HttpResponse def notify_view(request, *args, **kwargs): """ 支付完成之后的通知(微信官方返回的數(shù)據(jù)) :param request: :param args: :param kwargs: :return: """ try: # 1.獲得支付通知的參數(shù) 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": "\xe6\x94\xaf\xe4\xbb\x98\xe6\x88\x90\xe5\x8a\x9f", # "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": "\xe6\x94\xaf\xe4\xbb\x98\xe6\x88\x90\xe5\x8a\x9f", # "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.回調(diào)報(bào)文簽名驗(yàn)證 # 同第一篇簽名驗(yàn)證的代碼 wechatpay_serial, wechatpay_timestamp, wechatpay_nonce, wechatpay_signature, certificate = check_wx_cert(request, mchid, pay_key, serial_num) if wechatpay_serial == serial_num: # 應(yīng)答簽名中的序列號同證書序列號應(yīng)相同 # 8.獲得回調(diào)報(bào)文中交易號后修改已支付訂單狀態(tài) res = conorder.objects.filter(orderno=orderno, paystatus=-1).first() if res: res.paystatus = 1 res.save() else: res.paystatus = -1 res.save() # 9.項(xiàng)目業(yè)務(wù)邏輯 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"微信回調(diào)接口報(bào)錯(cuò):{e},{traceback.format_exc()}") return HttpResponse({"code": "FAIL", "message": "失敗"}, status=400)
5、參考文章:
在此非常感謝博主,文章鏈接如下:https://zhuanlan.zhihu.com/p/402449405
到此這篇關(guān)于Python3 微信支付(小程序支付)V3接口的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Python3 微信支付V3接口內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于Python中兩個(gè)不同shape的數(shù)組間運(yùn)算規(guī)則
這篇文章主要介紹了關(guān)于Python中兩個(gè)不同shape的數(shù)組間運(yùn)算規(guī)則,眾所周知,相同?shape?的兩個(gè)數(shù)組間運(yùn)算是指兩個(gè)數(shù)組的對應(yīng)元素相加,我們經(jīng)常會(huì)碰到一些不同?shape?的數(shù)組間運(yùn)算,需要的朋友可以參考下2023-08-08Python 用Redis簡單實(shí)現(xiàn)分布式爬蟲的方法
本篇文章主要介紹了Python 用Redis簡單實(shí)現(xiàn)分布式爬蟲的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-11-11python、PyTorch圖像讀取與numpy轉(zhuǎn)換實(shí)例
今天小編就為大家分享一篇python、PyTorch圖像讀取與numpy轉(zhuǎn)換實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-01-01Python實(shí)現(xiàn)遍歷大量表格文件并篩選出數(shù)據(jù)缺失率低的文件
這篇文章主要為大家詳細(xì)介紹了如何利用Python實(shí)現(xiàn)遍歷大量表格文件并篩選出表格內(nèi)數(shù)據(jù)缺失率低的文件的功能,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-05-05Python實(shí)現(xiàn)yaml與json文件批量互轉(zhuǎn)
這篇文章主要為大家詳細(xì)介紹了如何利用Python語言實(shí)現(xiàn)yaml與json文件的批量互轉(zhuǎn),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以動(dòng)手嘗試一下2022-07-07基于Python實(shí)現(xiàn)帕累托圖的示例詳解
帕累托圖是一種特殊的直方圖, 在項(xiàng)目管理知識體系中屬于質(zhì)量管理的工具。本文為大家整理了Python實(shí)現(xiàn)帕累托圖的方法,需要的可以參考一下2023-03-03