nodejs搭建微信公众号开发——微信支付

写在最前面

微信公众号的开发一直没有深入接触,也是到了最近自己的项目要用到,才深入地接触其他开发文档。在这过程中也是各种度各种谷的查找相关资料,是多少碰到了不少的坑,现在就将这期间碰到的各种坑记录下来。

因为本文是基于nodejs+express写的,nodejs是前几个月才开始使用的,也并不熟悉,本文后面有出现的代码可能会有些不nodejs,还请有幸看到此文的读者勿喷!!!

微信公众号:

1,订阅号,可以每天向关注者群发一次消息的,没有支付功能的。不知道什么时候企业申请的订阅号必须微信认证通过后才可以使用,但个人订阅号直接申请通过就可使用的,所以现在很多公司的订阅号“感觉”都是用个人去申请的,如Uber的厦门公众号(一鹭U你)等。

2,服务号,具有微信支付功能的,每月有4次向关注者群发消息的机会。企业申请的服务号可以不用认证就可使用,只是一些高级功能没有权限使用而已,如支付功能等,这点比企业订阅号好。也不知道为什么微信要这样处理,企业申请的订阅号必须通过微信认证后才可使用。也不知道这其间的利益权衡是怎样的?!在这我也没去查找资料,我主要还是关心我的开发,哈哈……

微信各公众号类型的区别请各自去百度,资料很多的,具体还是要以自己的实际需求去决定哪种类型公众号适合自己或公司的,但总感觉不管是订阅号还是服务号,好像都是要的,我公司就是两种都要的(一个为每天都发信息,一个为了支付功能,为什么就不能集于一个呢,微信呢??!!)

微信支付+nodejs

微信支付现在有三种trade-type, APP, NATIVE, JSAPI, 本文主要实现native与jsapi的代码实现。
微信支付开发文档:https://pay.weixin.qq.com/wiki/doc/api/index.html

wxpay.jpg

这里我主要是对公众号支付与扫码支付进行实现而已,其他的支付暂时还未涉及到,也就没深入去研究,待以后有机会再来写文章。

这里我们就默认微信公众号,微信支付这些前期工作都已完成,若有不懂可直接网找资料,已有很多文章可查阅的。

下面直接上代码

基本数据与配置,详见代码块里注释

var request = require('request');
var crypto = require('crypto');
var qs = require('querystring');
var xml2js = require('xml2js');

var wechat = {};

// 微信基本数据
wechat.config = {
    // 微信公众号 appid
    appId: 'wxbc8b10***********',
    // 微信公众号 appsecret
    appSecret: 'c9934********************',
    // 微信商户号,微信支付要用到的
    mch_id: '***********',
    // 微信支付的api-key
    api_key: '***************',

    // 获取微信基础access-token的url
    accessTokenUrl:'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential',
    // 获取微信网页授权所需的jsapi-ticket的url
    ticketUrl:'https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=',

    // 微信支付是否支持信用卡支付
    limit_pay: 'no_credit',
    // 微信支付回调通知支付结果
    notify_url: 'http://www.jmkbio.com/wechat/wxpay-cb',   
    //微信支付统一下单的prepay_id的url
    prepay_id_url: 'https://api.mch.weixin.qq.com/pay/unifiedorder',

    //正式环境的微信端auth2.0网页授权回调URL
    webAuthServerUrl: 'http://www.******.com/wechat/authtoken', 

    //微信网页授权第一步所要请求获得code的URL
    webAuthCodeUrl: 'https://open.weixin.qq.com/connect/oauth2/authorize?',
    //微信网页授权所需的access_token,用于获取到用户的openid等信息
    webAuthTokenUrl: 'https://api.weixin.qq.com/sns/oauth2/access_token?',
};

//用于存储微信的基础access-token值,每天有请求限制次数
var gloAccessTokenData = {};

//公众号微信端页面请求所需jsapi-ticket数据缓存,每天有请求限制,用于签名并返回给前端构造wx.config
var jsapiTicketData = {};

//2小时过期时间,60*60*2
var expireTime = 7200 -100; 

微信端的网页端配置参数

