前言:小程序支付
商戶系統和微信支付系統主要交互:
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 # 到這里再次簽名流程后端結束,開始去寫前端*************************