Django微信公眾號開發(二)公眾號內微信支付
前言
微信公眾號開發又一重要項就是微信支付,如何在微信公眾號中或者微信自帶的瀏覽器中實現微信支付呢?這就是本文的目的。
對於微信支付有幾種分類,一種是app支付(顧名思義給予一些軟體app使用的)、微信內H5支付(什麼意思呢,就是微信內建的瀏覽器自帶了一些js、css檔案,當然是微信特有的,上篇部落格有講過,獲取到的使用者暱稱帶表情會亂碼,在微信自帶的瀏覽器你只要將它裝換為utf-8就能正常顯示,原因是內建瀏覽器有對應的font檔案,所以在內建瀏覽器中也有包含了支付功能的js檔案,沒錯在微信自帶的瀏覽器中你不用在你的html中引入js都可以使用它的內建js,本文會講解如何用它的js調起支付)、其它PC端瀏覽器的掃碼支付。
開發流程
- 伺服器
同樣需要伺服器,然後設定伺服器的域名,比如這裡還是設定域名為:www.show.netcome.net
。 配置公眾號收集資訊
首先需要一個有微信支付許可權和網頁授權許可權的公眾號,其次需要一個有微信支付許可權的商戶號(商戶號就是支付的錢到哪裡)。同樣,登入公眾平臺在開發–>基本配置–>公眾號開發資訊裡找到公眾號的開發者ID(AppID)和開發者密碼(AppSecret) ,然後在微信商戶平臺裡找到mch_id和api金鑰。
注意:必須先在微信公眾平臺設定網頁授權域名(這個域名你就填伺服器的www.show.netcome.net
)和在www.show.netcome.net/payment
)。公眾號支付在請求支付的時候會校驗請求來源是否有在商戶平臺做了配置,所以必須確保支付目錄已經正確的被配置,否則將驗證失敗,請求支付不成功。開發流程
微信支付不是說一開始就傳訂單編號、價格、商品等資訊去呼叫支付的,在呼叫支付介面前我們需要先去向微信發起下單請求,只有發起下單成功後才能呼叫支付介面。
首先配置你的公眾號、商戶和回撥頁面資訊,其它值做相應修改,引數檔案如下:
# -*- coding: utf-8 -*-
# ----------------------------------------------
# @Time : 18-3-21 上午11:50
# @Author : YYJ
# @File : wechatConfig.py
# @CopyRight: ZDWL
# ----------------------------------------------
"""
微信公眾號和商戶平臺資訊配置檔案
"""
# ----------------------------------------------微信公眾號---------------------------------------------- #
# 公眾號id
APPID = 'appid'
# 公眾號AppSecret
APPSECRET = 'appscrect'
# ----------------------------------------------微信商戶平臺---------------------------------------------- #
# 商戶id
MCH_ID = 'mc_id'
# 商戶API祕鑰
API_KEY = 'api祕鑰'
# ----------------------------------------------回撥頁面---------------------------------------------- #
# 使用者授權獲取code後的回撥頁面,如果需要實現驗證登入就必須填寫
REDIRECT_URI = 'http://meili.netcome.net/index'
PC_LOGIN_REDIRECT_URI = 'http://meili.netcome.net/index'
defaults = {
# 微信內建瀏覽器獲取code微信介面
'wechat_browser_code': 'https://open.weixin.qq.com/connect/oauth2/authorize',
# 微信內建瀏覽器獲取access_token微信介面
'wechat_browser_access_token': 'https://api.weixin.qq.com/sns/oauth2/access_token',
# 微信內建瀏覽器獲取使用者資訊微信介面
'wechat_browser_user_info': 'https://api.weixin.qq.com/sns/userinfo',
# pc獲取登入二維碼介面
'pc_QR_code': 'https://open.weixin.qq.com/connect/qrconnect',
# 獲取微信公眾號access_token介面
'mp_access_token': 'https://api.weixin.qq.com/cgi-bin/token',
# 設定公眾號行業介面
'change_industry': 'https://api.weixin.qq.com/cgi-bin/template/api_set_industry',
# 獲取公眾號行業介面
'get_industry': 'https://api.weixin.qq.com/cgi-bin/template/get_industry',
# 傳送模板資訊介面
'send_templates_message': 'https://api.weixin.qq.com/cgi-bin/message/template/send',
# 支付下單介面
'order_url': 'https://api.mch.weixin.qq.com/pay/unifiedorder',
}
SCOPE = 'snsapi_userinfo'
PC_LOGIN_SCOPE = 'snsapi_login'
GRANT_TYPE = 'client_credential'
STATE = ''
LANG = 'zh_CN'
下面就是支付下單和支付介面呼叫的封裝程式碼了,其中包括了上一篇部落格的授權登入程式碼和下一篇部落格的傳送模板訊息的程式碼封裝:
# -*- coding: utf-8 -*-
# ----------------------------------------------
# @Time : 18-3-21 下午1:36
# @Author : YYJ
# @File : WechatAPI.py
# @CopyRight: ZDWL
# ----------------------------------------------
import hashlib
import random
import time
from urllib import parse
from xml.etree.ElementTree import fromstring
import requests
from src.beauty.main.wechat.config import wechatConfig
class WechatAPI(object):
def __init__(self):
self.config = wechatConfig
self._access_token = None
self._openid = None
self.config = wechatConfig
self.dic = {}
@staticmethod
def process_response_login(rsp):
"""解析微信登入返回的json資料,返回相對應的dict, 錯誤資訊"""
if 200 != rsp.status_code:
return None, {'code': rsp.status_code, 'msg': 'http error'}
try:
content = rsp.json()
except Exception as e:
return None, {'code': 9999, 'msg': e}
if 'errcode' in content and content['errcode'] != 0:
return None, {'code': content['errcode'], 'msg': content['errmsg']}
return content, None
def process_response_pay(self, rsp):
"""解析微信支付下單返回的json資料,返回相對應的dict, 錯誤資訊"""
rsp = self.xml_to_array(rsp)
if 'SUCCESS' != rsp['return_code']:
return None, {'code': '9999', 'msg': rsp['return_msg']}
if 'prepay_id' in rsp:
return {'prepay_id': rsp['prepay_id']}, None
return rsp, None
@staticmethod
def create_time_stamp():
"""產生時間戳"""
now = time.time()
return int(now)
@staticmethod
def create_nonce_str(length=32):
"""產生隨機字串,不長於32位"""
chars = "abcdefghijklmnopqrstuvwxyz0123456789"
strs = []
for x in range(length):
strs.append(chars[random.randrange(0, len(chars))])
return "".join(strs)
@staticmethod
def xml_to_array(xml):
"""將xml轉為array"""
array_data = {}
root = fromstring(xml)
for child in root:
value = child.text
array_data[child.tag] = value
return array_data
def get_sign(self):
"""生成簽名"""
# 簽名步驟一:按字典序排序引數
key = sorted(self.dic.keys())
buffer = []
for k in key:
buffer.append("{0}={1}".format(k, self.dic[k]))
# self.dic["paySign"] = self.get_sign(jsApiObj)
parm = "&".join(buffer)
# 簽名步驟二:在string後加入KEY
parm = "{0}&key={1}".format(parm, self.config.API_KEY).encode('utf-8')
# 簽名步驟三:MD5加密
signature = hashlib.md5(parm).hexdigest()
# 簽名步驟四:所有字元轉為大寫
result_ = signature.upper()
return result_
def array_to_xml(self, sign_name=None):
"""array轉xml"""
if sign_name is not None:
self.dic[sign_name] = self.get_sign()
xml = ["<xml>"]
for k in self.dic.keys():
xml.append("<{0}>{1}</{0}>".format(k, self.dic[k]))
xml.append("</xml>")
return "".join(xml)
class WechatLogin(WechatAPI):
def get_code_url(self):
"""微信內建瀏覽器獲取網頁授權code的url"""
url = self.config.defaults.get('wechat_browser_code') + (
'?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect' %
(self.config.APPID, parse.quote(self.config.REDIRECT_URI),
self.config.SCOPE, self.config.STATE if self.config.STATE else ''))
return url
def get_code_url_pc(self):
"""pc瀏覽器獲取網頁授權code的url"""
url = self.config.defaults.get('pc_QR_code') + (
'?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect' %
(self.config.APPID, parse.quote(self.config.REDIRECT_URI), self.config.PC_LOGIN_SCOPE,
self.config.STATE if self.config.STATE else ''))
return url
def get_access_token(self, code):
"""獲取access_token"""
params = {
'appid': self.config.APPID,
'secret': self.config.APPSECRET,
'code': code,
'grant_type': 'authorization_code'
}
token, err = self.process_response_login(requests
.get(self.config.defaults.get('wechat_browser_access_token'),
params=params))
if not err:
self._access_token = token['access_token']
self._openid = token['openid']
return self._access_token, self._openid
def get_user_info(self, access_token, openid):
"""獲取使用者資訊"""
params = {
'access_token': access_token,
'openid': openid,
'lang': self.config.LANG
}
return self.process_response_login(requests
.get(self.config.defaults.get('wechat_browser_user_info'), params=params))
class WechatTemplates(WechatAPI):
def __init__(self):
super().__init__()
self.mp_access_token = None
self.mp_expires_in = None
def get_mp_access_token(self):
"""獲取公眾號的access_token"""
# err_code = {
# '-1': '系統繁忙,請稍候再試',
# '0': '請求成功',
# '40001': 'AppSecret錯誤或者AppSecret不屬於這個公眾號,請開發者確認AppSecret的正確性',
# '40002': '請確保grant_type欄位值為client_credential',
# '40164': '呼叫介面的IP地址不在白名單中,請在介面IP白名單中進行設定',
# }
url = self.config.defaults.get('mp_access_token') + (
'?grant_type=%s&appid=%s&secret=%s' %
(self.config.GRANT_TYPE, self.config.APPID,
self.config.APPSECRET))
token_data = eval(requests.get(url).content)
if 'access_token' not in token_data:
return token_data['errcode'], token_data['errmsg'], False
else:
self.mp_access_token = token_data['access_token']
self.mp_expires_in = token_data['expires_in']
return self.mp_access_token, self.mp_expires_in, True
# 以下功能暫不使用
# def change_industry(self):
# """設定所屬行業,每月可修改行業1次"""
# url = self.config.defaults.get('change_industry') + (
# '?access_token=%s' % self.mp_access_token)
# prams = {
# "industry_id1": "23",
# "industry_id2": "31"
# }
# data = requests.post(url, prams)
#
# def get_industry(self):
# """獲取行業資訊"""
# if self.mp_access_token is None:
# _, msg, success = self.get_mp_access_token()
# if not success:
# return msg, False
# url = self.config.defaults.get('get_industry') + (
# '?access_token=%s' % self.mp_access_token)
# industry_data = requests.get(url)
# if 'primary_industry' in industry_data:
# primary_industry = industry_data['primary_industry']
# secondary_industry = industry_data['secondary_industry']
# return primary_industry, secondary_industry, True
# else:
# return '', '獲取行業資訊錯誤', False
#
# def get_templates_id(self):
# pass
#
def send_templates_message(self, touser, template_id, data, url=None, miniprogram=None):
post_data = {
"touser": touser,
"template_id": template_id,
"data": data
}
if url is not None:
post_data['url'] = url
if miniprogram is not None:
post_data['miniprogram'] = miniprogram
url = self.config.defaults.get('send_templates_message') + (
'?access_token=%s' % self.mp_access_token)
back_data = requests.post(url, json=post_data)
print(back_data)
if "errcode" in back_data and back_data["errcode"] == 0:
return True
else:
return False
class WechatPayAPI(WechatAPI):
def __init__(self, package, sign_type=None):
super().__init__()
self.appId = self.config.APPID
self.timeStamp = self.create_time_stamp()
self.nonceStr = self.create_nonce_str()
self.package = package
self.signType = sign_type
self.dic = {"appId": self.appId, "timeStamp": "{0}".format(self.create_time_stamp()),
"nonceStr": self.create_nonce_str(), "package": "prepay_id={0}".format(self.package)}
if sign_type is not None:
self.dic["signType"] = sign_type
else:
self.dic["signType"] = "MD5"
def get_dic(self):
self.dic['paySign'] = self.get_sign()
return self.dic
class WechatOrder(WechatAPI):
def __init__(self, body, trade_type, out_trade_no, total_fee, spbill_create_ip, notify_url, device_info=None,
sign_type=None, attach=None, fee_type=None, time_start=None, time_expire=None, goods_tag=None,
product_id=None, detail=None, limit_pay=None, openid=None, scene_info=None):
super().__init__()
self.device_info = device_info #
self.nonce_str = self.create_nonce_str()
self.sign_type = sign_type #
self.detail = detail #
self.body = body
self.attach = attach #
self.out_trade_no = out_trade_no
self.fee_type = fee_type #
self.total_fee = total_fee
self.spbill_create_ip = spbill_create_ip
self.time_start = time_start #
self.time_expire = time_expire #
self.goods_tag = goods_tag #
self.notify_url = notify_url
self.trade_type = trade_type
self.product_id = product_id #
self.limit_pay = limit_pay #
self.openid = openid #
self.scene_info = scene_info #
self.dic = {"appid": self.config.APPID, "mch_id": self.config.MCH_ID,
"nonce_str": self.nonce_str, "body": self.body,
'out_trade_no': out_trade_no,
'openid': self.openid,
"total_fee": self.total_fee, "spbill_create_ip": self.spbill_create_ip,
"notify_url": self.notify_url,
"trade_type": self.trade_type}
if self.device_info is not None:
self.dic["device_info"] = self.device_info
if self.sign_type is not None:
self.dic["sign_type"] = self.sign_type
if self.detail is not None:
self.dic["detail"] = self.detail
if self.attach is not None:
self.dic["attach"] = self.attach
if self.fee_type is not None:
self.dic["fee_type"] = self.fee_type
if self.time_start is not None:
self.dic["time_start"] = self.time_start
if self.time_expire is not None:
self.dic["time_expire"] = self.time_expire
if self.goods_tag is not None:
self.dic["goods_tag"] = self.goods_tag
if self.product_id is not None:
self.dic["product_id"] = self.product_id
if self.limit_pay is not None:
self.dic["limit_pay"] = self.limit_pay
if self.openid is not None:
self.dic["openid"] = self.openid
if self.scene_info is not None:
self.dic["scene_info"] = self.scene_info
def order_post(self):
if self.config.APPID is None:
return None, True
xml_ = self.array_to_xml('sign')
data = requests.post(self.config.defaults['order_url'], data=xml_.encode('utf-8'),
headers={'Content-Type': 'text/xml'})
return self.process_response_pay(data.content)
上面的WechatOrder類就是支付下單,WechatPayAPI類是支付請求,你看官方文件的支付介面,可能剛開始你會問怎麼呼叫這個介面不傳商品資訊和價格資訊啊,其實這些資訊是在支付下單的時候傳過去的,下單需要的引數如下(根據你的需要填寫非必須的欄位):
名稱 | 變數名 | 必填 | 型別 | 示例值 | 描述 |
---|---|---|---|---|---|
公眾賬號ID | appid | 是 | String(32) | wxd678efh567hg6787 | 微信支付分配的公眾賬號ID(企業號corpid即為此appId),在我的引數檔案的APPID配置 |
商戶號 | mch_id | 是 | String(32) | 1230000109 | 微信支付分配的商戶號,在我的引數檔案的MCH_ID配置 |
裝置號 | device_info | 否 | String(32) | 013467007045764 | 自定義引數,可以為終端裝置號(門店號或收銀裝置ID),PC網頁或公眾號內支付可以傳”WEB” |
隨機字串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 隨機字串,長度要求在32位以內,在我的WechatAPI的create_nonce_str() |
簽名 | sign | 是 | String(32) | C380BEC2BFD727A4B6845133519F3AD6 | 通過簽名演算法計算得出的簽名值,在我的WechatAPI的get_sign() |
簽名型別 | sign_type | 否 | String(32) | MD5 | 簽名型別,預設為MD5,支援HMAC-SHA256和MD5。 |
商品描述 | body | 是 | String(128) | 騰訊充值中心-QQ會員充值 | 商品簡單描述,該欄位請按照規範傳遞,具體請見官方文件 |
商品詳情 | detail | 否 | String(6000) | … | 商品詳細描述,對於使用單品優惠的商戶,改欄位必須按照規範上傳,具體請見官方文件 |
附加資料 | attach | 否 | String(127) | 深圳分店 | 附加資料,在查詢API和支付通知中原樣返回,可作為自定義引數使用。 |
商戶訂單號 | out_trade_no | 是 | String(32) | 20150806125346 | 商戶系統內部訂單號,要求32個字元內,只能是數字、大小寫字母_- |
標價幣種 | fee_type | 否 | String(16) | CNY | 符合ISO 4217標準的三位字母程式碼,預設人民幣:CNY,詳細列表請參見貨幣型別 |
標價金額 | total_fee | 是 | Int | 88 | 訂單總金額,單位為分,詳見支付金額 |
終端IP | spbill_create_ip | 是 | String(16) | 123.12.12.123 | APP和網頁支付提交使用者端ip,Native支付填呼叫微信支付API的機器IP。r沒有獲取到做測試的時候可以直接填127.0.0.1 |
交易起始時間 | time_start | 否 | String(14) | 20091225091010 | 訂單生成時間,格式為yyyyMMddHHmmss,如2009年12月25日9點10分10秒錶示為20091225091010。其他詳見官方文件時間規則 |
交易結束時間 | time_expire | 否 | String(14) | 20091227091010 | 訂單失效時間,格式為yyyyMMddHHmmss,如2009年12月27日9點10分10秒錶示為20091227091010。訂單失效時間是針對訂單號而言的,由於在請求支付的時候有一個必傳引數prepay_id只有兩小時的有效期,所以在重入時間超過2小時的時候需要重新請求下單介面獲取新的prepay_id。其他詳見時間規則。建議:最短失效時間間隔大於1分鐘 |
訂單優惠標記 | goods_tag | 否 | String(32) | WXG | 訂單優惠標記,使用代金券或立減優惠功能時需要的引數,具體請見官方文件 |
通知地址 | notify_url | 是 | String(256) | 非同步接收微信支付結果通知的回撥地址,通知url必須為外網可訪問的url,不能攜帶引數。 | |
交易型別 | trade_type | 是 | String(16) | JSAPI | JSAPI 公眾號支付、NATIVE 掃碼支付、APP APP支付說明詳見引數規定 |
商品ID | product_id | 否 | String(32) | 12235413214070356458058 | trade_type=NATIVE時(即掃碼支付),此引數必傳。此引數為二維碼中包含的商品ID,商戶自行定義。 |
指定支付方式 | limit_pay | 否 | String(32) | no_credit | 上傳此引數no_credit–可限制使用者不能使用信用卡支付 |
使用者標識 | openid | 否 | String(128) | oUpF8uMuAJO_M2pxb1Q9zNjWeS6o | trade_type=JSAPI時(即公眾號支付),此引數必傳,此引數為微信使用者在商戶對應appid下的唯一標識。openid如何獲取,可參考【獲取openid】。企業號請使用【企業號OAuth2.0介面】獲取企業號內成員userid,再呼叫【企業號userid轉openid介面】進行轉換 |
+場景資訊 | scene_info | 否 | String(256) | {“store_info” : {“id”: “SZTX001”,”name”: “騰大餐廳”,”area_code”: “440305”,”address”: “科技園中一路騰訊大廈” }} | 該欄位用於上報場景資訊,目前支援上報實際門店資訊。該欄位為JSON物件資料,物件格式為{“store_info”:{“id”: “門店ID”,”name”: “名稱”,”area_code”: “編碼”,”address”: “地址” }} ,欄位詳細說明請點選行前的+展開 |
接著是我的urls.py檔案中加一個下單支付請求url和支付結果返回回撥url:
from django.conf.urls import url
from src.beauty.main.wechat.apps.index.views import AuthView, GetInfoView, WechatPay
urlpatterns = [
url(r'^$', views.home),
# 支付下單及請求
url(r'^wechatPay$', WechatPay.as_view()),
# 授權請求
url(r'^auth/$', AuthView.as_view()),
# 之前的授權回撥頁面
url(r'^index$', GetInfoView.as_view()),
# 調起支付後返回結果的回撥頁面
url(r'^success$', views.success),
# 這裡我省掉了我的其它頁面
]
然後是我的views.py檔案,我只展示支付和結果返回的view:
from django.contrib.auth import logout, authenticate, login
from django.contrib.auth.backends import ModelBackend
# from django.core import serializers
import json
import requests
import base64
import random
import time
from datetime import datetime, date
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import Q
from django.http import HttpResponse, HttpResponseServerError
from django.shortcuts import render, redirect
# from src.beauty.main.wechat.utils.wechatAPI import WechatAPI
from src.beauty.main.wechat.utils.WechatAPI import WechatLogin, WechatTemplates, WechatOrder, WechatPayAPI
from django.views.generic import View
from django.conf import settings
from django.http import HttpResponseRedirect
class WechatPay(View):
@staticmethod
def post(request):
# 這個if判斷是我傳入的訂單的id,測試的時候沒有傳入,你可以測試的時候去掉這個判斷
if 'order' in request.POST:
# order = request.POST['order']
# order = Order.objects.filter(is_effective=True).filter(uuid=order).first()
body = 'JSP支付測試'
trade_type = 'JSAPI'
import random
rand = random.randint(0, 100)
out_trade_no = 'HSTY3JMKFHGA325' + str(rand)
total_fee = 1
spbill_create_ip = '127.0.0.1'
notify_url = 'http://www.show.netcome.net/success'
order = WechatOrder(body=body,
trade_type=trade_type,
out_trade_no=out_trade_no,
openid=request.session['openid'],
total_fee=total_fee,
spbill_create_ip=spbill_create_ip,
notify_url=notify_url)
datas, error = order.order_post()
if error:
return HttpResponseServerError('get access_token error')
order_data = datas['prepay_id'].encode('iso8859-1').decode('utf-8'),
pay = WechatPayAPI(package=order_data[0])
dic = pay.get_dic()
dic["package"] = "prepay_id=" + order_data[0]
return HttpResponse(json.dumps(dic), content_type="application/json")
def success(request):
# 這裡寫支付結果的操作,重定向
return redirect('/')
最後在你的需要支付頁面的html中新增如下:
<script>
function onBridgeReady(data){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId": data.appId, //公眾號名稱,由商戶傳入
"timeStamp": data.timeStamp, //時間戳,自1970年以來的秒數
"nonceStr": data.nonceStr, //隨機串
"package": data.package, //訂單id,這是微信下單微信生成的訂單id不是你自己的
"signType":"MD5", //微信簽名方式:
"paySign": data.paySign //微信簽名
}, function(res){
if(res.err_msg == "get_brand_wcpay_request:ok" ) {
# 支付成功的跳轉頁面,我這裡跳到了首頁
window.location.href = '/';
} // 使用以上方式判斷前端返回,微信團隊鄭重提示:res.err_msg將在使用者支付成功後返回ok,但並不保證它絕對可靠。
});
}
# 點選支付的響應函式,調起下單請求,並返回支付需要上傳的資料(都在data裡)
$('#wechatPay').click(function(){
var order = $(this).data('parm');
$.ajaxSetup({
data: {csrfmiddlewaretoken: '{{ csrf_token }}'},
});
$.ajax({
type: 'POST',
url: '/wechatPay',
data: {
'order': order
},
dataType: 'json',
success: function (data) {
if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady(data);
}
},
error: function () {
}
});
});
</script>
結語
中間配置有問題歡迎留言,這是我的呼叫結果: