nodejs开发微信支付接口
文本主要讲解如何使用nodejs来对接微信支付,对接以app支付为例说明。
首先我们需要来看一下后台具体都需要做哪些功能:
- 统一下单
- 接收订单结果通知
- 查询订单
- 申请退款
- 查询退款
- 退款结果通知接收
后面我会逐步说一下具体的实现方法,做这些工作之前需要做一些准备工作。首先是一些必要的微信参数:appid、appsecret、mchid、key,双向证书(nodejs开发使用的证书是以.p12为后缀的文件)。
然后需要准备的就是一些开发模块了,本文介绍的nodejs框架为express。需要额外安装的一个模块就是xml2js,因为微信返回的一些信息都是xml格式的,需要使用这个模块进行解析。
模块准备完了,我们就可以进行开发了。
统一下单
我们先来做的是统一下单这个接口,基本流程是由客户端发起请求,服务器接到请求后调用微信统一下单接口,生成订单,然后服务器将微信服务器返回的信息返回给客户端,客户端通过这些信息来拉起微信支付。至此,统一下单流程就结束了。
下面我们需要来看一下该如何实现。因为需要发起请求,我们这里将发送请求封装成一个方法,便于后续的重复使用,我们将它命名为common.js,在这个方法中还需要封装一些其他的方法,比如时间格式化,请看下面代码:
代码语言:javascript复制const https = require('https');
const crypto = require('crypto');
// 对Date的扩展,将 Date 转化为指定格式的String
// 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符,
// 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)
// 例子:
// (new Date()).Format("yyyy-MM-dd hh:mm:ss.S") ==> 2019-07-02 08:09:04.423
// (new Date()).Format("yyyy-M-d h:m:s.S") ==> 2019-7-2 8:9:4.18
Date.prototype.Format = function (fmt) { //author: meizz
var o = {
"M ": this.getMonth() 1, //月份
"d ": this.getDate(), //日
"h ": this.getHours(), //小时
"m ": this.getMinutes(), //分
"s ": this.getSeconds(), //秒
"q ": Math.floor((this.getMonth() 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
if (/(y )/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() "").substr(4 - RegExp.$1.length));
for (var k in o)
if (new RegExp("(" k ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" o[k]).substr(("" o[k]).length)));
return fmt;
};
//字符串md5
exports.md5 = function md5(str) {
return crypto.createHash('md5').update(str, 'utf-8').digest('hex');
};
//封装post请求
exports.post_https_requestXml = function (urlstring, post_data, callback) {
callback = callback || function () {
};
var urlData = url.parse(urlstring);
var hostIP = urlData.host;
if (urlData.host.indexOf(":") > 0) {
hostIP = urlData.host.substr(0, urlData.host.indexOf(":"));
}
var options = {
hostname: hostIP,
port: urlData.port,
path: urlData.path,
method: 'POST',
};
if(post_data.agentOptions){
options.pfx = post_data.agentOptions.pfx;
options.passphrase = post_data.agentOptions.passphrase;
}
var req = https.request(options, function (res) {
var body = "";
res.setEncoding('utf8');
res.on('data', function (chunk) {
body = chunk;
});
res.on('end', function () {
callback(body);
});
});
req.on('error', function (e) {
console.log('error:' e.message);
callback('');
});
req.write(post_data.body);
req.end();
};
虽然我将它命名为了requestxml,其实他也可以正常发送json数据的,在这个方法里面有一个特别的地方,那就是
代码语言:javascript复制 if(post_data.agentOptions){
options.pfx = post_data.agentOptions.pfx;
options.passphrase = post_data.agentOptions.passphrase;
}
这段代码。它的作用是为了退款准备的,退款的接口需要双向证书验证的,pfx代表的是证书内容,passphrase代表证书密码,如此一来我们就无需将证书安装到本地计算机了,将其携带发送就可以了。
好了,退款的相关介绍后面会有介绍,我们这里先重点说统一下单。我们将这个文件命名为pay.js。微信的所有接口都需要进行签名验证的,具体算法说明可以直接看官方的文档,我们这里还看具体的实现方法。
代码语言:javascript复制/**
* 微信签名封装
* @param obj 待签名对象
* @param key 商户平台设置的密钥key
* @returns {*}
*/
exports.getWechatSign = (obj,key)=>{
try{
let tempObj = Object.assign({},obj);
let signStr = "";
let newObje = {};
//将参数进行ASCII字典序排序,然后拼接成字符串
Object.keys(tempObj).sort().map(item=>{
if(tempObj[item]!="" && !(tempObj[item] instanceof Array && tempObj[item].length==0) && !(tempObj[item] instanceof Object && Object.keys(tempObj[item]).length==0) ) {
if (tempObj[item] instanceof Object) {
tempObj[item] = JSON.stringify(tempObj[item]);
}
signStr =(item "=" tempObj[item] "&");
newObje[item] = tempObj[item]
}
});
if(signStr){
signStr =("key=" key)
}
//签名进行md5运算,然后转换为大写
const sign = common.md5(signStr).toUpperCase();
newObje.sign = sign;
return newObje
}catch(e){
console.log(e);
return null
}
};
由于微信发送以及接受的数据格式是xml,所以我们还需要封装一个方法,将json格式转换为xml格式,以及将xml转换为json格式,这里就需要用到xml2js了,在之前的文章我介绍过解析xml文件,使用到的是xmlreader,至于这里可根据个人熟悉哪个用哪个,个人觉得这里更适合使用xml2js:
代码语言:javascript复制const xml2js = require('xml2js');
/**
* 将obj转为微信提交xml格式,包含签名
* @param obj 转换为xml格式的对象
* @param key 商户平台设置的密钥key
* @returns {string} 签名并转换完成的字符串
*/
exports.json2xml=(obj,key)=>{
let tempObj = Object.assign({},obj);
let jsonxml = "";
tempObj = exports.getWechatSign(tempObj,key);
if(tempObj){
jsonxml ='<xml>';
Object.keys(tempObj).sort().map(item=>{
jsonxml =`<${item}>${tempObj[item]}</${item}>`
});
jsonxml =`</xml>`;
}
return jsonxml
};
/**
* 格式化xml数据为json格式
* @param xmlData
* @returns {Promise<any>}
*/
exports.parseXml = (xmlData)=>{
let {parseString} = xml2js;
let res;
return new Promise((resolve,reject)=>{
parseString(xmlData, {
trim: true,
explicitArray: false
}, function (err, result) {
if(err){
reject(err)
}else{
res = result;
resolve(res.xml);
}
});
})
};
至此,基本的准备工作做完了,我们可以进行主体开发了:
代码语言:javascript复制const common = require('common');
/**
*
* @param params = {
* appid:应用ID,微信开放平台审核通过的应用APPID(请登录open.weixin.qq.com查看,注意与公众号的APPID不同),
* mch_id:微信支付分配的商户号
* key:商户平台设置的密钥key
* spbill_create_ip:支持IPV4和IPV6两种格式的IP地址。调用微信支付API的机器IP
* textInfo:商品描述交易字段格式根据不同的应用场景按照以下格式:腾讯充值中心-QQ会员充值
* total_fee:订单总金额,单位为分
* trade_type:支付类型,JSAPI--JSAPI支付(或小程序支付)、NATIVE--Native支付、APP--app支付,MWEB--H5支付,不同trade_type决定了调起支付的方式,请根据支付产品正确上传
* expireTime: 过期时间,单位小时,默认及最大值为两小时
* callBackUrl:接收支付结果通知url
* }
* @param callback
*/
exports.wechatUnifiedorder = async (params,callback)=>{
//微信支付统一下单
try{
let {appid,mch_id,key,spbill_create_ip,textInfo,total_fee,trade_type,expireTime,,callBackUrl} = params;
if(!appid){
callback("缺少应用ID");
return
}
if(!mch_id){
callback("缺少商户号");
return
}
if(!key){
callback("缺少商户平台密钥key");
return
}
if(!spbill_create_ip){
callback("缺少客户端IP");
return
}
if(!textInfo){
callback("缺少商品描述,格式:app名称--商品名");
return
}
if(!total_fee || total_fee < 1){
callback("商品总金额必须大于1");
return
}
if(expireTime && expireTime <= 0){
callback("订单超时时间应大于0,并小于或等于2小时");
return
}
expireTime = ( expireTime && expireTime > timeout_express ? timeout_express : expireTime )|| timeout_express;
const nonce_str = common.randomWord(false,30);//此方法是用来生成随机数的,请自行封装
let nowDate = new Date();
const time_start = nowDate.Format("yyyy MM dd hh mm ss").replace(/s/g,"");
let expireTimeTemp = new Date((nowDate.getTime()) expireTime*60*60*1000);
const time_expire = expireTimeTemp.Format("yyyy MM dd hh mm ss").replace(/s/g,"");
const out_trade_no =common.createOut_trade_no();//生成订单号方法,请自行封装
let subObj = {
appid,
mch_id,//商户号
device_info:"WEB",
nonce_str,
body:textInfo,
out_trade_no,
total_fee,//单位为分
spbill_create_ip,
notify_url:callBackUrl,
trade_type,
time_start,
time_expire
};
const jsonxml = exports.json2xml(subObj,key);
let requestUrl = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
if(jsonxml){
const PayInfo = await new Promise((resolve,reject)=>{
let requestOptions = {
body:jsonxml
};
common.post_https_requestXml(requestUrl,requestOptions,async (xmlRes)=>{
try{
console.log(xmlRes);
if(xmlRes.indexOf("xml")>=0){
let ParaseXml= await exports.parseXml(xmlRes);
resolve(ParaseXml);
}else{
resolve({
success:false,
msg:xmlRes
});
}
}catch(e){
reject(e)
}
})
})
let {prepay_id} = PayInfo;
console.log(PayInfo);
let ClientPayConfig = exports.getClientPayConfig(appid,key,mch_id,prepay_id,out_trade_no);//将返回的信息构造为json格式返回给客户端,以便以调起微信支付,下面会有实现方法
if(ClientPayConfig){
let resultInfo = {
success:true,
info:ClientPayConfig
};
callback(null,resultInfo)
}else{
console.log("统一下单wechatUnifiedorder:构造客户端返回信息异常");
callback("统一下单wechatUnifiedorder:构造客户端返回信息异常")
}
}else{
console.log("统一下单wechatUnifiedorder:构造xml或签名异常");
callback("统一下单wechatUnifiedorder:构造xml或签名异常")
}
}catch(e){
console.log(e);
callback(e)
}
};
/**
* 生成前端调启支付界面的必要参数
* @param {String} appid 应用ID,微信开放平台审核通过的应用APPID(请登录open.weixin.qq.com查看,注意与公众号的APPID不同),
* @param {String} key 商户平台设置的密钥key
* @param {String} partnerid 微信支付分配的商户号
* @param {String} prepayid 微信返回的支付交易会话ID
* @param {String} out_trade_no 订单号
* return 正常返回签名后的对象,否则返回null
*/
exports.getClientPayConfig = (appid,key,partnerid,prepayid,out_trade_no)=>{
let obj = {
appid,
timestamp: String(Math.floor(Date.now()/1000)),
noncestr: common.randomWord(false,30),
prepayid,
partnerid,
package: 'Sign=WXPay',
// signType: 'MD5'
};
obj = exports.getWechatSign(obj,key);
if(obj){
obj.out_trade_no = out_trade_no;
return obj;
}else{
return null
}
};
统一下单所需要的所有方法都以及完成了,接口所需要做的就是传递相应的参数即可,后面我会继续介绍其他的接口实现方法。