Python3 微信支付(小程序支付)V3接口
起因:
????????因公司項(xiàng)目需要網(wǎng)上充值功能,從而對(duì)接微信支付,目前也只對(duì)接了微信支付的小程序支付功能,在網(wǎng)上找到的都是對(duì)接微信支付V2版本接口,與我所對(duì)接的接口版本不一致,無(wú)法使用,特此記錄下微信支付完成功能,使用Django完成后端功能,此文章用于記錄使用,
????????以下代碼僅供參考,如若直接商用出現(xiàn)任何后果請(qǐng)自行承擔(dān),本人概不負(fù)責(zé)。
功能:
? ? ? ?調(diào)起微信支付,微信回調(diào)
代碼:
? ? ? ? 1、準(zhǔn)備工作:
? ? ? ? 2、調(diào)起微信支付(后端只能請(qǐng)求微信支付接口向微信支付官方獲取到預(yù)支付交易會(huì)話(huà)標(biāo)識(shí),并返回給前端,前端才能調(diào)起輸入密碼支付界面)
import json import decimal import tracebackimport requests from django.http import HttpResponsedef payment_view(request, *args, **kwargs):"""微信支付(小程序):param request::param args::param kwargs::return:"""try:reqdata = json.loads(request.body)# 前端參數(shù)jscode = reqdata["jscode"] # 微信IDprice = decimal.Decimal(reqdata["price"]).quantize(decimal.Decimal("0.00")) # 充值金額,保留兩位小數(shù)nickname = reqdata["nickname"] # 微信昵稱(chēng)/支付寶名稱(chēng) 前端獲取到返給后端做記錄,可要可不要的字段paymode = reqdata["paymode"] # 支付方式 1微信支付remark = reqdata["remark"] # 支付內(nèi)容描述# 根據(jù)jscode 獲取openIDrets = 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.獲取支付的微信openidprint(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="未獲取到微信用戶(hù)信息"))# 1.以交易日期生成交易號(hào)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)文bodyurl = WX_Pay_URLbody = {"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"}, # 正式上線(xiàn)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ā)送請(qǐng)求獲得prepay_idtry:response = requests.post(url, data=data, headers=headers) # 獲取預(yù)支付交易會(huì)話(huà)標(biāo)識(shí)(prepay_id)print("預(yù)支付交易會(huì)話(huà)標(biāo)識(shí)", 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)答簽名中的序列號(hào)同證書(shū)序列號(hào)應(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, # 訂單號(hào)'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"證書(shū)序列號(hào)驗(yàn)簽失敗{e}, {traceback.format_exc()}")return HttpResponse(general_error_msg(msg="下單失敗"))else:log.error(f"證書(shū)序列號(hào)比對(duì)失敗【請(qǐng)求頭中證書(shū)序列號(hào):{wechatpay_serial};本地存儲(chǔ)證書(shū)序列號(hào):{serial_no};】")return HttpResponse(general_error_msg(msg="調(diào)起微信支付失敗!"))else:log.error(f"獲取預(yù)支付交易會(huì)話(huà)標(biāo)識(shí) 接口報(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 datetimeimport 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)識(shí) 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: 類(lèi)型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 resultelif type == 6:return datetime.datetime.now().strftime("%Y%m%d")# 重構(gòu)系統(tǒng)jargon類(lèi),用于處理時(shí)間格式報(bào)錯(cuò)問(wèn)題 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_keykey_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():"""生成訂單號(hào):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 resultdef 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 signexcept 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):"""微信平臺(tái)證書(shū):param response: 請(qǐng)求微信支付平臺(tái)所對(duì)應(yīng)的的接口返回的響應(yīng)值:param mchid: 商戶(hù)號(hào):param pay_key: 商戶(hù)號(hào)秘鑰:param serial_no: 證書(shū)序列號(hào):return:"""wechatpay_serial, wechatpay_timestamp, wechatpay_nonce, wechatpay_signature, certificate = None, None, None, None, Nonetry:# 11.應(yīng)答簽名驗(yàn)證wechatpay_serial = response.headers['Wechatpay-Serial'] # 獲取HTTP頭部中包括回調(diào)報(bào)文的證書(shū)序列號(hà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.獲取微信平臺(tái)證書(shū) (等于又把前面的跑一遍,實(shí)際上應(yīng)是獲得一次證書(shū)就存起來(lái),不用每次都重新獲取一次)url2 = "https://api.mch.weixin.qq.com/v3/certificates"# 11.2.生成證書(shū)請(qǐng)求隨機(jī)串random_str2 = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))# 11.3.生成證書(shū)請(qǐng)求時(shí)間戳time_stamps2 = str(int(time.time()))# 11.4.生成請(qǐng)求證書(shū)的簽名串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請(qǐng)求頭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ā)送請(qǐng)求獲得證書(shū)response2 = requests.get(url2, headers=headers2) # 只需要請(qǐng)求頭cert = response2.json()# 11.8.證書(shū)解密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"微信平臺(tái)證書(shū)驗(yàn)證報(bào)錯(cuò):{e};{traceback.format_exc()}")return wechatpay_serial, wechatpay_timestamp, wechatpay_nonce, wechatpay_signature, certificate, serial_nodef verify(check_data, signature, certificate):"""驗(yàn)簽函數(shù):param check_data::param signature::param certificate::return:"""key = RSA.importKey(certificate) # 這里直接用了解密后的證書(shū),但沒(méi)有去導(dǎo)出公鑰,似乎也是可以的。怎么導(dǎo)公鑰還沒(méi)搞懂。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'):"""定義微信支付請(qǐng)求接口中請(qǐng)求頭認(rèn)證:param mchid: 商戶(hù)ID:param serial_num: 證書(shū)序列號(hào):param data: 請(qǐng)求體內(nèi)容:param method: 請(qǐng)求方法:return: headers(請(qǐng)求頭)"""# 4.定義生成簽名的函數(shù) get_sign(sign_str)# 5.生成請(qǐng)求隨機(jī)串random_str = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(32))# 6.生成請(qǐng)求時(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請(qǐng)求頭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_stamps4、微信回調(diào)
import decimal import json import tracebackfrom django.http import HttpResponsedef notify_view(request, *args, **kwargs):"""支付完成之后的通知(微信官方返回的數(shù)據(jù)):param request::param args::param kwargs::return:"""try:# 1.獲得支付通知的參數(shù)body = request.bodydata = 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)breakexcept 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_state"] == "SUCCESS" else Falseif 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, serial_no = check_wx_cert(request, mchid, pay_key, serial_num)if wechatpay_serial == serial_no: # 應(yīng)答簽名中的序列號(hào)同證書(shū)序列號(hào)應(yīng)相同# 8.獲得回調(diào)報(bào)文中交易號(hào)后修改已支付訂單狀態(tài)res = conorder.objects.filter(orderno=orderno, paystatus=0).first()if res:res.paystatus = 1res.save()else:res.paystatus = -1res.save()# 9.項(xiàng)目業(yè)務(wù)邏輯return HttpResponse({"code": "SUCCESS", "message": "成功"})else:log.error(f"證書(shū)序列號(hào)比對(duì)失敗【請(qǐng)求頭中證書(shū)序列號(hào):{wechatpay_serial};本地存儲(chǔ)證書(shū)序列號(hào):{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、對(duì)應(yīng)表模型
????????
class conorder(models.Model):id = models.CharField(db_column='id', max_length=50, default=getuuid, primary_key=True, verbose_name="訂單ID")orderno = models.CharField(db_column='orderno', max_length=50, null=False, blank=False, verbose_name="訂單編號(hào)")serialno = models.CharField(db_column='serialno', max_length=50, null=True, blank=False, verbose_name="流水號(hào)")openid = models.CharField(db_column='openid', max_length=50, null=True, blank=False, verbose_name="微信ID/支付寶ID")openname = models.CharField(db_column='openname', max_length=255, null=True, blank=False,verbose_name="微信名稱(chēng)/支付寶名稱(chēng)")orgid = models.CharField(db_column='orgid', max_length=50, null=False, blank=False, verbose_name="組織機(jī)構(gòu)ID")iotuserid = models.CharField(db_column='iotuserid', max_length=50, null=False, blank=False, verbose_name="人員ID")invoice = models.CharField(db_column='invoice', max_length=50, null=True, blank=False, verbose_name="發(fā)票")invoicenum = models.CharField(db_column='invoicenum', max_length=50, null=True, blank=False, verbose_name="發(fā)票號(hào)碼")paymode = models.SmallIntegerField(db_column='paymode', null=False, blank=False, verbose_name="支付模式 1微信支付")paysource = models.CharField(db_column='paysource', max_length=50, null=True, blank=False,verbose_name="支付來(lái)源 如零錢(qián)/銀行卡")goodstotalprice = models.DecimalField(db_column='goodstotalprice', max_digits=12, decimal_places=2, null=False, blank=False,verbose_name="充值金額 單位元")paystatus = models.SmallIntegerField(db_column='paystatus', null=False, blank=False,verbose_name="支付狀態(tài) 1成功 0待支付 -1支付失敗")remark = models.CharField(db_column='remark', max_length=200, null=True, blank=False, verbose_name="備注")delstate = models.SmallIntegerField(db_column='delstate', default=0, verbose_name='數(shù)據(jù)狀態(tài) 0正常 1已刪除')createtime = models.DateTimeField(db_column='createtime', auto_now_add=True, null=True, verbose_name="建立時(shí)間")createoprid = models.CharField(db_column='createoprid', max_length=50, null=True, verbose_name="建立人員")updatetime = models.DateTimeField(db_column='updatetime', auto_now=True, null=True, verbose_name="更新時(shí)間 充值成功與否的時(shí)間")updateoprid = models.CharField(db_column='updateoprid', max_length=50, null=True, verbose_name="更新人員")class Meta:db_table = "order"indexes = [models.Index(fields=['iotuserid'], name='order_userid'),]verbose_name = "充值訂單表"verbose_name_plural = verbose_name6、借鑒地址:
? ? ? ? 在此非常感謝博主,文章鏈接如下:一文基本搞定python的django框架下微信支付v3的主要流程-1 - 知乎??????從去年底開(kāi)始,下決心自己寫(xiě)代碼來(lái)搞定自已策劃的微信小程序” 來(lái)推鑒--投融資項(xiàng)目推薦服務(wù)平臺(tái)“后,微信支付就成為擋在前面的一座大山。畢竟是從一個(gè)從沒(méi)開(kāi)發(fā)過(guò)一個(gè)程序的基本零基礎(chǔ),到要真正上線(xiàn)一個(gè)能商業(yè)運(yùn)…https://zhuanlan.zhihu.com/p/402449405
7、請(qǐng)注意:
????????以上代碼僅供參考,如若直接商用出現(xiàn)任何后果請(qǐng)自行承擔(dān),本人概不負(fù)責(zé)。?
總結(jié)
以上是生活随笔為你收集整理的Python3 微信支付(小程序支付)V3接口的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 全双工和半双工的区别?
- 下一篇: python懒人小工具:python打包