nodejs 微信支付 (koa 框架)

nodejs实现微信支付,koa框架的接入流程,此文档通过测试,在小程序/微信公众号的后端代码中使用, 已经用于生产环境, 如有需要,请放心复制.

根据官方微信支付的文档
和流程图

image

支付过程可以分为后端流程前端流程

后端流程

后端分为2步

  1. 根据用户的下单请求调用微信统一下单api拿到返回的关键数据prepay_id

export const prepay = async ({openid,orderId,desc,totalPrice,spbill_create_ip})=> {
    // 通过查阅文档,调用统一下单有10个参数是必须的
    let obj = {
        appid,
        mch_id,
        nonce_str: get_nonce_str(32),
        body: desc,
        out_trade_no: orderId,
        total_fee: parseInt(totalPrice * 100),
        spbill_create_ip,
        notify_url,
        trade_type:'JSAPI',
        openid
    }
    // js的默认排序即为ASCII的从小到大进行排序(字典排序)
    let arr = Object.keys(obj).sort().map(item => {
        return `${item}=${obj[item]}`;
    });
       // 这里拼接签名字符串的时候一定要注意: 商户的key是要单独拿出来拼在最后面的
    let str = arr.join('&') + '&key=' + key;
    // appid=wxf8600b***b5dfb&body=德胜村&mch_id=1490909372&nonce_str=plfbp2bhr0id1z6aktmndfot94hkewcv&notify_url=https://server.***.cn/wechat/pay_notify&openid=oFm4h0WvnQWB4ocFmdPzsWywlE8c&out_trade_no=20150806125346&spbill_create_ip=127.0.0.1&total_fee=56600&trade_type=JSAPI&key=Lzy1234567890111***5161718192
    
    obj.sign = getSign(str);
    let res;
    try{
        // 调用微信统一下单接口拿到 prepay_id
        res = await wechatPay(obj);
        let {prepay_id} = res;
        if(prepay_id){
            res = getClientPayConfig(prepay_id)
        }
        // console.log(res);
    }catch(e){
        res = e;
        console.log(e);
    }
    return res;
}


/**
 * 统一下单 prepay_url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
 * @param {Object} obj 调用统一下单的必须参数
 */ 
const wechatPay = (obj)=>{
    let xml = json2xml(obj);
    console.log(xml)
    return new Promise((resolve,reject)=>{
        // 这里用了reques库,不熟悉的同学可以看看相关文档 https://github.com/request/request
        // 总之就是向微信的统一下单接口提交一个xml
        request({method:'POST',url: prepay_url,body: xml},(err,res, body)=>{
            if(err){
                reject(err);
            }else{
                //如果成功即可得到微信返回参数
                console.log(body);
                let obj = parseXml(body).xml;
                resolve(obj);
            }
        });
    });
}


/**
 * 对指定字符串进行md5加密
 * @param {String} str 
 */
const getSign = (str)=>{
    console.log(str)
    let hash = crypto.createHash('md5').update(str,'utf8');
    return hash.digest('hex').toUpperCase();
}

/**
 * 转化xml用了xml2js库  
    https://github.com/Leonidas-from-XIV/node-xml2js
 * @param {Object} obj 
 */
const json2xml = (obj)=>{
    let builder = new xml2js.Builder({
        headless:true,
        allowSurrogateChars: true,
        rootName:'xml',
        cdata:true
    });
    var xml = builder.buildObject(obj);
    return xml;
}

const parseXml = (xml)=>{
    let {parseString} = xml2js;
    let res;
    parseString(xml,  {
        trim: true,
        explicitArray: false
    }, function (err, result) {
        res = result;
    });
    return res;
} 

/**
 * 生成指定长度的随机数
 * @param {*int} len 
 */
const get_nonce_str = (len)=>{
    let str = '';
    while(str.length < len){
        str +=  Math.random().toString(36).slice(2);
    }
    return str.slice(-len);
}

  1. 通过prepay_id生成前端调启微信支付界面的必要参数
    官方文档
/**
 * 生成前端调启支付界面的必要参数
 * @param {String} prepay_id 
 */
const getClientPayConfig = (prepay_id)=>{
    let obj = {
        appId: appid,
        timeStamp: String(Math.floor(Date.now()/1000)),,
        nonceStr: get_nonce_str(32),
        package: 'prepay_id=' + prepay_id,
        signType: 'MD5'
    }
    let arr = Object.keys(obj).sort().map(item => {
        return `${item}=${obj[item]}`;
    });
    let str = arr.join('&') + '&key=' + key;
    obj.paySign = getSign(str);
    return obj;
}

前端流程

前端分为2步 官方文档

  1. 向后台提交支付订单的请求
  2. 拿到后台返回参数, 调启支付页面
 $api({
    method:'POST',
    url:'/order',
    data: obj,
    success:(data)=>{
        console.log(data);
        /**
         *  {
                appId:"wxf860****03b5dfb"
                nonceStr:"o5any3uj14tyfjr8wa249n5s0nnp9rkl"
                package:"prepay_id=wx20171104151803201b17a3100900948881"
                paySign:"D2632F71E4CB7E9E18D329460FDF5EB0"
                signType:"MD5"
                timeStamp:"1509779883"
            }
        */
        let obj = Object.assign({
            'success':function(res){
                wx.showModal({
                    title: '提示',
                    content: '支付成功',
                    showCancel: false
                });
            },
            'fail':function(res){
                wx.showModal({
                    title: '提示',
                    content: '取消支付',
                    showCancel: false
                });
            }
        },data)
        //调用小程序支付api,若为网页支付,查看相应文档即可. (若是spa网页支付,有一个支付目录的坑.最粗暴的方式:刷新进入支付页面)
        wx.requestPayment(obj)
    }
})

签名错误

根据经验 签名错误是xml的加密出错了
这里贴出一个提交统一下单的原始xml
这里说明一下: 经过亲测 spbill_create_ip, notify_url这两个参数即使是写死的也不是导致签名错误的原因


<xml>
    <appid>wxf8600b48303b5dfb</appid>
    <body>德胜村</body>
    <mch_id>1490909372</mch_id>
    <nonce_str>t7z9yb0wa8e5zhcwaw4ovjlrzj39t2xh</nonce_str>
    <notify_url>https://server.**.cn/wechat/pay_notify</notify_url>
    <openid>oFm4h0WvnQWB4ocFmdPzsWywlE8c</openid>
    <out_trade_no>20150806125346</out_trade_no>
    <spbill_create_ip>127.0.0.1</spbill_create_ip>
    <total_fee>56600</total_fee>
    <trade_type>JSAPI</trade_type>
    <sign>39FD69074F0B184D10CC5E826914785A</sign>
</xml>

<xml>
<return_code><![CDATA[FAIL]]></return_code>
<return_msg><![CDATA[签名错误]]></return_msg>
</xml>

遇到签名错误,不要着急,进行以下2步排查,定能解决问题

  1. 官方调试界面,输入自己的参数,看看最终的签名是否和自己生成的一致
  1. 如果签名没错,那肯定是商户信息的问题了(本人此处被坑了很久,老板给了我mch_idkey都是正确的,结果未安装操作证书,导致我调试了很久找不到原因,心中一万匹草泥马)

检查商户信息,也就是商户号mch_id和商户的key(这里需要注意key,是申请微信支付成功后,腾讯发给申请者邮件里面的秘钥,要想此秘钥生效还需要安装操作证书)

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

推荐阅读更多精彩内容