微信网页开发文档 : https://mp.weixin.qq.com/wiki/11/74ad127cc054f6b80759c40f77ec03db.html
因为在取得access-token与jsapi-ticket微信端每天都有一定的请求频率限制,2000/天,所以在每个网页的初始化时wx.config里的数据时,我们都要事先判断一下缓存里是否有token与ticket数据,是否已过期,详见下面代码块。** 记住此处的access-token 是微信的基础access-token,微信公众里的很多权限是通过这个token值进行判断的,与网页auth2.0的access-token不同,那个是为了得到用户的信息才用到(snsapi-base, snsapi-info)**。

wxconfig.jpg
// 取得微信web端所需的wxConfig初始化数据
/*********************
    前端所需的数据,timestamp,nonceStr,signature,
    wx.config({
        timestamp: , // 必填,生成签名的时间戳
        nonceStr: '', // 必填,生成签名的随机串
        signature: '',// 必填,签名,见附录1
    });
    _url:微信网页端的请求url值,不包括#后面的数据,
    _cb: 回调函数,接收处理形成的wxconfig数据
***************************/
wechat.getWxConfig = function(_url, _cb) {
    //缓存数据里取得相关的数据, jsapi-ticket等
    if (jsapiTicketData && jsapiTicketData.timestamp) {
        //判断过期时间是否已到
        var t = getTimeStamp() - jsapiTicketData.timestamp;
        console.log('the gap of the lasttime to get jsapi-ticket : ', t);
        // jsapi-ticket未过期,使用缓存数据进行签名处理
        if (t < expireTime) {
            console.log('use cache data to get jsapi-ticket!!');
            // 取得网页所需的数据,签名,appid,timestamp, noncestr等
            var _signData = reSignature(_url, jsapiTicketData.ticket);
            _cb && _cb(_signData);
        } else { 
            console.log('time is out, reget jsapi-ticket!!!');
            //过期时间已到,重新取得网页所需的数据,签名,appid,timestamp, noncestr等
            wechat.getJsapiTicket(function(_tk) {
                var _signData = reSignature(_url, _tk);
                _cb && _cb(_signData);
            });
        }
    } else {
        console.log('first time to get jsapi-ticket!');
        //该页面首次请求,取得网页所需的数据,签名,appid,timestamp, noncestr等
        wechat.getJsapiTicket(function(_tk) {
                var _signData = reSignature(_url, _tk);
                _cb && _cb(_signData);
            });
    }
};

//取得timestamp
function getTimeStamp() {
    return parseInt(new Date().getTime() / 1000) + '';
};

//取得随机数
function getNonceStr() {
    return Math.random().toString(36).substr(2, 15);
};

//形成key=value&key1=value&...的字符串
function getRawString(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) {
        //如果参数的值为空不参与签名
        if (newArgs[k]) {
            string += '&' + k + '=' + newArgs[k];
        }
      }
      string = string.substr(1);
      return string;
};

//形成向微信服务器请求的xml格式数据
function getXmlFormat(_array) {
    var keys = Object.keys(_array);
    var _xmlData = '<xml>';
    keys.forEach(function(key) {
        _xmlData += '<' + key + '>' + _array[key] + '</' + key + '>';
    });

    //取得签名加密字符串
    var _paySign = paySign(_array);     
    _xmlData += '<sign>' + _paySign + '</sign>';
    _xmlData += '</xml>';

    // console.log('xml data ===', _xmlData);
    return _xmlData;
};

//取得微信端返回来的xml标签里的value
function getXMLNodeValue(node_name, xml, flag){
    flag = flag || false;
    var _reNodeValue = '';
    var tmp = xml.split('<' + node_name + '>');
    if (tmp) {
        var _tmp = tmp[1].split('</' + node_name + '>')[0];
        if (!flag) {
            var _tmp1 = _tmp.split('[');
            _reNodeValue = _tmp1[2].split(']')[0]
        } else {
            _reNodeValue = _tmp;
        }   
    }
    return _reNodeValue;
};

