node.js之微信小程序支付和退款

1. 前期准备

需要用到的资料和账号

· AppID(小程序ID),AppSecret(小程序密钥)

· 商户号(mchid)

· 微信支付证书源文件,微信支付API证书序列号

· 商户号APIv3秘钥,用于微信支付成功后回调

其中商户号需要凭营业执照才能申请,个人是无法接入微信支付的。申请到商户号之后还需要在微信小程序的管理平台关联一下商户号。

image.png

然后还需要去申请公钥和私钥证书。具体的申请流程可看下方微信官方的文档:
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_1.shtml(小程序支付接入准备)

2.开发必备插件

看了下微信支付的官方文档,微信官方只提供了java、php还有Go语言的sdk,并没有node.js,后面逛了一圈社区发现wechatpay-node-v3这款插件,是专门针对node后台服务进行微信支付的工具。具体可参考:

https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_2.shtml(小程序支付开发指引)

3.插件引入和使用

根目录安装:

npm install wechatpay-node-v3 fs

fs用于读取公钥和私钥证书。将下载好的证书放置项目内同一目录。

image.png

在路由内引入:

 const WxPay = require('wechatpay-node-v3'); // 支持使用require
 const fs = require('fs');
 const path = require("path");

 const apiclient_cert = path.resolve(__dirname, 'apiclient_cert.pem');
 const apiclient_key = path.resolve(__dirname, 'apiclient_key.pem');
 const pay = new WxPay({
        appid: 'wx4beb40ab8exxxxxx',//小程序appid
        mchid: '1639xxxxxx',//商户号
        publicKey: fs.readFileSync(apiclient_cert), // 公钥
        privateKey: fs.readFileSync(apiclient_key), // 秘钥
 });

注意:本篇 node.js服务是基于Express应用框架搭建。

3.小程序服务端预设微信下单数据

router.post('/order/wx/pay', async (req, res) => {
        const userId = req.user._conditions.userId;
       //自己生成订单号(如果是待付款订单再次支付,不再生成新订单)
        let orderNumber = req.body.orderNumber ? req.body.orderNumber : tools.orderNumber();
        const params = {
            appid: 'wx4beb40ab8exxxxxx',
            mchid: '1639xxxxxx',
            description: '订单支付',
            out_trade_no: orderNumber, 
            notify_url: 'https://lxxxxx.cn/web/api/notify_order',
            amount: {
                total: Math.ceil(Number(req.body.money)*100),//向上取整解决科学计数法问题
                currency: "CNY"
            },
            payer: {
                openid: userId
            }
        };
        const result = await pay.transactions_jsapi(params);
       
         //订单详情再次支付不再生成订单
        if(!req.body.orderNumber ){
            let obj = {
                orderNumber: orderNumber, //订单号
                createdTime: tools.createdTime(), //订单时间
                createdUser: userId,
                goodsList: req.body.goodsList,//商品信息
                money: req.body.money,//支付钱数
                orderStatus: 0, //0 未支付 1已支付(未配送) 2已完成(已支付配送完成)  3订单取消
                discountMoney: req.body.discountMoney,//折扣信息
                payType:  req.body.payType,//支付方式 微信:1  余额: 2
                delivery: req.body.delivery,//配送信息
                address:  req.body.address,//收货地址信息
            }
            //生成未支付订单
            await order.create(obj);
        }
        res.send({
            code: 200,
            data: result,
            message: "查询成功",
        });
    });

说明:以上代码需要特别注意的是notify_url参数对应的地址; 该url是当用户成功支付之后微信服务器就会向这个回调url发送支付结果的信息,一般我们是在这个回调url里面进行一些支付成功之后的业务处理,而且这个回调url是需要ssl证书认证的也就是https,且在链接后面不能携带参数。url示例:
https://lxxxxx.cn/web/api/notify_order

注意:这个回调url必须能公网访问的哦,不能是本地环境的链接

由于pay.transactions_jsapi返回的是一个promise对象,因此我们使用async和await函数进行接收结果,其中result就是微信小程序api发起支付所需要的参数。

