Nodejs ·

nodejs开发微信支付之统一下单

nodejs开发微信支付接口

文本主要讲解如何使用nodejs来对接微信支付,对接以app支付为例说明。

首先我们需要来看一下后台具体都需要做哪些功能:
- 统一下单
- 接收订单结果通知
- 查询订单
- 申请退款
- 查询退款
- 退款结果通知接收

后面我会逐步说一下具体的实现方法,做这些工作之前需要做一些准备工作。首先是一些必要的微信参数:appid、appsecret、mchid、key,双向证书(nodejs开发使用的证书是以.p12为后缀的文件)。

然后需要准备的就是一些开发模块了,本文介绍的nodejs框架为express。需要额外安装的一个模块就是xml2js,因为微信返回的一些信息都是xml格式的,需要使用这个模块进行解析。

模块准备完了,我们就可以进行开发了。

统一下单

我们先来做的是统一下单这个接口,基本流程是由客户端发起请求,服务器接到请求后调用微信统一下单接口,生成订单,然后服务器将微信服务器返回的信息返回给客户端,客户端通过这些信息来拉起微信支付。至此,统一下单流程就结束了。

下面我们需要来看一下该如何实现。因为需要发起请求,我们这里将发送请求封装成一个方法,便于后续的重复使用,我们将它命名为common.js,在这个方法中还需要封装一些其他的方法,比如时间格式化,请看下面代码:

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数据的,在这个方法里面有一个特别的地方,那就是

 if(post_data.agentOptions){
        options.pfx = post_data.agentOptions.pfx;
        options.passphrase = post_data.agentOptions.passphrase;
    }

这段代码。它的作用是为了退款准备的,退款的接口需要双向证书验证的,pfx代表的是证书内容,passphrase代表证书密码,如此一来我们就无需将证书安装到本地计算机了,将其携带发送就可以了。

好了,退款的相关介绍后面会有介绍,我们这里先重点说统一下单。我们将这个文件命名为pay.js。微信的所有接口都需要进行签名验证的,具体算法说明可以直接看官方的文档,我们这里还看具体的实现方法。

/**
 * 微信签名封装
 * @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:

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);
            }
        });
    })

};

至此,基本的准备工作做完了,我们可以进行主体开发了:

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
    }

};

统一下单所需要的所有方法都以及完成了,接口所需要做的就是传递相应的参数即可,后面我会继续介绍其他的接口实现方法。

参与评论