Django微信公众号开发(二)公众号内微信支付

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/youand_me/article/details/79646173

前言

  微信公众号开发又一重要项就是微信支付,如何在微信公众号中或者微信自带的浏览器中实现微信支付呢?这就是本文的目的。
  对于微信支付有几种分类,一种是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)和在微信商户平台设置您的公众号支付支付目录,设置路径:商户平台–>产品中心–>开发配置–>公众号支付–>添加(添加一个支付url,比如你的支付页面是: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) http://www.weixin.qq.com/wxpay/pay.php 异步接收微信支付结果通知的回调地址,通知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>

结语

中间配置有问题欢迎留言,这是我的调用结果:
这里写图片描述

猜你喜欢

转载自blog.csdn.net/youand_me/article/details/79646173