前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >nodejs开发微信支付之统一下单

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

作者头像
OECOM
发布2020-07-01 16:52:44
1.6K0
发布2020-07-01 16:52:44
举报
文章被收录于专栏:OECOMOECOM

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
    }

};

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 统一下单
相关产品与服务
云开发 CloudBase
云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档