用Node.js开通微信支付功能

最新接收了微信公众号开发的任务,也是第一次接触node.js,不过这一篇我暂时先不说node.js的使用,由于接通微信支付的确是花费了我好多时间和精力,为了下次能够快速接通当然也和大家分享一下,特以此文记录一下。

一.首先在微信公众号后台

设置 -- > 公众号设置 --> 功能设置 -- > 网页授权域名 (填写微信公众号菜单跳转 到 自己的网站的域名 例如wx.mingrivr.com)

其中
设置 -- > 公众号设置 --> 功能设置 -- > 业务域名 (最好也设置一下,这样如果你的网站需要输入的时候就不会出现红色的提示框提示谨慎输入的字样)

另外切换到
微信支付 --> 开发配置 (将支付授权目录精确到你要发起支付的请求界面的url地址,并以“/” 结束)
然后添加测试的白名单
将测试的微信号添加上去

二.回到程序中来

支付流程:
选择充值的金额之后 --> 告诉自家服务器 生成一个订单号返回 --> 微信支付 --> 支付成功、失败之后回调 --> 告诉自家服务器 余额中充值 并返回结果 -- > 如果成功 结束 ;不成功 微信原路退款

在node.js 工程中,建议建立一个json文件存放关于wxpay配置的相关信息

{  
  "wxAppId" : "", // 公众号后台 开发 --> 基本配置  中可以看到
  "wxAppSecret" : "",// 公众号后台 开发 --> 基本配置  中可以看到
  "wxMchId" : "", // 支付商户的id
  "wxPayKey" : ""// 支付商户的密钥
}

调起微信支付的时候主要就是下面一个方法

WeixinJSBridge.invoke(    'getBrandWCPayRequest',
 { 
  "appId" :paramObj.appId,     //公众号名称,由商户传入       
  "timeStamp":paramObj.timeStamp,         //时间戳,自1970年以来的秒数        
  "nonceStr" :paramObj.nonceStr, //随机串        "package":paramObj.package,        
  "signType":paramObj.signType, //微信签名方式:        
  "paySign":paramObj.paySign //微信签名  
  }, 
 function(res){      
  if(res.err_msg == "get_brand_wcpay_request:ok" ) { 
        //支付成功的回调 微信团队说不一定准确 哈哈哈
    }       
 if(res.err_msg == "get_brand_wcpay_request:cancel" || res.err_msg == "get_brand_wcpay_request:fail"){    
       //微信支付失败或者取消的回调
     }   
 });

其实在你需要调起微信支付的页面的js里面添加这个方法就能调起微信支付,重点来啦,你看到需要传的参数了么?虽然只有五个参数,但是步骤还真是不少?一起来看看吧!

一般为了安全起见,获取这些个参数放在后台,我的程序中也就是node.js负责的这部分。也就是说客户端发起一个请求的时候我在route中通知自家服务端,然后发起微信请求,获取所需的参数之后传回到客户端发起微信支付,收到微信支付回调信息之后再发起请求告诉自家服务器支付结果。

看一下node.js中关于微信支付的代码吧

第一步.获取code

请求 code 的目的就是获取用户的 openid(用户相对于当前公众号的唯一标识) 和access_token,请求的 API
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect

1.  appid公众号的 appid,可以在公众号中看到
2.  redirect_uri 自定义的微信回调地址, 微信会在你请求完上面的地址后跳转到你定义的redirect_uri的地址, 带着 code,此处的 redirect_url 需要 **url_encode** *php*, 如果你的程序是 node 则需要使用 **encodeURLComponent(url)**编码
3.  response_type=code,这个没什么好说的就是固定的
4.  scope=snsapi_userinfo,也可以选择snsapi_base 就不会出现绿色的授权界面
5.  state=STATE 固定这样写就好,详细说明可以查看微信官网的说明
6.  wechat_redirect 固定这样写就好,详细说明可以查看微信官网的说明
第二步.获取access_token openid

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

1.  appid 微信公众号 id,微信公众号后台获取
2.  secret 微信公众号的密钥, 微信公众号后台获取
3.  code, 第一步获取用到的 code
4.  grant_type=authorization_code 固定就好
第三步.先把需要获取的简单的几个参数的方法展示一下

获取随机的NonceStr

