Skip to content
目录

微信小程序支付

1、支付流程

大概流程就是:

  • 前端请求后端支付接口,并把登录的 code(因为是自己测试所以要把 code 传给后端,用来获取用户的 openID)和支付的 money 传给后端;
  • 后端调用小程序的登录 API 获取用户的 openID,然后调用统一下单 API,把必要的参数传过去,就可以获取预付单信息(prepay_id),
  • 然后获取当前的时间搓(timeStamp),整合预付单信息(prepay_id,参数是 packpage)、随机字符串(nonceStr)、支付签名(paySign),签名算法(signType)一起返回给前端就可以。
  • 最后前端调用 wx.requestPayment 接口,并把后端返回的数据一起传给微信就可以发起支付了。

支付业务流程图

示例:

json
{
  "appId": "wx2fefab******256ef0",
  "nonceStr": "crf3pkxsezcdx****ps7s89ygo15nveu",
  "package": "prepay_id=wx191016483***503b1440142100",
  "signType": "MD5",
  "timeStamp": "1560910608",
  "paySign": "B4354A3583B****028F97A8896"
}

2、基础配置

js
    appId: 'wxb5c6********10b',           // 小程序ID
    secret: 'fd3a92*******56443ca',       // 小程序Secret
    merchantKey: '1bb294c********2a0d487',//商户API密钥
    merchantId: '153******431'            //商户ID

3、小程序支付接口

小程序支付 API

js
wx.requestPayment({
  timeStamp: '', //时间戳
  nonceStr: '', //随机字符串,长度为32个字符以下
  package: '', //统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=***
  signType: '', //签名算法 默认MD5
  paySign: '', //签名,具体签名方案参见
  success(res) {},
  fail(res) {},
});

signType 直接写死 Md5,timeStamp 也容易获取到,那主要有nonceStr,package,paySign需要后端生成。

4、生成 nonceStr 和 timeStamp

js
// 随机字符串,不长于32位。推荐随机数生成算法
const createNonceStr = () => Math.random().toString(36).substr(2, 15);

const createTimestamp = () => parseInt(+new Date() / 1000).toString();

5、生成 package 和 paySign

paySign 生成举例
签名算法

通过统一下单接口,生成 package 和 paySign,调用统一下单接口之前要生成一个统一下单签名,调用接口后在二次签名生成paySign

工具方法:

js
const config = require('./config');
const crypto = require('crypto');
const parseString = require('xml2js').parseString;
const wxpay = {
  // 随机字符串,不长于32位。推荐随机数生成算法
  createNonceStr: () => Math.random().toString(36).substr(2, 15),
  //时间戳秒
  createTimestamp: () => parseInt(+new Date() / 1000).toString(),
  //生成统一下单签名
  paysignjsapi: (obj) => {
    let str = wxpay.raw(obj);
    str = str + '&key=' + config.merchantKey;
    return crypto.createHash('md5').update(str).digest('hex').toUpperCase();
  },
  //小程序paySign
  paysign: (obj) => {
    let str = wxpay.raw1(obj);
    str = str + '&key=' + config.merchantKey;
    return crypto.createHash('md5').update(str).digest('hex');
  },
  //非空参数值的参数按照参数名ASCII码从小到大排序(字典序)
  raw: (args) => {
    let keys = Object.keys(args).sort();
    let newArgs = {};
    keys.forEach((key) => (newArgs[key.toLowerCase()] = args[key]));
    let str = '';
    for (let k in newArgs) {
      str += '&' + k + '=' + newArgs[k];
    }
    return str.substr(1);
  },
  //二次签名时候的参数不需要转换为小写的
  raw1: (args) => {
    let keys = Object.keys(args).sort();
    let newArgs = {};
    keys.forEach((key) => (newArgs[key] = args[key]));
    let str = '';
    for (let k in newArgs) {
      str += '&' + k + '=' + newArgs[k];
    }
    return str.substr(1);
  },

  //商品金额转分
  getmoney: (num, precision = 12) =>
    +parseFloat((num * 100).toPrecision(precision)),
  //发送xml格式数据
  getfromData: (obj) => {
    let formData = `<xml>
                            <appid>${obj.appid}</appid>
                            <attach>${obj.attach}</attach>
                            <body>${obj.body}</body>
                            <mch_id>${obj.mch_id}</mch_id>
                            <nonce_str>${obj.nonce_str}</nonce_str>
                            <notify_url>${obj.notify_url}</notify_url>
                            <openid>${obj.openid}</openid>
                            <out_trade_no>${obj.out_trade_no}</out_trade_no>
                            <spbill_create_ip>${obj.spbill_create_ip}</spbill_create_ip>
                            <total_fee>${obj.total_fee}</total_fee>
                            <trade_type>${obj.trade_type}</trade_type>
                            <sign>${obj.sign}</sign>
                        </xml>`;
    return formData;
  },
  //获取请求的IP
  getClientIp: (request) => {
    let ip =
      request.headers['x-forwarded-for'] ||
      request.ip ||
      request.connection.remoteAddress ||
      request.socket.remoteAddress ||
      request.connection.socket.remoteAddress ||
      '';
    if (ip.split(',').length > 0) {
      ip = ip.split(',')[0];
    }
    return ip;
  },
  // 支付成功后再次验证签名
  payResultSign: (result) => {
    const ret = {
      appid: result.appid,
      attach: result.attach,
      bank_type: result.bank_type,
      cash_fee: result.cash_fee,
      fee_type: result.fee_type,
      is_subscribe: result.is_subscribe,
      mch_id: result.mch_id,
      nonce_str: result.nonce_str,
      openid: result.openid,
      out_trade_no: result.out_trade_no,
      result_code: result.result_code,
      return_code: result.return_code,
      time_end: result.time_end,
      total_fee: result.total_fee,
      trade_type: result.trade_type,
      transaction_id: result.transaction_id,
    };
    let string = wxpay.raw1(ret);
    //key为在微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置
    string = string + '&key=' + config.merchantKey;
    return crypto
      .createHash('md5')
      .update(string, 'utf8')
      .digest('hex')
      .toUpperCase();
  },
  //解析支付结果
  notifyResult: (ctx) =>
    new Promise((resolve, reject) => {
      try {
        let data = '';
        ctx.req.on('data', (chunk) => {
          data += chunk;
        });
        ctx.req.on('end', () => {
          parseString(data, { explicitArray: false }, (err, result) => {
            ctx.logger.info('解析结果成功了');
            ctx.logger.info(result);
            resolve(result.xml);
          });
        });
      } catch (error) {
        ctx.logger.info('解析结果失败了');
        ctx.logger.info(error);
        reject(error);
      }
    }),
  //支付回调结果通知给微信
  notifyTowx: (obj) => {
    if (obj.return_code == 'SUCCESS') {
      return '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
    } else {
      return '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[FAIL]]></return_msg></xml>';
    }
  },
};
module.exports = wxpay;

生成统一下单签名 paysignjsapi 还需要获取用户的 openID 参数
主要是要调用接口

js
https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code

主要代码逻辑:

js
'use strict';
var xml2js = require('xml2js');
const Controller = require('../core/base_controller');
const fly = require('flyio');
const { config } = require('../utils/config');
const parseString = require('xml2js').parseString;
const {
  paysignjsapi,
  createNonceStr,
  getmoney,
  getfromData,
  paysign,
  createTimestamp,
  getClientIp,
} = require('../utils/index');