//响应网页端请求的签名数据
function reSignature(_url, _ticket) {
    var timestamp = getTimeStamp();
    var noncestr = getNonceStr();

    var str = 'jsapi_ticket=' + _ticket + '&noncestr='+ noncestr + '&timestamp=' + timestamp + '&url=' + _url;
    console.log(str);
    var signature = crypto.createHash('sha1').update(str).digest('hex');

    console.log('jsapi signature is ', signature);
    var _dataSign = { 
                        appId: wechat.config.appId,
                        timestamp: timestamp,
                        nonceStr: noncestr,
                        signature: signature
                    };

    return _dataSign;
};

//根据数据格式需求生成签名
function paySign(_array) {
    _array = _array || {};
    //拼接成微信服务器所需字符格式
    var string = getRawString(_array);
    //key为在微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置
    var key = wechat.config.api_key;
    string = string + '&key='+key;  
    var crypto = require('crypto');
    var cryString = crypto.createHash('md5').update(string,'utf8').digest('hex');
    //对加密后签名转化为大写
    return cryString.toUpperCase();
};

// 取得微信的基础access-token,有别于网页auth2.0的access-token
wechat.getGloAcessToken = function(_cb) {
    // 决断是否是首次获取该数据
    if (gloAccessTokenData.token && gloAccessTokenData.timestamp) {
        var t = getTimeStamp() - gloAccessTokenData.timestamp;
        console.log('the gap of last time to get glo-access-token is : ', t);
        // 数据是否过期判断
        if (t < expireTime) {
            console.log('get the cache access-token data!');
            _cb && _cb(gloAccessTokenData.token);
        } else {
            console.log('expiretime is out,reget the access-token data!');
            justGetAccessToken(_cb);          
        }
    } else {
        console.log('firt time to connect, get the access-token data!!');
        justGetAccessToken(_cb);         
    }
};

// 请求获得token数据, 基础的access-token,与autho2.0网页版不同
function justGetAccessToken(_cb) {
    var _tokenUrl = wechat.config.accessTokenUrl + '&appId=' + wechat.config.appId + '&secret=' + wechat.config.appSecret;
    request.get(_tokenUrl, function(error, response, body) {
        if (error) {
            console.log('getToken error1111', error);
        }
        else {
            try {
                console.log('success to get the access-token data ===', JSON.parse(body));
                var _token = JSON.parse(body).access_token;
                // 将取得的access-token保存到内存
                gloAccessTokenData = {
                    token: _token,
                    timestamp: getTimeStamp()
                }
                _cb && _cb(_token);
            }
            catch (e) {
                console.log('getToken error2222', e);
            }
        }
    });
};

// 取得微信网页端所需的jsapi-ticket
wechat.getJsapiTicket = function(_cb) {
    // 先判断内存(缓存)中是否已有jsapi-ticket数据
    if (jsapiTicketData && jsapiTicketData.timestamp) {
        var t = getTimeStamp() - jsapiTicketData.timestamp;
        console.log('the gap of last time to get jsapi-ticket is : ', t);
        // 数据是否过期判断
        if (t < expireTime) {
            console.log('get the cache access-token data!');
            _cb && _cb(jsapiTicketData.ticket);
        } else {
            console.log('expiretime is out,reget the jsapi-ticket data!');
            justGetJsapiTicket(_cb);          
        }
    } else {
        console.log('first time to get the jsapi-ticket data!');
        justGetJsapiTicket(_cb); 
    }   
};

// 根据基础access-token(重新)取得jsapi-ticket值
function justGetJsapiTicket(_cb) {
    // 取得jsapi-ticket需有基础的access-token数据
    wechat.getGloAcessToken(function(_tk) {
        var _ticUrl = wechat.config.ticketUrl + _tk + '&type=jsapi';
        request.get(_ticUrl, function(error, res, body) {
            if (error) {
                console.log('getJsapiTicket error1111', error);
            }
            else {
                try {
                    var _ticket = JSON.parse(body).ticket;
                    console.log('get new ticket success--', _ticket);                    
                    var timestamp = getTimeStamp();

                    //将token与ticket数据保存在内存中
                    jsapiTicketData = {
                        timestamp: timestamp,
                        token: _tk,
                        ticket: _ticket
                    };

                    _cb && _cb(_ticket);  
                }
                catch (e) {
                    console.log('getJsapiTicket error2222', e);
                }
            }
        });
    });
};