voucherControl.createNonceStr = function(){   
 return Math.random().toString(36).substr(2, 15);
};
第四步.获取prepay_id
voucherControl.getPrepayId = function(obj){ 
  // 生成统一下单接口参数   
 var UnifiedorderParams = {      
   appid : wxConfig.wxAppId,       
   body : obj.body,       
   mch_id : wxConfig.wxMchId,        
   nonce_str: voucherControl.createNonceStr(),       
   notify_url : obj.notify_url,// 微信付款后的回调地址       
   openid : openid,        
   out_trade_no : obj.out_trade_no,//new Date().getTime(), //订单号                  
  spbill_create_ip : obj.spbill_create_ip,       
   total_fee : obj.total_fee,       
   trade_type : 'JSAPI',   
 };   
 // 返回 promise 对象    
return  new Promise(function (resolve, reject) {       
 // 获取 sign 参数      
  UnifiedorderParams.sign = voucherControl.getSign(UnifiedorderParams);        
var url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';       
 request.post(
{
url : url,
body:JSON.stringify(voucherControl.getUnifiedorderXmlParams(UnifiedorderParams))
},
 function (error, response, body) {                   
 var prepay_id = '';           
 if (!error && response.statusCode == 200) {              
 // 微信返回的数据为 xml 格式, 需要装换为 json 数据, 便于使用            
xml2jsparseString(body, {async:true}, function (error, result) {  
prepay_id = result.xml.prepay_id[0];                
    // 放回数组的第一个元素                   
 resolve(prepay_id);               
 });           
 } else {         
       reject(body);            
}       
 });   
 })}
第五步.获取sign

获取微信支付的签名

voucherControl.getSign = function(signParams){   
 // 按 key 值的ascll 排序   
  var keys = Object.keys(signParams);   
 keys = keys.sort();    
var newArgs = {};    
keys.forEach(function (val, key) {  
      if (signParams[val]){       
     newArgs[val] = signParams[val];     
   }  
  })    
var string = queryString.stringify(newArgs)+'&key='+wxConfig.wxPayKey;    
return crypto.createHash('md5').update(queryString.unescape(string), 'utf8').digest("hex").toUpperCase();
}
第六步.获取将json数据变为xml数据(因为微信统一下单的时候body中传数据必须为xml数据)

获取微信统一下单参数

voucherControl.getUnifiedorderXmlParams = function(obj){   
 var body = '<xml>' + 
  '<appid>'+wxConfig.wxAppId+'</appid>' +     
  '<body>'+obj.body+'</body>' +      
  '<mch_id>'+wxConfig.wxMchId+'</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 body;
}
第七步.获取微信支付的所有参数

微信支付的所有参数 *
@param req 请求的资源, 获取必要的数据 *
@returns {{
appId: string,
timeStamp: Number,
nonceStr: *,
package: string,
signType: string,
paySign: *}}