class WxpayController extends Controller {
  //微信支付
  async orderWxpay() {
    const params = this.ctx.request.body;
    const reqUrl = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
    const ip = getClientIp(this.ctx.request);
    console.log('请求的IP');
    console.log(ip);
    console.log(this.ctx.request);
    const payObj = {
      //签名
      appid: config.appId,
      body: '微信支付,商品详细描述',
      mch_id: config.merchantId,
      nonce_str: createNonceStr(),
      notify_url: 'https://www.XXX.com',
      openid: params.openId,
      out_trade_no: params.orderId,
      spbill_create_ip: ip,
      total_fee: getmoney(params.money),
      trade_type: 'JSAPI',
    };
    //返回微信小程序的请求结果
    const xcxParams = {
      code: 0,
      data: null,
      msg: '',
    };
    //统一下单签名
    payObj.sign = paysignjsapi(payObj);
    //发送XML数据
    const fromData = getfromData(payObj);
    console.log(fromData);
    await fly.request(reqUrl, fromData, { method: 'POST' }).then((res) => {
      console.log('支付');
      console.log(res.response.statusCode);
      if (res.response.statusCode === 200) {
        parseString(res.response.body, (err, result) => {
          console.log(err);
          console.log(result);
          if (result.xml.result_code[0] === 'SUCCESS') {
            xcxParams.code = 1;
            xcxParams.msg = '获取成功';
            //调用成功后,在二次签名生成小程序的paySign
            xcxParams.data.appId = result.xml.appid[0];
            xcxParams.data.nonceStr = result.xml.nonce_str[0];
            xcxParams.data.package = `prepay_id=${result.xml.prepay_id[0]}`;
            xcxParams.data.signType = 'MD5';
            xcxParams.data.timeStamp = createTimestamp();
            console.log('未二次签名钱');
            console.log(xcxParams);
            xcxParams.data.paySign = paysign(xcxParams.data);
            console.log('二次签名后');
            console.log(xcxParams);
          } else {
            xcxParams.code = 0;
            xcxParams.data = null;
            xcxParams.msg = `${result.xml.err_code_des[0]}-订单ID${params.orderId}`;
          }
        });
      }
    });
    this.ctx.body = xcxParams;
  }
}

module.exports = WxpayController;

返回的给前端这些支付参数,就可以发起支付了。

6、支付回调 notify_url

上面只是成功发起了支付,用户有没有支付,钱有没有到账,还需要一个支付回调的接口,异步处理支付结果通知

[坑一]
使用this.ctx.request一直接受不到回调数据,不是按着正常 post 发过来的,用 stream 获取要用,req 监听获取 node 原始请求数据。
[坑二]
把通知的结果在返还给为微信,不然微信会一直重复调用支付通知接口,会掉 10 次. 执行顺序有问题。

支付回调接口:

js
//微信支付回调
    async wxpayNotify() {
        /* { xml:
            { appid: 'wxb5c6c3c7812ae10b',
                attach: 'course',
                bank_type: 'CFT',
                cash_fee: '1',
                fee_type: 'CNY',
                is_subscribe: 'N',
                mch_id: '1538354631',
                nonce_str: 'n6yvy15s1td',
                openid: 'omrS15GeN1iBsL-kpALggFh5LGPM',
                out_trade_no: '5d0c9d0818c8a1159efe1936',
                result_code: 'SUCCESS',
                return_code: 'SUCCESS',
                sign: '8C1C6FD8535C2240232A91060FABBCFB',
                time_end: '20190621170219',
                total_fee: '1',
                trade_type: 'JSAPI',
                transaction_id: '4200000320201906213026967473' }
             } */
        //解析数据
        let notifyXML = '',
            resultTowx = '',
            money = 0,
            orderInfo = {};
        await wxpay.notifyResult(this.ctx).then(res => (notifyXML = res));
        //签名校验
        const signed = wxpay.payResultSign(notifyXML) === notifyXML.sign;
        //如果支付成功
        if (
            signed &&
            notifyXML.result_code === 'SUCCESS' &&
            notifyXML.return_code === 'SUCCESS'
        ) {
                resultTowx = wxpay.notifyTowx(notifyXML);
                //处理支付成功后的业务逻辑
            }
        }
        this.ctx.logger.info('返回微信的结果');
        this.ctx.logger.info(resultTowx);
        this.ctx.body = resultTowx;
    }

node egg 框架处理微信支付成功回调 xml 数据
基于 node 实现微信支付功能
egg 之微信支付微信小程序支付服务端实现——nodejs
微信支付接口签名校验工具