前言:小程序支付
商户系统和微信支付系统主要交互:
1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API】
2、商户server调用支付统一下单,api参见公共api【统一下单API】
3、商户server调用再次签名,api参见公共api【再次签名】
4、商户server接收支付通知,api参见公共api【支付结果通知API】
5、商户server查询支付结果,api参见公共api【查询订单API】
支付流程总结:
用户登录点击下单—小程序wx.request请求商户支付接口—商户带上必要参数请求微信支付url — 生成预支付交易单(prepay_id) — 返回正确的预支付交易(再次签名)—调起支付(wx.requestPayment)
1)小程序登入API解释
2)统一下单接口解释
1当用户发起下单请求,且用户是登入状态的情况下,我们就可以创建我们商城的订单了。
2当订单创建完毕,我们应该发起支付,调用支付接口,也就是统一下单接口(这里的下单不是我们商城的下单,而是对微信官方进行支付下单),我们按照微信统一下单接口发送数据,微信下单接口会同步返回数据给我们,也就是上图中prepay_id
3)再次签名
1当我们拿到统一下单的数据以后,我们要再次进行签名(对数据加密以及处理)。
2 然后将数据发送给我们小程序。
3 小程序拿到我们发送的数据后调起支付界面,这样用户就可以进行支付了,如果支付成功,微信会直接返回 结果给我们小程序
4)异步通知接口
1在第三步再次签名中我们小程序已经知道用户是否支付成功,但是我们后端还不知道该订单是否支付成功
2 基于1的问题,微信会以异步的方式通知我们后端程序,我们拿到微信异步通知的数据,我们就可以对订单 进行修改。那这样就实现了,前后都知道用户的支付结果
一、统一下单
这里下单不是我们业务中下单,这里的下单是对我们微信支付接口下单
1.1 接口链接
URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder
携带下面这些必要参数,post请求该url会有返回值
1.2 请求参数
在里面我们只对部分必传字段进行解释,其他字段根据开发的需求而定,详细内容见API
1 appid
我们小程序的唯一标识,在注册的时候我们已经知道了
2 mch_id
我们使用支付,就必须开通支付,以公司的证件开通,开通过后就分配该商户号mch_id
3 nonce_str:随机字符串不能超过32位
代码实现:

# 如果是做验证码的话,不要出现1/i,0/o def get_random(self): import random data = "123456789zxcvbnmasdfghjklqwertyuiopZXCVBNMASDFGHJKLQWERTYUIOP" nonce_str = "".join(random.sample(data, 30)) return nonce_str
4 sign:签名,期作用是为了防止我们发送的数据陪黑客拦截,根据官方算法,我们这里直接实现:

