微信小程序支付服务端实现——nodejs

403 阅读4分钟
const Util = require('../utils.js')
const {wechatApp} = require('../config');
var crypto = require('crypto');
var request = require('request');
const Router = require('koa-router');
const router = new Router();

router.post('/buyer/orderPay', order.orderPay) //订单支付
router.get('/buyer/notify', order.notifypay); //支付通知

 async function  orderPay(ctx,next) { //订单支付
    let data = ctx.request.body;
    if(!(data.userOpenid || data.total_fee || data.detail)) {
        ctx.status = 400;
        ctx.body = '参数不齐全';
        return;
    }
    var apiUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";
    var total_fee = data.total_fee *100   //订单价格,单位是分
    var openid= data.userOpenid
    var detail = data.detail; //对商品的描述
    var out_trade_no = Util.getWxPayOrdrID(); //订单号
    var timeStamp = Util.createTimeStamp(); //时间节点
    var nonce_str = Util.createNonceStr() + Util.createTimeStamp(); //随机字符串
    var spbill_create_ip = Util.get_client_ip(ctx); //请求ip
    var notify_url ='https://wangtingting.top:9006/buyer/notify';  
    let formData = Util.getfromData(wechatApp.appId, detail, wechatApp.mch_id, nonce_str,notify_url, openid, out_trade_no, spbill_create_ip, total_fee)
    let resultData;
    let status = 200;
    await new Promise(function(resolve, reject){
        request({
            url: apiUrl,
            method: 'POST',
            body: formData
        },function (err, response, body) {
            if (!err && response.statusCode === 200){
                var result_code = Util.getXMLNodeValue('result_code', body.toString("utf-8"));
                var resultCode = result_code.split('[')[2].split(']')[0];
                resolve(dealPaymentSuccessful(resultCode, body, nonce_str, timeStamp))
            }else{                         
                var err_code_des = Util.getXMLNodeValue('err_code_des',body.toString("utf-8"));
                var errDes = err_code_des.split('[')[2].split(']')[0];
                status = 400
                resolve(errDes);
            }
        })
    }).then(function(data) {
        resultData = data;
    })
    ctx.status = status;
    ctx.body = resultData
}

function dealPaymentSuccessful(resultCode, body, nonce_str, timeStamp){           
    if(resultCode === 'SUCCESS'){ 
        //成功
        var prepay_id = Util.getXMLNodeValue('prepay_id', body.toString("utf-8")).split('[')[2].split(']')[0]; //获取到prepay_id
        //签名
        var _paySignjs = Util.paysignjs(wechatApp.appId, nonce_str, 'prepay_id='+ prepay_id,'MD5',timeStamp);
        var args = {
            appId: wechatApp.appId,
            timeStamp: timeStamp,
            nonceStr: nonce_str,
            signType: "MD5",
            package: prepay_id,
            paySign: _paySignjs,
            status:200
        };
        return args;
    }
}
async function notifypay(ctx, next) {
    console.log('@@@@@@@@@@@@@@@@@支付成功')
}

config.js

//以下四个变量都从微信公众平台获取或设置
const wechatApp = {
    appId : 'wxdfb7470ba6c115be',    //appId
    secret : '92a4355dd2c56fb4f6007f38ce61348f',//小程序密钥
    key : 'd9da1c81374d04cda8b56b17a5c42100',//商家密钥
    mch_id : '1525987971'//商家号
}
module.exports={
    wechatApp
};

Utils.js

var crypto = require('crypto')
const {wechatApp} = require('./config')

function getWxPayOrdrID(){
  var myDate = new Date();
  var year = myDate.getFullYear();
  var mouth = myDate.getMonth() + 1;
  var day = myDate.getDate();
  var hour = myDate.getHours();
  var minute = myDate.getMinutes();
  var second = myDate.getSeconds();
  var msecond = myDate.getMilliseconds(); //获取当前毫秒数(0-999)
  if(mouth < 10){ /*月份小于10  就在前面加个0*/
      mouth = String(String(0) + String(mouth));
  }
  if(day < 10){ /*日期小于10  就在前面加个0*/
      day = String(String(0) + String(day));
  }
  if(hour < 10){ /*时小于10  就在前面加个0*/
      hour = String(String(0) + String(hour));
  }
  if(minute < 10){ /*分小于10  就在前面加个0*/
      minute = String(String(0) + String(minute));
  }
  if(second < 10){ /*秒小于10  就在前面加个0*/
      second = String(String(0) + String(second));
  }
  if (msecond < 10) {
      msecond = String(String(00) + String(second));
  } else if(msecond >= 10 && msecond < 100){
      msecond = String(String(0) + String(second));
  }

  var currentDate = String(year) + String(mouth) + String(day) + String(hour) + String(minute) + String(second) + String(msecond);
  return currentDate;
}


