微信小程序支付

Posted 朱兆筠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微信小程序支付相关的知识,希望对你有一定的参考价值。

关于微信小程序支付功能,他的官方文档真的写的很清楚了!但是整理吸收一下还是不错的

官网呈上:https://developers.weixin.qq.com/miniprogram/dev/api/wx.requestPayment.html

核心流程:下面的图和交互流程描述写的真是不能再清晰了

商户系统和微信支付系统主要交互:

1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API

2、商户server调用支付统一下单,api参见公共api【统一下单API

3、商户server调用再次签名,api参见公共api【再次签名

4、商户server接收支付通知,api参见公共api【支付结果通知API

5、商户server查询支付结果,api参见公共api【查询订单API

二、根据以上流程一一拆分一下:

(1)小程序内调用登录接口,获取到用户的openid:这个就不多赘述了,凡是做小程序的,肯定会有登录流程,也会存储相关的openid,不是支付的重点,有兴趣异步官网:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html

(2)商户server调用支付统一下单,api参见公共api【统一下单API】详细官网https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1&index=1:这个就有些说道了,不过看完文档就一步了然了,无非是调用微信的接口,传递一些参数,如果要说仔细说的话,就是参数的获取以及加密方式了,下面会一一分解

  1. 统一接口URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder,这个就是统一下单API
  2. 参数描述:参数很多很多,我们只要知道必传字段和一些区别就好,用的时候看一下官网就可以
字段名变量名必填类型示例值描述
小程序ID appid String(32) wxd678efh567hg6787 微信分配的小程序ID
商户号 mch_id String(32) 1230000109 微信支付分配的商户号
设备号 device_info String(32) 013467007045764 自定义参数,可以为终端设备号(门店号或收银设备ID),PC网页或公众号内支付可以传"WEB"
随机字符串 nonce_str String(32) 5K8264ILTKCH16CQ2502SI8ZNMTM67VS 随机字符串,长度要求在32位以内。推荐随机数生成算法
签名 sign String(32) C380BEC2BFD727A4B6845133519F3AD6 通过签名算法计算得出的签名值,详见签名生成算法
签名类型 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和H5支付提交用户端ip,Native支付填调用微信支付API的机器IP。
交易起始时间 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,详细说明见参数规定
商品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】。
电子发票入口开放标识 receipt String(8) Y Y,传入Y时,支付成功消息和支付详情页将出现开票入口。需要在微信支付商户平台或微信公众平台开通电子发票功能,传此字段才可生效

 最后我们要的结果是xml格式

<xml>
   <appid>wx2421b1c4370ec43b</appid>
   <attach>支付测试</attach>
   <body>JSAPI支付测试</body>
   <mch_id>10000100</mch_id>
   <detail><![CDATA[{ "goods_detail":[ { "goods_id":"iphone6s_16G", "wxpay_goods_id":"1001", "goods_name":"iPhone6s 16G", "quantity":1, "price":528800, "goods_category":"123456", "body":"苹果手机" }, { "goods_id":"iphone6s_32G", "wxpay_goods_id":"1002", "goods_name":"iPhone6s 32G", "quantity":1, "price":608800, "goods_category":"123789", "body":"苹果手机" } ] }]]></detail>
   <nonce_str>1add1a30ac87aa2db72f57a2375d8fec</nonce_str>
   <notify_url>http://wxpay.wxutil.com/pub_v2/pay/notify.v2.php</notify_url>
   <openid>oUpF8uMuAJO_M2pxb1Q9zNjWeS6o</openid>
   <out_trade_no>1415659990</out_trade_no>
   <spbill_create_ip>14.23.150.211</spbill_create_ip>
   <total_fee>1</total_fee>
   <trade_type>JSAPI</trade_type>
   <sign>0CB01533B8C1EF103065174F50BCA001</sign>
</xml>

 

借鉴了前人优秀代码整理如下

请求支付,前提已知参数