voucherControl.getBrandWCPayParams = function(obj,callback ){  
  var prepay_id_promise = voucherControl.getPrepayId(obj);   
  prepay_id_promise.then(function (prepay_id) {     
  var prepay_id = prepay_id;        
  var wcPayParams = {            
    "appId" : wxConfig.wxAppId,     //公众号名称,由商户传入           
    "timeStamp" : parseInt(new Date().getTime() / 1000).toString(), //时间戳,自1970年以来的秒数          
    "nonceStr" : voucherControl.createNonceStr(), //随机串            // 通过统一下单接口获取           
    "package" : "prepay_id="+prepay_id,           
    "signType" : "MD5",         //微信签名方式:        };       
   wcPayParams.paySign = voucherControl.getSign(wcPayParams); //微信支付签名         
      callback(null, wcPayParams);   
   },function (error) {       
      callback(error);    
  });
}
第八步.将微信支付取得的参数传回客户端去调用微信支付吧
voucherControl.getBrandWCPayParams(obj, function (error, responseData) {   
 if (error) {       
   callback(error);   
 } else {            
   callback(null, responseData);   
 }
});
第九步.支付完成的回调

微信支付完了后会在 h5 页面的微信支付的回调函数里面放回值,res.err_msg == "get_brand_wcpay_request:ok" ,这样就是成功了, 但是不是就完事儿了呢 ? 也不是,为什么呢? 微信真的收到钱了么? 收到的钱是不是你传递给微信的值呢 ?你还需要将支付的结果写数据库什么的,这些都是未知。还记的在统一下单接口中有个必须参数就是notify_url : NOTIFY_URL,// 微信付款后的回调地址
这个地址是用户传递给微信的, 微信在收到用户的付款后会以 post 的方式请求这个接口,微信会传递用户付款的信息过来, 不过是 xml 格式的。类系这样的 xml 格式:

<xml>
 <appid><![CDATA[wx2421b1c4370ec43b]]></appid>
 <attach><![CDATA[支付测试]]></attach>
 <bank_type><![CDATA[CFT]]></bank_type> 
<fee_type><![CDATA[CNY]]></fee_type> 
<is_subscribe><![CDATA[Y]]></is_subscribe>
 <mch_id><![CDATA[10000100]]></mch_id> 
<nonce_str><![CDATA[5d2b6c2a8db53831f7eda20af46e531c]]></nonce_str> 
<openid><![CDATA[oUpF8uMEb4qRXf22hE3X68TekukE]]></openid> <out_trade_no><![CDATA[1409811653]]></out_trade_no> 
<result_code><![CDATA[SUCCESS]]></result_code>
 <return_code><![CDATA[SUCCESS]]></return_code> 
<sign><![CDATA[B552ED6B279343CB493C5DD0D78AB241]]></sign> <sub_mch_id><![CDATA[10000100]]></sub_mch_id> 
<time_end><![CDATA[20140903131540]]></time_end>
 <total_fee>1</total_fee> 
<trade_type><![CDATA[JSAPI]]></trade_type> 
<transaction_id><![CDATA[1004400740201409030005092168]]></transaction_id></xml>

根据自己的业务逻辑解析这个 xml 格式的数据就好了。注意:这里你在获取到数据后微信需要得到你的回应, 如果你一直不回应微信, 微信会请求你好几次, 这样估计你的逻辑会有问题吧,所以你需要给微信返回 xml 的格式的 回应。

<xml> 
<return_code><![CDATA[SUCCESS]]></return_code>
 <return_msg><![CDATA[OK]]></return_msg>
</xml>
注意点:node ,express 框架开发, 如果你在微信的支付成功后的回调中没有获取到任何 xml 的值

你需要安装一组件:body-parser-xml, 你可以使用 npm install body-parser-xml --save
安装, 在 app.js 里面 require('body-parser-xml')(bodyParser);
,使用中间件的方式
// 解决微信支付通知回调数据

app.use(bodyParser.xml({
 limit: '2MB', // 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 数据了。

pay.getAccessToken({
    notify_url : 'http://demo.com/', //微信支付完成后的回调 
    out_trade_no : new Date().getTime(), //订单号 
    attach : '名称', body : '购买信息',
    total_fee : '1', // 此处的额度为分
    spbill_create_ip : req.connection.remoteAddress,
 }, function (error, responseData) {
     res.render('payment', { title : '微信支付',
     wxPayParams : JSON.stringify(responseData)
 }); 
});
最后我将node.js关于微信支付的代码封装成一个js

使用方法见js中的 useWxPay 方法,将所需参数传入就会得到微信下单的所有参数

/**
 * Created by haHa on 2016/11/21.
 * 微信支付的js文件
 */

var wxPayControl = module.exports;
var request = require("request");
var url = require('url');
var queryString = require('querystring');
var crypto = require('crypto');
var xml2jsparseString = require('xml2js').parseString;
var wxConfig = require('../z_configs/wxconfig.json');
/*
{
    "wxAppId" : "",//公众号appid
    "wxAppSecret" : "",//公众号appsecrect
    "wxMchId" : "",//商户id
    "wxPayKey" : ""//商户密钥
}
*/

var access_token;
var expires_in;
var refresh_token;
var openid = "";
var scope;


/*
  微信支付的js文件使用
 */
function useWxPay() {

    var notify_url = '你发起支付的页面地址';
    var spbill_create_ip = getClienIp(req);
    var obj = {
        body : "明日VR-账户游戏币充值", //描述微信支付的意义
        notify_url : notify_url,// 微信付款后的回调地址
        out_trade_no : data.orderNo,//new Date().getTime(), //订单号
        spbill_create_ip :spbill_create_ip,
        total_fee : total_fee//金额
    }
    wxPayControl.getAccessToken(obj,wxCode,function (err,wxParam) {

    })
}

/**
 * 获取客户端的ip
 * req是发起请求的req
 */
getClienIp = function(req) {

    //固定ip
    return "139.224.53.144"

    //因为 输出是 ::fffff:139.224.53.144 所以固定测试一下
    // return req.headers['x-forwarded-for'] ||
    //     req.connection.remoteAddress ||
    //     req.socket.remoteAddress ||
    //     req.connection.socket.remoteAddress;
};

/*
 第一步 获取code
*/


/**
 * 获取微信的 AccessToken 和 openid
 */
wxPayControl.getAccessToken = function(obj,wxCode,callback) {

    console.log("wecode ~~~");
    console.log(wxCode);
    var getAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid="+wxConfig.wxAppId+"&secret="+wxConfig.wxAppSecret+"&code="+wxCode+"&grant_type=authorization_code";
    request.post({url : getAccessTokenUrl}, function (error, response, body) {
        console.log("微信支付 获取accesstoken");
        console.log(body);
        if (!error && response.statusCode == 200){
            if (40029 == body.errcode) {
                callback(error, body);
            } else {
                body = JSON.parse(body);
                access_token = body.access_token;
                expires_in = body.expires_in;
                refresh_token = body.refresh_token;
                openid = body.openid;
                scope = body.scope;
                // 拼接微信的支付的参数
                getBrandWCPayParams(obj, function (error, responseData) {
                    if (error) {
                        callback(error);
                    } else {
                        console.log("微信支付的参数");
                        console.log(responseData);
                        callback(null, responseData);
                    }
                });
            }
        } else {
            callback(error);
        }
    });
}

/**
 * 获取微信统一下单的接口数据
 */
function getPrepayId(obj){
    // 生成统一下单接口参数
    var UnifiedorderParams = {
        appid : wxConfig.wxAppId,
        body : obj.body,
        mch_id : wxConfig.wxMchId,
        nonce_str: createNonceStr(),
        notify_url : obj.notify_url,// 微信付款后的回调地址
        openid : openid,
        out_trade_no : obj.out_trade_no,//new Date().getTime(), //订单号
        spbill_create_ip : obj.spbill_create_ip,
        total_fee : obj.total_fee,
        trade_type : 'JSAPI',

    };
    // 返回 promise 对象
    return  new Promise(function (resolve, reject) {
        // 获取 sign 参数
        UnifiedorderParams.sign = getSign(UnifiedorderParams);
        var url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
        request.post({url : url, body:JSON.stringify(getUnifiedorderXmlParams(UnifiedorderParams))}, function (error, response, body) {
            console.log("微信下单返回数据");
            console.log(response);
            var prepay_id = '';
            if (!error && response.statusCode == 200) {
                // 微信返回的数据为 xml 格式, 需要装换为 json 数据, 便于使用
                xml2jsparseString(body, {async:true}, function (error, result) {
                    prepay_id = result.xml.prepay_id[0];
                    // 放回数组的第一个元素
                    resolve(prepay_id);
                });
            } else {
                reject(body);
            }
        });
    })
}

/**
 * 微信支付的所有参数
 * @param req 请求的资源, 获取必要的数据
 * @returns {{appId: string, timeStamp: Number, nonceStr: *, package: string, signType: string, paySign: *}}
 */
function getBrandWCPayParams(obj,callback ){
    var prepay_id_promise = getPrepayId(obj);
    prepay_id_promise.then(function (prepay_id) {
        var prepay_id = prepay_id;
        var wcPayParams = {
            "appId" : wxConfig.wxAppId,     //公众号名称,由商户传入
            "timeStamp" : parseInt(new Date().getTime() / 1000).toString(),         //时间戳,自1970年以来的秒数
            "nonceStr" : createNonceStr(), //随机串
            // 通过统一下单接口获取
            "package" : "prepay_id="+prepay_id,
            "signType" : "MD5",         //微信签名方式:
        };
        wcPayParams.paySign = getSign(wcPayParams); //微信支付签名
        console.log(wcPayParams.paySign);
        console.log("微信支付的所有参数");
        console.log(wcPayParams);
        callback(null, wcPayParams);
    },function (error) {
        callback(error);
    });
}

/**
 * 获取微信统一下单参数
 */
function getUnifiedorderXmlParams(obj){
    var body = '<xml>' +
        '<appid>'+wxConfig.wxAppId+'</appid>' +
        '<body>'+obj.body+'</body>' +
        '<mch_id>'+wxConfig.wxMchId+'</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 body;
}

/**
 * 获取微信支付的签名
 * @param payParams
 */
function getSign(signParams){
    // 按 key 值的ascll 排序
    console.log('支付签名');
    console.log(signParams);
    var keys = Object.keys(signParams);
    keys = keys.sort();
    var newArgs = {};
    keys.forEach(function (val, key) {
        if (signParams[val]){
            newArgs[val] = signParams[val];
        }
    })
    var string = queryString.stringify(newArgs)+'&key='+wxConfig.wxPayKey;
    console.log(string);

    // 生成签名
    return crypto.createHash('md5').update(queryString.unescape(string), 'utf8').digest("hex").toUpperCase();
}

/**
 * 获取随机的NonceStr
 */
function createNonceStr(){
    return Math.random().toString(36).substr(2, 15);
}

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

推荐阅读更多精彩内容