微信网页授权取得用户的openid等信息(snsapi-base, snpapi-info)

因为在微信端支付时,即选择为jsapi时,必须传递openid值才可生成有效的支付数据,所以在这边需要获得用户的openid信息。
http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html

//取得auth2.0网页授权code请求Url, _cb用于重定向该url并将后续得到的code值去得到用户的openid值 
/*
  _path: 获取code的回调路径,用于形成最终的微信服务器回调地址
         redirect_uri = baseUrl + _path
  _scope: 取得用户授权的类型,snsapi_base是静默授权并自动跳转到回调页的
          snsapi_userinfo授权需要用户手动同意,并且由于用户同意过,用来获取用户的基本信息
*/
wechat.getWebAuthCodeUrl = function(_path, _scope) {
    _path = _path || '';
    var  _codeParams = {
        appid: wechat.config.appId,
        //网页auth2.0授权取得code后的回调地址,需urlencode处理
        redirect_uri: wechat.config.webAuthServerUrl + _path, 
        response_type: 'code',
        scope: _scope || 'snsapi_base', //是静默授权或是手工授权
        state: 'STATA'
    };
    var _webCodeUrl = wechat.config.webAuthCodeUrl +  qs.stringify(_codeParams) + '#wechat_redirect';
    console.log('web auth get code', _webCodeUrl);

    return _webCodeUrl;
};

//取得网页授权数据, access_token, openid等
wechat.getWebAuthToken = function(_code, _cb, _cbfail) {
    var _tokenParams = {
        appid: wechat.config.appId,
        secret: wechat.config.appSecret,
        code: _code,
        grant_type: 'authorization_code',
      };

    var _webTokenUrl = wechat.config.webAuthTokenUrl + qs.stringify(_tokenParams);
    console.log('web auth get access_token url: ', _webTokenUrl);

    request({
        method: 'get',
        url: _webTokenUrl
    }, function(err, res, body) {
        if (body) {
            var _data = JSON.parse(body);
            console.log('the openid of wx-user is ===', _data.openid);
            _cb && _cb(_data);
        } else {
            console.log('fail to get the web auth-token&&openid, error msg is ', err);
        }
    });
};

生成支付统一订单-prepay_id

https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1

// 取得微信支付返回的数据,用于生成二维码或是前端js支付数据
wechat.getWeChatPayid = function(_spbillId, _traType, _openid, _out_trade_no, _attach, _product_id, _body, _cb, _cbfail){
    console.log('客户端请求ip:', _spbillId);

    //取得需向微信服务器发送的数据,且通过该数据组进行xml与sign数据生成
    //数据集必须包含所有微信端所必须的字段数据信息
    var _preArray = {
        appid: wechat.config.appId,
        mch_id: wechat.config.mch_id, //微信支付商户号
        notify_url: wechat.config.notify_url, //回调函数
        out_trade_no: _out_trade_no || ('pro_wxpay' + Math.floor((Math.random()*1000)+1)), //订单号
        attach: _attach || '支付功能', //附加信息内容
        product_id: _product_id || 'wills001', // 商品ID, 若trade_type=NATIVE,此参数必传
        body: _body || 'H5端支付功能开发', // 支付内容
        openid: _openid || '',
        spbill_create_ip: _spbillId || '127.0.0.1', //客户端ip
        time_stamp: getTimeStamp(), 
        trade_type: _traType || 'JSAPI', 
        total_fee: 1, //支付金额,单位分
        nonce_str: getNonceStr(),
        limit_pay: wechat.config.limit_pay, //是否支付信用卡支付
    };

    //取得xml请求数据体
    var _formData = getXmlFormat(_preArray);

    //向微信服务端请求支付
    request({
        url : wechat.config.prepay_id_url,
        method : 'POST',
        body : _formData
    }, function (err, response, body) {
        if (!err && response.statusCode == 200) {
            //返回来的XML数据
            var _reBodyXml = body.toString('uft-8');
            console.log('return xml data ==', _reBodyXml);
            //取得return_code进行成功与否判断
            var _reCode = getXMLNodeValue('return_code', _reBodyXml, false);
            // console.log('return code', _reCode);

            var rePrepayId = {
                prepay_id: '',
                code_url: '',
                timestamp: _preArray.time_stamp,
                nonceStr: _preArray.nonce_str,
                paySign: '',
                msg: '请求prepay_id'
            };
            if (_reCode=='SUCCESS') {
                var _resultCode = getXMLNodeValue('result_code', _reBodyXml, false);
                if (_resultCode=='SUCCESS') {
                    //成功时返回prepay_id与二维码
                   rePrepayId.prepay_id = getXMLNodeValue('prepay_id', _reBodyXml, false);
                   rePrepayId.msg = '成功取得prepay_id';
                   if (_preArray.trade_type == 'NATIVE') {
                       rePrepayId.code_url = getXMLNodeValue('code_url', _reBodyXml, false);
                   } else if(_preArray.trade_type == 'JSAPI') {
                        var _signPara = {
                                appId: wechat.config.appId,
                                timeStamp: _preArray.time_stamp,
                                nonceStr: _preArray.nonce_str,
                                package: 'prepay_id=' + rePrepayId.prepay_id,
                                signType: 'MD5'
                            };
                        rePrepayId.paySign = paySign(_signPara);
                   }                
                } else {
                    rePrepayId.msg = getXMLNodeValue('err_code_des', _reBodyXml, false);
                }
                _cb && _cb(rePrepayId);
            } else if (_reCode=='FAIL') {
                rePrepayId.msg = getXMLNodeValue('return_msg', _reBodyXml, false);
                _cbfail && _cbfail(rePrepayId);
            }          
        }
    });
    _formData = null;
};

