前言
微信公眾號開發(fā)又一重要項就是微信支付,如何在微信公眾號中或者微信自帶的瀏覽器中實現(xiàn)微信支付呢?這就是本文的目的。
對于微信支付有幾種分類,一種是app支付(顧名思義給予一些軟件app使用的)、微信內(nèi)H5支付(什么意思呢,就是微信內(nèi)置的瀏覽器自帶了一些js、css文件,當然是微信特有的,上篇博客有講過,獲取到的用戶昵稱帶表情會亂碼,在微信自帶的瀏覽器你只要將它裝換為utf-8就能正常顯示,原因是內(nèi)置瀏覽器有對應的font文件,所以在內(nèi)置瀏覽器中也有包含了支付功能的js文件,沒錯在微信自帶的瀏覽器中你不用在你的html中引入js都可以使用它的內(nèi)置js,本文會講解如何用它的js調起支付)、其它PC端瀏覽器的掃碼支付。
開發(fā)流程
www.show.netcome.net
。配置公眾號收集信息
首先需要一個有微信支付權限和網(wǎng)頁授權權限的公眾號,其次需要一個有微信支付權限的商戶號(商戶號就是支付的錢到哪里)。同樣,登錄公眾平臺在開發(fā)–>基本配置–>公眾號開發(fā)信息里找到公眾號的開發(fā)者ID(AppID)和開發(fā)者密碼(AppSecret) ,然后在微信商戶平臺里找到mch_id和api密鑰。
注意:必須先在微信公眾平臺設置網(wǎng)頁授權域名(這個域名你就填服務器的www.show.netcome.net
)和在微信商戶平臺設置您的公眾號支付支付目錄,設置路徑:商戶平臺–>產(chǎn)品中心–>開發(fā)配置–>公眾號支付–>添加(添加一個支付url,比如你的支付頁面是:www.show.netcome.net/payment
)。公眾號支付在請求支付的時候會校驗請求來源是否有在商戶平臺做了配置,所以必須確保支付目錄已經(jīng)正確的被配置,否則將驗證失敗,請求支付不成功。
開發(fā)流程
微信支付不是說一開始就傳訂單編號、價格、商品等信息去調用支付的,在調用支付接口前我們需要先去向微信發(fā)起下單請求,只有發(fā)起下單成功后才能調用支付接口。
首先配置你的公眾號、商戶和回調頁面信息,其它值做相應修改,參數(shù)文件如下:
# -*- coding: utf-8 -*-# ----------------------------------------------# @Time : 18-3-21 上午11:50# @Author : YYJ# @File : wechatConfig.py# @CopyRight: ZDWL# ----------------------------------------------"""微信公眾號和商戶平臺信息配置文件"""# ----------------------------------------------微信公眾號---------------------------------------------- ## 公眾號idAPPID = 'appid'# 公眾號AppSecretAPPSECRET = 'appscrect'# ----------------------------------------------微信商戶平臺---------------------------------------------- ## 商戶idMCH_ID = 'mc_id'# 商戶API秘鑰API_KEY = 'api秘鑰'# ----------------------------------------------回調頁面---------------------------------------------- ## 用戶授權獲取code后的回調頁面,如果需要實現(xiàn)驗證登錄就必須填寫REDIRECT_URI = 'http://meili.netcome.net/index'PC_LOGIN_REDIRECT_URI = 'http://meili.netcome.net/index'defaults = { # 微信內(nèi)置瀏覽器獲取code微信接口 'wechat_browser_code': 'https://open.weixin.qq.com/connect/oauth2/authorize', # 微信內(nèi)置瀏覽器獲取access_token微信接口 'wechat_browser_access_token': 'https://api.weixin.qq.com/sns/oauth2/access_token', # 微信內(nèi)置瀏覽器獲取用戶信息微信接口 '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', # 設置公眾號行業(yè)接口 'change_industry': 'https://api.weixin.qq.com/cgi-bin/template/api_set_industry', # 獲取公眾號行業(yè)接口 'get_industry': 'https://api.weixin.qq.com/cgi-bin/template/get_industry', # 發(fā)送模板信息接口 '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'
下面就是支付下單和支付接口調用的封裝代碼了,其中包括了上一篇博客的授權登錄代碼和下一篇博客的發(fā)送模板消息的代碼封裝:
# -*- coding: utf-8 -*-# ----------------------------------------------# @Time : 18-3-21 下午1:36# @Author : YYJ# @File : WechatAPI.py# @CopyRight: ZDWL# ----------------------------------------------import hashlibimport randomimport timefrom urllib import parsefrom xml.etree.ElementTree import fromstringimport requestsfrom src.beauty.main.wechat.config import wechatConfigclass 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數(shù)據(jù),返回相對應的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數(shù)據(jù),返回相對應的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(): """產(chǎn)生時間戳""" now = time.time() return int(now) @staticmethod def create_nonce_str(length=32): """產(chǎn)生隨機字符串,不長于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): """生成簽名""" # 簽名步驟一:按字典序排序參數(shù) 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): """微信內(nèi)置瀏覽器獲取網(wǎng)頁授權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瀏覽器獲取網(wǎng)頁授權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': '系統(tǒng)繁忙,請稍候再試', # '0': '請求成功', # '40001': 'AppSecret錯誤或者AppSecret不屬于這個公眾號,請開發(fā)者確認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): # """設置所屬行業(yè),每月可修改行業(yè)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): # """獲取行業(yè)信息""" # 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 '', '獲取行業(yè)信息錯誤', 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 Falseclass 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.dicclass 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類是支付請求,你看官方文檔的支付接口,可能剛開始你會問怎么調用這個接口不傳商品信息和價格信息啊,其實這些信息是在支付下單的時候傳過去的,下單需要的參數(shù)如下(根據(jù)你的需要填寫非必須的字段):
名稱 | 變量名 | 必填 | 類型 | 示例值 | 描述 |
---|---|---|---|---|---|
公眾賬號ID | appid | 是 | String(32) | wxd678efh567hg6787 | 微信支付分配的公眾賬號ID(企業(yè)號corpid即為此appId),在我的參數(shù)文件的APPID配置 |
商戶號 | mch_id | 是 | String(32) | 1230000109 | 微信支付分配的商戶號,在我的參數(shù)文件的MCH_ID配置 |
設備號 | device_info | 否 | String(32) | 013467007045764 | 自定義參數(shù),可以為終端設備號(門店號或收銀設備ID),PC網(wǎng)頁或公眾號內(nèi)支付可以傳”WEB” |
隨機字符串 | nonce_str | 是 | String(32) | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | 隨機字符串,長度要求在32位以內(nèi),在我的WechatAPI的create_nonce_str() |
簽名 | sign | 是 | String(32) | C380BEC2BFD727A4B6845133519F3AD6 | 通過簽名算法計算得出的簽名值,在我的WechatAPI的get_sign() |
簽名類型 | sign_type | 否 | String(32) | MD5 | 簽名類型,默認為MD5,支持HMAC-SHA256和MD5。 |
商品描述 | body | 是 | String(128) | 騰訊充值中心-QQ會員充值 | 商品簡單描述,該字段請按照規(guī)范傳遞,具體請見官方文檔 |
商品詳情 | detail | 否 | String(6000) | … | 商品詳細描述,對于使用單品優(yōu)惠的商戶,改字段必須按照規(guī)范上傳,具體請見官方文檔 |
附加數(shù)據(jù) | attach | 否 | String(127) | 深圳分店 | 附加數(shù)據(jù),在查詢API和支付通知中原樣返回,可作為自定義參數(shù)使用。 |
商戶訂單號 | out_trade_no | 是 | String(32) | 20150806125346 | 商戶系統(tǒng)內(nèi)部訂單號,要求32個字符內(nèi),只能是數(shù)字、大小寫字母_- |
標價幣種 | fee_type | 否 | String(16) | CNY | 符合ISO 4217標準的三位字母代碼,默認人民幣:CNY,詳細列表請參見貨幣類型 |
標價金額 | total_fee | 是 | Int | 88 | 訂單總金額,單位為分,詳見支付金額 |
終端IP | spbill_create_ip | 是 | String(16) | 123.12.12.123 | APP和網(wǎng)頁支付提交用戶端ip,Native支付填調用微信支付API的機器IP。r沒有獲取到做測試的時候可以直接填127.0.0.1 |
交易起始時間 | time_start | 否 | String(14) | 20091225091010 | 訂單生成時間,格式為yyyyMMddHHmmss,如2009年12月25日9點10分10秒表示為20091225091010。其他詳見官方文檔時間規(guī)則 |
交易結束時間 | time_expire | 否 | String(14) | 20091227091010 | 訂單失效時間,格式為yyyyMMddHHmmss,如2009年12月27日9點10分10秒表示為20091227091010。訂單失效時間是針對訂單號而言的,由于在請求支付的時候有一個必傳參數(shù)prepay_id只有兩小時的有效期,所以在重入時間超過2小時的時候需要重新請求下單接口獲取新的prepay_id。其他詳見時間規(guī)則。建議:最短失效時間間隔大于1分鐘 |
訂單優(yōu)惠標記 | goods_tag | 否 | String(32) | WXG | 訂單優(yōu)惠標記,使用代金券或立減優(yōu)惠功能時需要的參數(shù),具體請見官方文檔 |
通知地址 | notify_url | 是 | String(256) | http://www.weixin.qq.com/wxpay/pay.php | 異步接收微信支付結果通知的回調地址,通知url必須為外網(wǎng)可訪問的url,不能攜帶參數(shù)。 |
交易類型 | trade_type | 是 | String(16) | JSAPI | JSAPI 公眾號支付、NATIVE 掃碼支付、APP APP支付說明詳見參數(shù)規(guī)定 |
商品ID | product_id | 否 | String(32) | 12235413214070356458058 | trade_type=NATIVE時(即掃碼支付),此參數(shù)必傳。此參數(shù)為二維碼中包含的商品ID,商戶自行定義。 |
指定支付方式 | limit_pay | 否 | String(32) | no_credit | 上傳此參數(shù)no_credit–可限制用戶不能使用信用卡支付 |
用戶標識 | openid | 否 | String(128) | oUpF8uMuAJO_M2pxb1Q9zNjWeS6o | trade_type=JSAPI時(即公眾號支付),此參數(shù)必傳,此參數(shù)為微信用戶在商戶對應appid下的唯一標識。openid如何獲取,可參考【獲取openid】。企業(yè)號請使用【企業(yè)號OAuth2.0接口】獲取企業(yè)號內(nèi)成員userid,再調用【企業(yè)號userid轉openid接口】進行轉換 |
+場景信息 | scene_info | 否 | String(256) | {“store_info” : {“id”: “SZTX001”,”name”: “騰大餐廳”,”area_code”: “440305”,”address”: “科技園中一路騰訊大廈” }} | 該字段用于上報場景信息,目前支持上報實際門店信息。該字段為JSON對象數(shù)據(jù),對象格式為{“store_info”:{“id”: “門店ID”,”name”: “名稱”,”area_code”: “編碼”,”address”: “地址” }} ,字段詳細說明請點擊行前的+展開 |
接著是我的urls.py文件中加一個下單支付請求url和支付結果返回回調url:
from django.conf.urls import urlfrom src.beauty.main.wechat.apps.index.views import AuthView, GetInfoView, WechatPayurlpatterns = [ 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, loginfrom django.contrib.auth.backends import ModelBackend# from django.core import serializersimport jsonimport requestsimport base64import randomimport timefrom datetime import datetime, datefrom django.core.serializers.json import DjangoJSONEncoderfrom django.db.models import Qfrom django.http import HttpResponse, HttpResponseServerErrorfrom django.shortcuts import render, redirect# from src.beauty.main.wechat.utils.wechatAPI import WechatAPIfrom src.beauty.main.wechat.utils.WechatAPI import WechatLogin, WechatTemplates, WechatOrder, WechatPayAPIfrom django.views.generic import Viewfrom django.conf import settingsfrom django.http import HttpResponseRedirectclass 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年以來的秒數(shù) "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,但并不保證它絕對可靠。 }); } # 點擊支付的響應函數(shù),調起下單請求,并返回支付需要上傳的數(shù)據(jù)(都在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>
結語
中間配置有問題歡迎留言,這是我的調用結果: