08月18, 2018

小程序微信支付详解与代码示例

微信支付开放模式

微信支付分为两大开放模式,普通模式服务商模式

普通模式就是企业自己申请微信支付,自己看文档,自己开发。

*.png

*.png

服务商模式就是第三方外包公司(专门靠给别人开发软件赚钱的公司)或者有清算资质的银行机构来为有需求的企业或者商户提供微信支付的能力。这里的第三方外包公司和银行机构叫做服务商,有需求的商户或者企业叫做特约商户

*.png

*.png

微信支付推出普通模式很容易理解,主要为了给有技术开发能力的企业和商户提供微信支付功能和服务;推出服务商模式,主要在于开拓市场,一方面借助服务商帮忙推广和扩展市场,另一方面降低微信支付接入的技术门槛。当然,服务商从这个过程中也可以得到好处,包括但不限于微信给的返佣、返点和特约商户给的服务费(如有)等,更多服务商权益请参考微信支付|服务商平台星火计划

关于服务商模式来自用户的评价:

我觉得微信服务商这个挺好的,原来我自己打理公众号,实在是太困难,怎么弄也弄不好,后来就是找的服务商,觉得省心多了,现在就专心做生意就好了。这都是互惠互利的事儿。

微信支付开发流程

这里主要以普通模式的开放模式为例,介绍微信支付的接入和开发流程。

首先,需要注意:微信支付对商户开放的所有面对用户使用的api,都是由appid和mch_id成对使用的。简言之,就是在调用所有微信支付api时,你的appid和mch_id要匹配。

微信支付的申请和接入流程请参考接入申请教程

下面来看微信支付的开发流程,首先奉上官方业务流程时序图:

alt

如图所示,商户系统即为开发者自己的系统,对应开发者服务器后台;微信小程序对应开发者的小程序前端;微信后台对应的微信官方的后台服务器。

商户系统和微信支付系统主要交互流程有以下5步:

  1. 前端小程序代码中调用wx.login接口获取code,调用wx.request发送code到开发者服务器(商户系统),开发者服务器拿code去微信服务器换取用户的openid,参见公共api【小程序登录API】。一般情况下,开发者服务器获取到openid后不会直接传将明文传给前端,会先在后端生成token,token中可以存任何想存的内容,一般是用户的唯一标识和登录状态,或者权限状态之类的一些信息,这个token就是用来前后端加密校验的。
  1. 开发者服务器拿到token将其解析后获得openid或者直接获得openid,携带openid和其他参数向微信服务器请求调用支付统一下单,成功后将微信服务器返回的字段加工后返回给小程序端,包括:nonceStr、package、signType、timeStamp和paySign,其中paySign是要根据微信服务器返回的字段再次签名加密计算出的,参见公共api【统一下单API】

  2. 小程序端接受到后端返回的json对象后,由小程序前端直接使用这些参数调起小程序调起支付API,参见公共api【小程序调起支付API】

  3. 用户支付成功后,根据第2步开发者服务器调用支付统一下单请求时所填的notify_url的接口路径,向此路径发送支付通知,根据通知更新业务数据库数据,参见公共api【支付结果通知API】

  4. 开发者服务器在第4步中,为确保交易的安全,可以向微信服务器发送查询订单的请求,来确保用户确实支付成功;另外其他场景如需与微信服务器核对支付状态,均可使用公共api【查询订单API】

大致流程如上所述,这里简单写了一个前后端的支付demo示例代码,小程序端页面展示图如下:

alt

该示例页面主要由两个button组成,涉及的文件结构如下:

.
├── demo
|   ├── pay
|       ├── pay.wxml
|       └── pay.js
|       └── pay.json
|       └── pay.wxss
|       └── md5.js
|       └── util.js

展示代码(pay.wxml、pay.wxss和pay.json):

<!--pages/demo/pay/pay.wxml-->
<text>下单和支付demo教程</text>
<button size='' type='primary' bindtap='ordersubmit'>下单</button>
<button size='' type='primary' bindtap='orderpay'>支付</button>
/* pages/demo/pay/pay.wxss */
button {margin: 30px;}
{
  "navigationBarBackgroundColor": "#ffffff",
  "navigationBarTextStyle": "black",
  "navigationBarTitleText": "大官人的博客",
  "backgroundColor": "#eeeeee",
  "backgroundTextStyle": "light"
}

如上pay.wxml、pay.wxss和pay.json三个文件代码没啥好解释的,主要用于构造简单的两个按钮的支付示例页面。当用户点击下单按钮的时候会调用wx.login,成功后通过wx.request往后台服务器发请求(带上res.code),如下ordersubmit函数代码所示。

// pages/demo/pay/pay.js
import utilMd5 from '../../utils/md5.js'
import util from '../../utils/util.js'
var appInstance = getApp()

Page({

  /**
   * 页面的初始数据
   */
  data: {
    paypackage: '',
  },

  ordersubmit: function(){
    let self = this
    wx.login({
      success: function (res) {
        if (res.code) {
          //发起网络请求
          wx.request({
            url: 'http://127.0.0.1:8360/v1/pay/ordersubmit',
            data: {
              code: res.code
            },
            method: 'POST',
            success: function (res) {  
              console.log(res)            
              self.package = res.data.data.package
              self.timeStamp = res.data.data.timeStamp + ''
              self.nonceStr = res.data.data.nonceStr
              self.signType = res.data.data.signType
              self.paySign = res.data.data.paySign
            }
          })
        } else {
          console.log('登录失败!' + res.errMsg)
        }
      }
    });

  },

  orderpay: function () { 
    let self = this


    //调用wx.requestPayment(OBJECT)发起微信支付
    wx.requestPayment(
      {
        'timeStamp': self.timeStamp,
        'nonceStr': self.nonceStr,
        'package': self.package,
        'signType': self.signType,
        'paySign': self.paySign,
        'success': function (res) {
          // {errMsg: "requestPayment:ok"}
          console.log(res)
         },
        'fail': function (res) {
          console.log(res)
         },
        'complete': function (res) { }
      })       

  },
})

携带code的请求被开发者服务器后台接收后,后台需要拿code去微信服务器换用户的openid,换到openid后,再由开发者后台调用微信服务器的统一下单接口(https://api.mch.weixin.qq.com/pay/unifiedorder) ,这之前需要做签名的校验,会用到md5等加密算法。发送统一下单请求后,微信服务器也会返回相应的信息,此时也需要对微信服务器的sign值做校验。如果一切顺利,返回小程序端含package=prepay_id=wx2017033010242291fcfe0db70013231072的json对象,供小程序调起支付API。开发者服务器代码如下所示:

// 这里是对应小程序端发送的http://127.0.0.1:8360/demo/ordersubmit的后端接口,采用ThinkJS 3编写(Node.JS优秀国产框架)
const Base = require('./base.js');
const rp = require('request-promise');
const md5 = require('md5');
const xml2js = require('xml2js');

module.exports = class extends Base {
    async ordersubmitAction() {
        const code = this.post('code');

        function random32(){
            let str = '',
            pos = 0,            
            arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];

            for(var i=0; i<32; i++){
                pos = Math.round(Math.random() * (arr.length-1));
                str += arr[pos];
            }
            return str;
        }

        //1、向以下地址发送请求换取openid, session_key和unionid(需关注公众号,且绑定在同一微信开放平台),主要目的为了获得openid
        //https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code

        const loginOptions = {
            method: 'GET',
            url: 'https://api.weixin.qq.com/sns/jscode2session',
            qs: {
                grant_type: 'authorization_code',
                js_code: code,
                secret: think.config('wxapp.secret'),
                appid: think.config('wxapp.appid')
            }
        };

        const loginResult = await rp(loginOptions);
        const loginResultObj = JSON.parse(loginResult);
        const openid = loginResultObj.openid;

        console.log(loginResultObj);

        //2、调用统一下单api
        //https://api.mch.weixin.qq.com/pay/unifiedorder
        const nonce_str = random32();
        const requestParams = {
            appid: think.config('wxapp.appid'),
            mch_id: think.config('wxapp.mch_id'),
            nonce_str: nonce_str,
//            sign:
            sign_type: 'MD5',
            body: '订单编号:010',
            out_trade_no: 'sn010',
            total_fee: parseInt(0.01 * 100),
            spbill_create_ip: '123.12.12.123',
            notify_url: think.config('wxapp.notify_url'),
            trade_type: 'JSAPI',
            openid: openid
        };

        //2.1、用md5加密算法计算sign值
        //第一步:对参数(requestParams)按照key=value的格式,并按照参数名ASCII字典序排序如下:
        //第二步:拼接API密钥:
        let paramStr = '';
        for (let key of Object.keys(requestParams).sort()) {
            paramStr += key + '=' + requestParams[key] +'&';
        }
//        paramStr = paramStr.substring(0,paramStr.length-1);
        paramStr += 'key='+think.config('wxapp.partner_key');


        //第三步:使用sign_type中配置的方式加密,这里是MD5
        requestParams.sign = md5(paramStr).toUpperCase();

        //第四步:将json对象转换成XML
        var builder = new xml2js.Builder({rootName: 'xml'});
        var xml = builder.buildObject(requestParams);
        console.log(xml);

        //2.2、向如下地址发送统一下单请求
        //https://api.mch.weixin.qq.com/pay/unifiedorder

        const orderOptions = {
            method: 'POST',
            url: 'https://api.mch.weixin.qq.com/pay/unifiedorder',
            formData: {
                xml
            }
        };

        const orderResultXml = await rp(orderOptions);
//        const orderResultObj = JSON.parse(orderResult);

        console.log(orderResultXml);

        //3、将微信返回的xml数据转换成json对象,返回到前端
        //3.1、现将异步函数转成同步,再将xml转成json对象
        const parseString = think.promisify(xml2js.parseString, xml2js);
        const orderResultObj = await parseString(orderResultXml);

        console.log(orderResultObj.xml);

        // 需要判断return_code是否正确
        if(!'SUCCESS' === orderResultObj.xml.return_code){
            return this.fail('签名不正确');
        }

        //3.2、校验微信服务器返回的sign值
        let validateStr = '';
        for (let key of Object.keys(orderResultObj.xml).sort()) {
            if('sign' != key){
                validateStr += key + '=' + orderResultObj.xml[key][0] +'&';
            }
        }


        validateStr += 'key='+think.config('wxapp.partner_key');
        console.log(validateStr);
        validateStr = md5(validateStr).toUpperCase();
        console.log(validateStr);


        if(validateStr != orderResultObj.xml.sign[0]){
            return this.fail('微信服务器返回sign不正确');
        }

        //——如果业务结果返回失败,如订单已支付,将返回结果,错误码,错误描述返回前端
        if('FAIL' === orderResultObj.xml.result_code[0]){
            const returnResult = {
                result_code: orderResultObj.xml.result_code[0],
                err_code: orderResultObj.xml.err_code[0],
                err_code_des: orderResultObj.xml.err_code_des[0]
            }
            return this.success(returnResult);
        }

        // ——如果微信返回的sign正确,则计算paysign
        let timeStamp = parseInt(new Date().getTime()/1000);
        let paySignStr= 'appId=' + think.config('wxapp.appid')+ '&nonceStr='+ orderResultObj.xml.nonce_str[0] + '&package=prepay_id='+ orderResultObj.xml.prepay_id[0] + '&signType=MD5&timeStamp=' + timeStamp + '&key=' + think.config('wxapp.partner_key');
        let paySign = md5(paySignStr).toUpperCase();

        //——如果业务结果返回成功,将package和返回结果返回前端
        const returnResult ={
            result_code: orderResultObj.xml.result_code[0],
            nonceStr: orderResultObj.xml.nonce_str[0],
            package:'prepay_id=' + orderResultObj.xml.prepay_id[0],
            signType: 'MD5',
            timeStamp: timeStamp,
            paySign: paySign
        }

        return this.success(returnResult);

    }


    async notifyAction() {
        const returnObj = this.post();

        if('SUCCESS' === returnObj.return_code){
            //1、校验微信服务器返回的sign值
            let validateStr = '';
            for (let key of Object.keys(returnObj.xml).sort()) {
                if('sign' != key && !think.isTrueEmpty(returnObj.xml[key][0])){
                    validateStr += key + '=' + returnObj.xml[key][0] +'&';
                }else if('sign' != key){
                    validateStr += key + '=' + returnObj.xml[key] +'&';
                }
            }
            validateStr += 'key='+think.config('wxapp.partner_key');
            console.log(validateStr);
            validateStr = md5(validateStr).toUpperCase();
            console.log(validateStr);

            if(validateStr != returnObj.xml.sign[0]){
                return this.fail('微信服务器返回sign不正确');
            }

            //2、校验订单金额

        }

        const result = {
            return_code: 'SUCCESS',
            return_msg: 'OK'
        }

        const builder = new xml2js.Builder({rootName: 'xml'});
        const xml = builder.buildObject(result);

        this.ctx.body = xml;
        return;

    }


};

小程序端收到回复后,将调用orderpay函数,进行小程序调起支付API。支付成功后,即会向开发者后端服务器发送支付通知(通知地址由ordersubmitAction接口中填写notify_url决定)。这里的示例对应notifyAction这个地址,开发者后台接到微信服务器发过来的通知后,需要对通知的签名sign做校验,如果sign和订单金额以及状态均没问题,才可以写入数据库。必要时可以调用【查询订单API】进行订单查询。

示例中所涉及的其他文件:

// 工具类util.js
function random32() {
  let str = '',
    pos = 0,
    arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];

  for (var i = 0; i < 32; i++) {
    pos = Math.round(Math.random() * (arr.length - 1));
    str += arr[pos];
  }
  return str;
}

module.exports = {
  random32: random32
}

完整示例代码请浏览: https://github.com/haodalong/daguanren-wxapp-demo (记得给小星星哦)

本文链接:https://www.daguanren.cc/post/wxapp_pay_illustration_example.html

-- EOF --

Comments