function getfromData(appId, detail, mch_id, nonce_str,notify_url, openid, out_trade_no, spbill_create_ip, total_fee) {
  var formData = "<xml>";
  formData += "<appid>"+appId+"</appid>"; //appid
  formData += "<body>" + detail + "</body>"; //商品描述
  formData += "<mch_id>"+mch_id+"</mch_id>"; //商户号
  formData += "<nonce_str>"+nonce_str+"</nonce_str>"; //随机字符串
  formData += "<notify_url>"+notify_url+"</notify_url>";
  formData += "<openid>" + openid + "</openid>";
  formData += "<out_trade_no>" + out_trade_no + "</out_trade_no>";//订单号
  formData += "<spbill_create_ip>"+spbill_create_ip+"</spbill_create_ip>";
  formData += "<total_fee>" + total_fee + "</total_fee>";
  formData += "<trade_type>JSAPI</trade_type>";
  formData += "<sign>" + paysignjsapi(appId,detail,mch_id,nonce_str,notify_url,openid,out_trade_no,spbill_create_ip,total_fee,'JSAPI') + "</sign>";
  formData += "</xml>";
  return formData;
}

// 随机字符串产生函数
function createNonceStr() {
  return Math.random().toString(36).substr(2, 15)
}
// 时间戳产生函数
function createTimeStamp() {
  return parseInt(new Date().getTime() / 1000) + ''
}

var get_client_ip = function(req) {
  var ip = req.headers['x-forwarded-for'] ||
      req.ip ||
      req.connection.remoteAddress ||
      req.socket.remoteAddress ||
      req.connection.socket.remoteAddress || '';
  if(ip.split(',').length>0){
      ip = ip.split(',')[0]
  }
  return ip;
};

function paysignjsapi(appid,body,mch_id,nonce_str,notify_url,openid,out_trade_no,spbill_create_ip,total_fee,trade_type) {
  var ret = {
      appid: appid,
      body: body,
      mch_id: mch_id,
      nonce_str: nonce_str,
      notify_url: notify_url,
      openid: openid,
      out_trade_no: out_trade_no,
      spbill_create_ip: spbill_create_ip,
      total_fee: total_fee,
      trade_type: trade_type
  };
  var string = raw(ret);
  string = string + '&key='+ wechatApp.key;
  console.log('string', string);
  var sign = crypto.createHash('md5').update(string, 'utf8').digest('hex');
  return sign.toUpperCase()
}

//解析xml
function getXMLNodeValue(node_name, xml) {
  var tmp = xml.split("<" + node_name + ">");
  console.log('$', tmp[1]);
  var _tmp = tmp[1].split("</" + node_name + ">");
  return _tmp[0];
}


function paysignjs(appid, nonceStr, package, signType, timeStamp) {
  var ret = {
      appId: appid,
      nonceStr: nonceStr,
      package: package,
      signType: signType,
      timeStamp: timeStamp
  };
  var string = raw1(ret);
  string = string + '&key='+wechatApp.key;
  return crypto.createHash('md5').update(string, 'utf8').digest('hex');
}


function raw1(args) {
  var keys = Object.keys(args);
  keys = keys.sort()
  var newArgs = {};
  keys.forEach(function(key) {
      newArgs[key] = args[key];
  });

  var string = '';
  for(var k in newArgs) {
      string += '&' + k + '=' + newArgs[k];
  }
  string = string.substr(1);
  return string;
}

function raw(args) {
  var keys = Object.keys(args);
  keys = keys.sort();
  var newArgs = {};
  keys.forEach(function(key) {
      newArgs[key.toLowerCase()] = args[key];
  });
  var string = '';
  for(var k in newArgs) {
      string += '&' + k + '=' + newArgs[k];
  }
  string = string.substr(1);
  return string;
}

module.exports = {
  getXMLNodeValue,
  paysignjs,
  get_client_ip,
  createTimeStamp,
  createNonceStr,
  getWxPayOrdrID,
  getfromData
}

签名错误排除方法

  • 参数名ASCII码从小到大排序(字典序);

  • 如果参数的值为空不参与签名;
  • 参数名区分大小写;
  • 验证调用返回或微信主动通知签名时,传送的sign参数不参与签名,将生成的签名与该sign值作校验。
  • 如果你的xml数据包含中文(我们的app肯定会有中文,至少像商品名称大多数还是中文的),需要使用ISO8859-1进行编码才行如下
xml = new String(xml.getBytes(), "ISO8859-1");

如果以上都没问题的话,请将商家密钥(key)重置,我就是通过重置解决的【签名错误】

参考文章:

在线签名验证工具:pay.weixin.qq.com/wiki/tools/…


小程序代码参考

  pay: function() {
    wx.login({
      success: function(res) {
        if (res.code) {
          wx.request({
            url: 'https://wangtingting.top:9006/buyer/orderPay',
            data: {
              total_fee: 100,
              userOpenid: "omQFp5EPX4IPZm5o3DtqNV8gqJEY",
              detail:'123123231',
            },
            method: 'POST',
            success: function(res) {
              console.log("@@@", res)
              wx.requestPayment({
                timeStamp: res.data.timeStamp,
                nonceStr: res.data.nonceStr,
                package: res.data.package,
                signType: 'MD5',
                paySign: res.data.paySign,
                success: function(res) {
                  console.log(res);
                },
                fail: function(res) {
                  console.log(res);
                },
                complete: function(res) {
                  console.log(res);
                }
              })
            }
          })
        } else {
          console.log('获取用户登录态失败!' + res.errMsg)
        }
      }
    });
  }