def get_sign(): data_dic = { #随机字符串 "nonce_str": nonce_str, #商城订单号,不能重复,大家可以更具公司要求,生成订单号 "out_trade_no": out_trade_no, #客户端请求的ip "spbill_create_ip": spbill_create_ip, #回调地址,就是微信服务异步通知我们的支付结果时候的url,这个url不能携带参数 #错误 http://www.test.com/notify_url?name=weixin #正确 http://www.test.com/notify_url "notify_url": notify_url, #用户唯一标识,是那个用户要发起支付 "openid": open_id, #订单表述 "body": body, #固定值,用小程序支付必须传这个JSAPI "trade_type": "JSAPI", "appid": appid, #该次支付所要支付的钱数,是以分为单位,大家要注意 "total_fee": total_fee, "mch_id": mch_id } #这里pay_apikey是和mch_id一起分配的 sign_str = "&".join([f"{k}={data_dic[k]}" for k in sorted(data_dic)]) sign_str = f"{sign_str}&key={pay_apikey}" import hashlib md5 = hashlib.md5() md5.update(sign_str.encode("utf-8")) sign = md5.hexdigest() return sign.upper()
最终将数据组织成xml数据。如下:

# 下面开始生成data,f为参数替换,这一大堆就是data data=f""" <xml> <appid>{self.appid}</appid> <body>{ self.body}</body> <mch_id>{self.mch_id}</mch_id> <nonce_str>{self.nonce_str}</nonce_str> <notify_url>{self.notify_url}</notify_url> <openid>{self.openid}</openid> <out_trade_no>{self.out_trade_no}</out_trade_no> <spbill_create_ip>{self.spbill_create_ip}</spbill_create_ip> <total_fee>{self.total_fee}</total_fee> <trade_type>{self.trade_type}</trade_type> <sign>{self.sign}</sign> </xml> """
注:参数值用XML转义即可,CDATA标签用于说明数据不被XML解析器解析。
1.3 发送数据请求连接
这里最终是以xml的形式发送数据,所以我们发送数据的时候要用heard头中的"content-type:application/xml"
发送请求代码:

def xml_to_dict(self,data): import xml.etree.ElementTree as ET xml_dict = {} # 将二进制xml数据转换成了xml老母猪 data_dic = ET.fromstring(data) # 大概是个xml老母猪 <Element 'xml' at 0x0000000002168EF8> # 遍历老母猪,生成真正的字典,key为标签名,val为标签值 for item in data_dic: xml_dict[item.tag] = item.text # <xml> <appid>123456</appid> </xml> return xml_dict # {'appid': '123456'} def get_body_data(): body_data = f""" <xml> <appid>{appid}</appid> <mch_id>{mch_id}</mch_id> <nonce_str>{nonce_str}</nonce_str> <sign>{sign}</sign> <body>{body}</body> <out_trade_no>{out_trade_no}</out_trade_no> <total_fee>{total_free}</total_fee> <spbill_create_ip>{spbill_create_ip}</spbill_create_ip> <notify_url>{notify_url}</notify_url> <openid>{open_id}</openid> <trade_type>JSAPI</trade_type> </xml>""" import requests url = "https://api.mch.weixin.qq.com/pay/unifiedorder" response = requests.post(url, data_body.encode("utf-8"), headers={'content-type': "application/xml"}) #由于返回的数据也是xml的,我们需要将xml数据转化成字典, res_dict =xml_to_dic(response.content)
1.4 请求的返回数据
返回的数据也是二进制的xml我们需要将其转化为字典形式,取出prepay_id用于再次签名

# 二进制的xml转换为字典 def xml_to_dict(self,data): import xml.etree.ElementTree as ET xml_dict = {} # 将二进制xml数据转换成了xml老母猪 data_dic = ET.fromstring(data) # 大概是个xml老母猪 <Element 'xml' at 0x0000000002168EF8> # 遍历老母猪,生成真正的字典,key为标签名,val为标签值 for item in data_dic: xml_dict[item.tag] = item.text # <xml> <appid>123456</appid> </xml> return xml_dict # {'appid': '123456'} def pay(self): ... # xml发送二进制,返回的肯定也是二进制,requests模块返回的二进制数据在response.content里面 res_data=self.xml_to_dict(response.content) # 我们将接受到的二进制转换为字典 prepay_id = res_data["prepay_id"] # 至此统一下单流程完成++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
二、再次签名
收到统一下单中得到的prepay_id,我们就可以根据API再次签名流程和数据要求来准备数据,再次根据签名算法签名

def pay(self): ...... # 下面开始进入再次签名流程******************************************************************* # 调起再次签名函数,得到再次签名的数据返回给前端 data=self.two_sign(prepay_id) return data # 再次签名 文档中有参数说明 def two_sign(self,prepay_id): timeStamp=str(int(time.time())) nonceStr=self.get_str() # 这个和统一下单中生成方法一样,直接使用 data_dict={ "appId":settings.AppId, "timeStamp":timeStamp, "nonceStr":nonceStr, "package":f"prepay_id={prepay_id}", # 文档中有说明,就是统一下单返回值中的prepay_id "signType":"MD5" } # 按照官网只是一步一步来,和统一下单中sign步骤一样 sign_str = "&".join([f"{k}={data_dict[k]}" for k in sorted(data_dict)]) sign_str = f"{sign_str}&key={settings.pay_apikey}" md5 = hashlib.md5() md5.update(sign_str.encode("utf-8")) sign=md5.hexdigest().upper() # 看文档,前端需要data_dict中四个参数和这个签名 data_dict["paySign"]=sign data_dict.pop("appId") # 这个肯定不能给前端 return data_dict # 到这里再次签名流程后端结束,开始去写前端*************************
将再次签名的数据data发送给前端,小程序开始调用wx.requestPayment来调起支付
三 、完整代码
前台代码

pay:function(){ wx.request({ url: "http://127.0.0.1:8000/pay/", method: "POST", data:{"login_key":wx.getStorageSync("login_key")}, header: { "content-type": "application/json" }, success: function (e) { console.log(e) wx.requestPayment({ 'timeStamp': e.data.data.timeStamp, 'nonceStr': e.data.data.nonceStr, 'package': e.data.data.package, 'signType': e.data.data.signType, 'paySign': e.data.data.paySign, 'success': function (res) { console.log(res,"成功") }, 'fail': function (res) { console.log("支付失败",res) }, }) } }) },
view/Order 订单接口
from rest_framework.views import APIView from rest_framework.response import Response from app01.wx import wx_login from django.core.cache import cache import hashlib,time import random from app01.wx import settings import requests class Pay(APIView): def post(self,request): # 第一步:用login_key先取出openid再说别的 param=request.data if param.get("login_key"): openid,session_key=cache.get(param.get("login_key")).split("&") self.openid=openid # 将其制作成属性 # 获取ip 这一块是写后面data的时候发现需要ip来这里补充 #如果是Nginx做的负载均衡,就是Nginx来访问服务器,不能直接获取IP,就要HTTP_X_FORWARDED_FOR if request.META.get('HTTP_X_FORWARDED_FOR'): self.ip =request.META['HTTP_X_FORWARDED_FOR'] else: #如果没有用Nginx做负载均衡,就是用户直接访问,直接用REMOTE_ADDR 获取自身ip self.ip = request.META['REMOTE_ADDR'] data = self.pay() # 第二步 :获取各种必需的参数,官网统一订单有说明 return Response({"code":200,"msg":"ok","data":data}) else: return Response({"code":200,"msg":"缺少参数"}) # nonce_str:随机字符串,长度要求在32位以内 def get_str(self): str_all="1234567890abcdefghjklmasdwery" nonce_str="".join(random.sample(str_all,20)) return nonce_str # 唯一订单号 def get_order(self): # 这里只是示意,小公司用时间戳加随机就差不多了保证唯一了 # 大公司高并发,可以用redis生成全局唯一id再做拼接 order_id=str(time.strftime("%Y%m%d%H%M%S")) return order_id # 二进制的xml转换为字典 def xml_to_dict(self,data): import xml.etree.ElementTree as ET xml_dict = {} # 将二进制xml数据转换成了xml老母猪 data_dic = ET.fromstring(data) # 大概是个xml老母猪 <Element 'xml' at 0x0000000002168EF8> # 遍历老母猪,生成真正的字典,key为标签名,val为标签值 for item in data_dic: xml_dict[item.tag] = item.text # <xml> <appid>123456</appid> </xml> return xml_dict # {'appid': '123456'} # 根据官网步骤一步一步来 def get_sign(self): data_dic = { "nonce_str": self.nonce_str, "out_trade_no": self.out_trade_no, "spbill_create_ip": self.ip, "notify_url": self.notify_url, "openid": self.openid, "body": self.body, "trade_type": "JSAPI", "appid": self.appid, "total_fee": self.total_fee, "mch_id": self.mch_id } # f为参数替换 sign_str = "&".join([f"{k}={data_dic[k]}" for k in sorted(data_dic)]) sign_str = f"{sign_str}&key={settings.pay_apikey}" md5 = hashlib.md5() md5.update(sign_str.encode("utf-8")) return md5.hexdigest().upper() def pay(self): # 第二步;根据官方文档中各个参数,我们先组织这些参数,上面的方法都是得到函数 self.appid=settings.AppId self.mch_id=settings.pay_mchid self.nonce_str=self.get_str() # 随机字符串,长度要求在32位以内 self.body="饼哥生活费" # 商品简单描述,该字段请按照规范传递 self.out_trade_no=self.get_order() # 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一 self.total_fee=1 # 订单总金额,单位为分 self.spbill_create_ip=self.ip # 调用微信支付API的机器IP(在上面主函数post里面获取) self.notify_url="http://www.baidu.com" #异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数 self.trade_type="JSAPI" # 小程序交易类型,固定这样 self.sign = self.get_sign() # 通过签名算法计算得出的签名值,详见官方文档签名生成算法 # 下面开始生成data,f为参数替换,这一大堆就是data data=f""" <xml> <appid>{self.appid}</appid> <body>{ self.body}</body> <mch_id>{self.mch_id}</mch_id> <nonce_str>{self.nonce_str}</nonce_str> <notify_url>{self.notify_url}</notify_url> <openid>{self.openid}</openid> <out_trade_no>{self.out_trade_no}</out_trade_no> <spbill_create_ip>{self.spbill_create_ip}</spbill_create_ip> <total_fee>{self.total_fee}</total_fee> <trade_type>{self.trade_type}</trade_type> <sign>{self.sign}</sign> </xml> """ # 第三步:上面将所有需要的参数都准备齐全了,下面开始请求url发送数据,发送xml数据肯定是post请求,并且要发二进制 url="https://api.mch.weixin.qq.com/pay/unifiedorder" # 固定请求url response=requests.post(url,data.encode("utf-8"),headers={"content-type":"application/xml"}) # 第四步 :接收并处理请求返回值 # xml发送二进制,返回的肯定也是二进制,requests模块返回的二进制数据在response.content里面 res_data=self.xml_to_dict(response.content) # 我们将接受到的二进制转换为字典 prepay_id = res_data["prepay_id"] # 至此统一下单流程完成++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # 下面开始进入再次签名流程******************************************************************* # 调起再次签名函数,得到再次签名的数据返回给前端 data=self.two_sign(prepay_id) return data # 再次签名 文档中有参数说明 def two_sign(self,prepay_id): timeStamp=str(int(time.time())) nonceStr=self.get_str() # 这个和统一下单中生成方法一样,直接使用 data_dict={ "appId":settings.AppId, "timeStamp":timeStamp, "nonceStr":nonceStr, "package":f"prepay_id={prepay_id}", # 文档中有说明,就是统一下单返回值中的prepay_id "signType":"MD5" } # 按照官网只是一步一步来,和统一下单中sign步骤一样 sign_str = "&".join([f"{k}={data_dict[k]}" for k in sorted(data_dict)]) sign_str = f"{sign_str}&key={settings.pay_apikey}" md5 = hashlib.md5() md5.update(sign_str.encode("utf-8")) sign=md5.hexdigest().upper() # 看文档,前端需要data_dict中四个参数和这个签名 data_dict["paySign"]=sign data_dict.pop("appId") # 这个肯定不能给前端 return data_dict # 到这里再次签名流程后端结束,开始去写前端*************************