例如我在项目里的回调处理大致如下:

 router.post('/notify_order', async (req, res) => {
        try {
            // 申请的APIv3
            let key = '3SdsdfdfGK2Yuehi67UH3xxxxxxxxx';
            let {
                ciphertext,
                associated_data,
                nonce
            } = req.body.resource;
            // 解密回调信息
            const result = pay.decipher_gcm(ciphertext, associated_data, nonce, key);
            if (result.trade_state === 'SUCCESS') {
                const orderInfo = await order.findOne({
                    orderNumber: result.out_trade_no
                });
                if(orderInfo.orderStatus === 0){
                    await order.updateOne({
                        orderNumber: result.out_trade_no
                    }, {
                        $set: {
                            orderStatus: 1,
                            transactionId: result.transaction_id
                        }
                    })
                    //删除购物车对应商品
                    let _ids = [];
                    let domStr="";//发送订单邮件使用
                    orderInfo.goodsList.forEach((v,i)=>{
                        _ids.push(v._id)
                        domStr += `<div>商品${i+1}:</div>
                        <div>
                        <div>商品名称:${v.goodsName}</div>
                        <div>商品规格:${v.specification}</div>
                        <div>数量:${v.quantity}</div>
                        <div><image style="height:350px;width:350px" src=${v.mainImage}></image></div>
                        </div>`
                    })

                    //删除购物车数据
                    await shopcart.remove({
                        userId: orderInfo.createdUser,
                        _id: {$in: _ids},
                    })

                    //发送邮件给商家提醒
                    sendMail("54357xxx@qq.com","您有新的订单!",
                    `订单编号:${orderInfo.orderNumber}<br/> 
                    下单时间:${orderInfo.createdTime}<br/> 
                    订单金额:${orderInfo.money}元<br/> 
                    收货人:${orderInfo.address.contactName},${orderInfo.address.mobile}<br/> 
                    收货地址:${orderInfo.address.mainAddress + orderInfo.address.detailAddress}<br/> 
                    送达时间:${orderInfo.delivery.deliveryTime}<br/> 
                    订单备注:${orderInfo.delivery.remark || '无'}<br/> 
                    商品详情:` + domStr)

                    res.status(200);
                    res.send({
                        code: 'SUCCESS',
                        message: "成功",
                    });
                    
                }
            }
            
        } catch (error) {
            res.status(500);
            res.send({
                code: 'FAIL',
                message: "失败",
            });
        }
    });

根据上面代码可以看出,我在微信支付回调的url中首先判断处理状态trade_state === 'SUCCESS';其次再根据订单号查询该订单的支付信息,如果还是未支付状态这个时候就可以修改成支付完成状态了;最后发送邮件给商家告知有一笔新订单。

4.小程序前端下单部分代码

//微信支付 调用后端服务的 /order/wx/pay  接口
            wechatPay() {
                let params = {
                    address: this.info.addressInfo,
                    goodsList: this.info.shopcartInfo,
                    money: this.info.money,
                    discountMoney: this.info.discountMoney,
                    delivery: this.model,
                    payType: this.payType,
                }
                let _this = this;
                wechatPay(params).then(res => {
                    if (res.code === 200) {
                        uni.requestPayment({
                            provider: 'wxpay',
                            timeStamp: res.data.timeStamp,
                            nonceStr: res.data.nonceStr,
                            package: res.data.package,
                            signType: 'RSA',
                            paySign: res.data.paySign,
                            success: function(res) {
                                _this.$refs.uToast.show({
                                    type: 'success',
                                    message: '支付成功',
                                })
                                setTimeout(() => {
                                    _this.goBack();
                                }, 1500)
                            },
                            fail: function(err) {
                                uni.$u.toast("支付取消")
                            }
                        });
                    }
                })
            }

不难看出上面的代码在调用接口成功后返回了微信支付需要的一系列参数;在小程序前端我使用的是uniapp的uni.requestPayment方法调取微信支付,该方法需要的参数也在后端接口进行了返回,至此微信小程序一整套的支付流程就结束了。

5.小程序微信支付退款

退款和支付类似也一样有一个notify_url回调地址,代码如下:

router.post('/order/wx/refund',async (req,res)=>{
        let rNum = tools.refundOrderNumber()//自己生成退款单号
        let params = {
            out_trade_no: req.body.out_trade_no,//原订单号
            out_refund_no: rNum,
            notify_url:'https://lxxxxx.cn/web/api/notify_refund',
            amount:{
                refund: Math.ceil(Number(req.body.money)*100),
                total: Math.ceil(Number(req.body.money)*100),
                currency: 'CNY'
            }
        }
        const result = await pay.refunds(params);
         res.send({
             code: 200,
             data: result,
             message: "操作成功",
         });
     });

    //微信支付退款回调通知
    router.post('/notify_refund', async (req, res) => {
        try {
            // 申请的APIv3
            let key = '3SdsdfdfGK2Yuehi67UH3xxxxxxxxx';
            let {
                ciphertext,
                associated_data,
                nonce
            } = req.body.resource;
            // 解密回调信息
            const result = pay.decipher_gcm(ciphertext, associated_data, nonce, key);
            // logger.info("解密回调参数 result==",result)
            if (result.refund_status === 'SUCCESS') {
                const orderInfo = await order.findOne({
                    orderNumber: result.out_trade_no
                });
                if(orderInfo.orderStatus === 4){
                    await order.updateOne({
                        orderNumber: result.out_trade_no
                    }, {
                        $set: {
                            orderStatus: 5,//从退款中状态修改为退款成功状态
                            refundOrderNumber: result.out_refund_no
                        }
                    })
                    res.status(200);
                    res.send({
                        code: 'SUCCESS',
                        message: "成功",
                    });
                }
            }
            
        } catch (error) {
            res.status(500);
            res.send({
                code: 'FAIL',
                message: "失败",
            });
        }
    });

小程序前端调用/order/wx/refund接口,服务端在微信支付退款回调通知里处理订单状态,至此小程序微信支付退款也完成了。
如果文档内有描述有误的地方还请各位指出,谢谢!本篇over~

参考文档:
https://www.npmjs.com/package/wechatpay-node-v3
https://blog.csdn.net/weixin_45952249/article/details/126216205

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

推荐阅读更多精彩内容