appid = \'wxe09634ee0e363dcb\'                            # 小程序ID
mch_id = \'1111111111\'                                   # 商户号
total_fee = \'1\'                                         # 总金额
spbill_create_ip = \'111.111.111.11\'                     # 终端IP
notify_url = \'https://abc.abcdefg.com/wxpay/notify\'     # 通知地址
trade_type = \'JSAPI\'                                    # 交易类型
merchant_key = \'00000000000000000000000000000000\'       # 商户KEY

 

class CreatePay(HTTPMethodView):

    async def post(self,request):
        \'\'\'
        请求支付
        :return:
        \'\'\'
        data = {
            \'appid\': appid,  # 微信分配的小程序ID
            \'mch_id\': mch_id,  # 微信支付分配的商户号
            \'nonce_str\': get_nonce_str(),  # uuid获取随机字符串
            \'body\': \'测试\',  # 商品描述
            \'out_trade_no\': str(int(time.time())),  # 商户订单号
            \'total_fee\': \'1\',
            \'spbill_create_ip\': spbill_create_ip,  # 订单总金额,单位为分,详见支付金额
            \'notify_url\': notify_url,  # 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数
            \'attach\': \'{"msg": "北京总店"}\',  # 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
            \'trade_type\': trade_type,  # 小程序取值如下:JSAPI
            \'openid\': \'1111111111111111111111\'  # trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识
        }

        wxpay = WxPay(merchant_key, **data)
        pay_info = wxpay.get_pay_info()
        if pay_info:
            return text(pay_info)  # 把支付相关信息返回给用户确认,用户确认后,前端直接调用下面的微信接口,支付成功后返回用户信息,
        # 并且给商户后台推送消息,商户后台更新支付状态
        """
        wx.requestPayment(
                        {
                        \'timeStamp\': \'\',
                        \'nonceStr\': \'\',
                        \'package\': \'\',
                        \'signType\': \'MD5\',
                        \'paySign\': \'\',
                        \'success\':function(res){},
                        \'fail\':function(res){},
                        \'complete\':function(res){}
                        })
        """
        return text({\'errcode\': 40001, \'errmsg\': \'请求支付失败\'})

WxPayBP.add_route(CreatePay.as_view(),\'createPay\')
View Code

 

调用的方法Utils.py

def get_nonce_str():
    \'\'\'
    获取随机字符串
    :return:
    \'\'\'
    return str(uuid.uuid4()).replace(\'-\', \'\')


def dict_to_xml(dict_data):
    \'\'\'
    dict to xml
    :param dict_data:
    :return:
    <xml>

    <appid>wxd930ea5d5a258f4f</appid>

    <mch_id>10000100</mch_id>

    <device_info>1000</device_info>

    <body>test</body>

    <nonce_str>ibuaiVcKdpRxkhJA</nonce_str>

    <sign>9A0A8659F005D6984697E2CA0A9CF3B7</sign>

    </xml>
    :description:
    微信服务器与商户服务器之间采用 XML 格式进行交互,这就涉及到与语言原生数据类型进行转换以方便处理。
    交互的数据参数都是 key-value 的形式,因此在 Python 中使用字典会更加方便。
    而要解析 XML,也有一大把第三方库供使用,比如 BeautifulSoup。

    \'\'\'
    xml = ["<xml>"]
    for k, v in dict_data.iteritems():
        xml.append("<{0}>{1}</{0}>".format(k, v))
    xml.append("</xml>")
    return "".join(xml)


def xml_to_dict(xml_data):
    \'\'\'
    xml to dict
    :param xml_data:
    :return:
    \'\'\'
    xml_dict = {}
    root = ET.fromstring(xml_data)
    for child in root:
        xml_dict[child.tag] = child.text
    return xml_dict


class WxPay(object):

    def __init__(self, merchant_key, *args, **kwargs):
        self.url = \'https://api.mch.weixin.qq.com/pay/unifiedorder\' # 统一下单接口
        self.merchant_key = merchant_key  # 商户平台设置的密钥key
        self.pay_data = kwargs  # 下单参数
        super(WxPay, self).__init__()


    def create_sign(self, pay_data):
        \'\'\'
        生成签名
        :return:
        Reference document:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3
        第一步:对参数按照key=value的格式,并按照参数名ASCII字典序排序如下:

        stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";

        第二步:拼接API密钥:

        stringSignTemp=stringA+"&key=192006250b4c09247ec02edce69f6a2d" //注:key为商户平台设置的密钥key

        sign=MD5(stringSignTemp).toUpperCase()="9A0A8659F005D6984697E2CA0A9CF3B7" //注:MD5签名方式

        sign=hash_hmac("sha256",stringSignTemp,key).toUpperCase()="6A9AE1657590FD6257D693A078E1C3E4BB6BA4DC30B23E0EE2496E54170DACD6" //注:HMAC-SHA256签名方式
        \'\'\'
        stringA = \'&\'.join(["{0}={1}".format(k, pay_data.get(k))for k in sorted(pay_data)])
        stringSignTemp = \'{0}&key={1}\'.format(stringA, self.merchant_key)
        sign = hashlib.md5(stringSignTemp).hexdigest()  # 选择md5签名方式
        return sign.upper()

    def get_pay_info(self):
        \'\'\'
        获取支付信息
        :param xml_data:
        :return:
        \'\'\'
        sign = self.create_sign(self.pay_data)  # 生成签名
        self.pay_data[\'sign\'] = sign  # 更新统一下单接口参数,加上签名
        xml_data = dict_to_xml(self.pay_data)  #
        response = request(url=self.url, data=xml_data)
        """
                返回数据类型
                <xml>
                   <return_code><![CDATA[SUCCESS]]></return_code>
                   <return_msg><![CDATA[OK]]></return_msg>
                   <appid><![CDATA[wx2421b1c4370ec43b]]></appid>
                   <mch_id><![CDATA[10000100]]></mch_id>
                   <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
                   <openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid>
                   <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
                   <result_code><![CDATA[SUCCESS]]></result_code>
                   <prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id>
                   <trade_type><![CDATA[JSAPI]]></trade_type>
                </xml>
                """
        #  以下字段在return_code 和result_code都为SUCCESS的时候有返回 prepay_id
        if response:
            prepay_id = xml_to_dict(response).get(\'prepay_id\')
            paySign_data = {
                \'appId\': self.pay_data.get(\'appid\'),
                \'timeStamp\': self.pay_data.get(\'out_trade_no\'),
                \'nonceStr\': self.pay_data.get(\'nonce_str\'),
                \'package\': \'prepay_id={0}\'.format(prepay_id),
                \'signType\': \'MD5\'
            }
            # 再次签名
            """
            paySign = MD5(appId=wxd678efh567hg6787&nonceStr=5K8264ILTKCH16CQ2502SI8ZNMTM67VS
            &package=prepay_id=wx2017033010242291fcfe0db70013231072
            &signType=MD5
            &timeStamp=1490840662
            &key=qazwsxedcrfvtgbyhnujmikolp111111) = 22D9B4E54AB1950F51E0649E8810ACD6
            """
            paySign = self.create_sign(paySign_data)
            paySign_data.pop(\'appId\')
            paySign_data[\'paySign\'] = paySign
            return paySign_data

        return False
View Code

 

代码里需要注意一下就是create_sign函数,是按照官方文档要求,生成的签名 :https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3

举例:

(2.1)假设传送的参数如下:

appid:    wxd930ea5d5a258f4f
mch_id:    10000100
device_info:    1000
body:    test
nonce_str:    ibuaiVcKdpRxkhJA

 

(2.2)

第一步:对参数按照key=value的格式,并按照参数名ASCII字典序排序如下:

stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";

 

第二步:拼接API密钥:

stringSignTemp=stringA+"&key=192006250b4c09247ec02edce69f6a2d" //注:key为商户平台设置的密钥key

sign=MD5(stringSignTemp).toUpperCase()="9A0A8659F005D6984697E2CA0A9CF3B7" //注:MD5签名方式

sign=hash_hmac("sha256",stringSignTemp,key).toUpperCase()="6A9AE1657590FD6257D693A078E1C3E4BB6BA4DC30B23E0EE2496E54170DACD6" //注:HMAC-SHA256签名方式

 

(2.3)最终得到最终发送的数据:

<xml>

<appid>wxd930ea5d5a258f4f</appid>

<mch_id>10000100</mch_id>

<device_info>1000</device_info>

<body>test</body>

<nonce_str>ibuaiVcKdpRxkhJA</nonce_str>

<sign>9A0A8659F005D6984697E2CA0A9CF3B7</sign>

</xml>

 

 最后整理出来就是Utils.py中create_sign函数

    def create_sign(self, pay_data):
        \'\'\'
        生成签名
        :pay_data:{
            \'appid\': appid,  # 微信分配的小程序ID
            \'mch_id\': mch_id,  # 微信支付分配的商户号
            \'nonce_str\': get_nonce_str(),  # uuid获取随机字符串
            \'body\': \'测试\',  # 商品描述
            \'out_trade_no\': str(int(time.time())),  # 商户订单号
            \'total_fee\': \'1\',
            \'spbill_create_ip\': spbill_create_ip,  # 订单总金额,单位为分,详见支付金额
            \'notify_url\': notify_url,  # 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数
            \'attach\': \'{"msg": "北京总店"}\',  # 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
            \'trade_type\': trade_type,  # 小程序取值如下:JSAPI
            \'openid\': \'1111111111111111111111\'  # trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识
            "sign":""
        }
        :return:
        Reference document:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=4_3
        第一步:对参数按照key=value的格式,并按照参数名ASCII字典序排序如下:

        stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";

        第二步:拼接API密钥:

        stringSignTemp=stringA+"&key=192006250b4c09247ec02edce69f6a2d" //注:key为商户平台设置的密钥key

        sign=MD5(stringSignTemp).toUpperCase()="9A0A8659F005D6984697E2CA0A9CF3B7" //注:MD5签名方式

        sign=hash_hmac("sha256",stringSignTemp,key).toUpperCase()="6A9AE1657590FD6257D693A078E1C3E4BB6BA4DC30B23E0EE2496E54170DACD6" //注:HMAC-SHA256签名方式
        \'\'\'
        stringA = \'&\'.join(["{0}={1}".format(k, pay_data.get(k))for k in sorted(pay_data)])
        stringSignTemp = \'{0}&key={1}\'.format(stringA, self.merchant_key)
        sign = hashlib.md5(stringSignTemp).hexdigest()  # 选择md5签名方式
        return sign.upper()

 

(3) 商户server调用再次签名,api参见公共api【再次签名

  1. 小程序调起支付数据签名字段列表:方式跟上面统一下单接口签名方法一样,只是参数不一样罢了

    字段名变量名必填类型示例值描述
    小程序ID appId String wxd678efh567hg6787 微信分配的小程序ID
    时间戳 timeStamp String 1490840662 时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间
    随机串 nonceStr String 5K8264ILTKCH16CQ2502SI8ZNMTM67VS 随机字符串,不长于32位。推荐随机数生成算法
    数据包 package String prepay_id=wx2017033010242291fcfe0db70013231072 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=wx2017033010242291fcfe0db70013231072
    签名方式 signType String MD5 签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致
  2. 举例如下:

    paySign = MD5(appId=wxd678efh567hg6787&nonceStr=5K8264ILTKCH16CQ2502SI8ZNMTM67VS&package=prepay_id=wx2017033010242291fcfe0db70013231072&signType=MD5&timeStamp=1490840662&key=qazwsxedcrfvtgbyhnujmikolp111111) = 22D9B4E54AB1950F51E0649E8810ACD6

(4)、商户server接收支付通知,api参见公共api【支付结果通知API】:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_7

    支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。

    接口链接:

    该链接是通过【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。

    通知url必须为直接可访问的url,不能携带参数。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action”

(5)、商户server查询支付结果,api参见公共api【查询订单API】:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_2

后续补充.........

以上是关于微信小程序支付的主要内容,如果未能解决你的问题,请参考以下文章

微信小程序-云支付那些事儿

微信小程序实现支付功能

微信小程序支付及退款流程详解

微信小程序支付成功后跳转APP卡死

PHP实现微信小程序支付

微信小程序实现微信支付功能(可用)