微信支付回调函数

这部分因为我是用express4.x,在微信支付回调时返回的xml格式的数据,但因为express4.x的原因,在回调函数里未能通过 _req.body || _req.rawBody 获得到相应的数据,通过查找发现返回的数据都为{},最终是因为express4.x将body-parser分离出去,现在是以中间的形式加载到express4.x框架中去的,而其对xml并未做“解析”所致的。

因此我在自己的系统中装了body-parser-xml, 在app.js里增加

var bodyParser = require('body-parser');
require('body-parser-xml')(bodyParser);
// 解决微信支付通知回调数据
app.use(bodyParser.xml({
  limit: '1MB',   // Reject payload bigger than 1 MB 
  xmlParseOptions: {
    normalize: true,     // Trim whitespace inside text nodes 
    normalizeTags: true, // Transform tags to lowercase 
    explicitArray: false // Only put nodes in array if >1 
  }
}));

回调函数,对于微信回调xml数据,大家以自己的系统实际数据为准进行解析,下面公供参考。

// 微信支付回调,回调数据要以实际数据进行解析
/*
express4.X返回的数据
 _returnData = { xml: 
   { appid: 'wxbc8b10******************',
     attach: '支付功能',
     bank_type: 'CFT',
     cash_fee: '1',
     fee_type: 'CNY',
     is_subscribe: 'Y',
     mch_id: '137*******',
     nonce_str: '10fskie7bymn29',
     openid: 'ooqSov0HufIdX7YGY1ePDC5NJS-w',
     out_trade_no: 'pro_wxpay649',
     result_code: 'SUCCESS',
     return_code: 'SUCCESS',
     sign: '549B3D77F7C5E2766406A68BA3E27D78',
     time_end: '20160823162731',
     total_fee: '1',
     trade_type: 'JSAPI',
     transaction_id: '4000732001201608232045230805' 
    }
   }
*/
wechat.wxPayCallback = function(_req, _cb) {
    //返回来的XML数据,现在是以express4.X的返回数据为例子,实际中要以实际数据进行解析
    var _reBody = _req.body || _req.rawBody;
    var _payInfo = _reBody.xml;

    if (_payInfo.return_code == 'SUCCESS') {
        console.log('用户成功支付金额:', _payInfo.cash_fee);
        console.log('用户openid:', _payInfo.openid);
    } else {
        console.log('用户支付失败:', _payInfo.return_msg);
        console.log('用户openid:', _payInfo.openid);
    }
    var xml = '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';

    _cb && _cb(xml);
};

module.exports = wechat;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容