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

1. 前期準備

需要用到的資料和賬號

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

· 商戶號(mchid)

· 微信支付證書源文件,微信支付API證書序列號

· 商戶號APIv3秘鑰,用于微信支付成功后回調(diào)

其中商戶號需要憑營業(yè)執(zhí)照才能申請,個人是無法接入微信支付的。申請到商戶號之后還需要在微信小程序的管理平臺關聯(lián)一下商戶號。

image.png

然后還需要去申請公鑰和私鑰證書。具體的申請流程可看下方微信官方的文檔:
https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_1.shtml(小程序支付接入準備)

2.開發(fā)必備插件

看了下微信支付的官方文檔,微信官方只提供了java、php還有Go語言的sdk,并沒有node.js,后面逛了一圈社區(qū)發(fā)現(xiàn)wechatpay-node-v3這款插件,是專門針對node后臺服務進行微信支付的工具。具體可參考:

https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_2.shtml(小程序支付開發(fā)指引)

3.插件引入和使用

根目錄安裝:

npm install wechatpay-node-v3 fs

fs用于讀取公鑰和私鑰證書。將下載好的證書放置項目內(nèi)同一目錄。

image.png

在路由內(nèi)引入:

 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.小程序服務端預設微信下單數(shù)據(jù)

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),//向上取整解決科學計數(shù)法問題
                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,//支付錢數(shù)
                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參數(shù)對應的地址; 該url是當用戶成功支付之后微信服務器就會向這個回調(diào)url發(fā)送支付結果的信息,一般我們是在這個回調(diào)url里面進行一些支付成功之后的業(yè)務處理,而且這個回調(diào)url是需要ssl證書認證的也就是https,且在鏈接后面不能攜帶參數(shù)。url示例:
https://lxxxxx.cn/web/api/notify_order

注意:這個回調(diào)url必須能公網(wǎng)訪問的哦,不能是本地環(huán)境的鏈接

由于pay.transactions_jsapi返回的是一個promise對象,因此我們使用async和await函數(shù)進行接收結果,其中result就是微信小程序api發(fā)起支付所需要的參數(shù)。

例如我在項目里的回調(diào)處理大致如下:

 router.post('/notify_order', async (req, res) => {
        try {
            // 申請的APIv3
            let key = '3SdsdfdfGK2Yuehi67UH3xxxxxxxxx';
            let {
                ciphertext,
                associated_data,
                nonce
            } = req.body.resource;
            // 解密回調(diào)信息
            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="";//發(fā)送訂單郵件使用
                    orderInfo.goodsList.forEach((v,i)=>{
                        _ids.push(v._id)
                        domStr += `<div>商品${i+1}:</div>
                        <div>
                        <div>商品名稱:${v.goodsName}</div>
                        <div>商品規(guī)格:${v.specification}</div>
                        <div>數(shù)量:${v.quantity}</div>
                        <div><image style="height:350px;width:350px" src=${v.mainImage}></image></div>
                        </div>`
                    })

                    //刪除購物車數(shù)據(jù)
                    await shopcart.remove({
                        userId: orderInfo.createdUser,
                        _id: {$in: _ids},
                    })

                    //發(fā)送郵件給商家提醒
                    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: "失敗",
            });
        }
    });

根據(jù)上面代碼可以看出,我在微信支付回調(diào)的url中首先判斷處理狀態(tài)trade_state === 'SUCCESS';其次再根據(jù)訂單號查詢該訂單的支付信息,如果還是未支付狀態(tài)這個時候就可以修改成支付完成狀態(tài)了;最后發(fā)送郵件給商家告知有一筆新訂單。

4.小程序前端下單部分代碼

//微信支付 調(diào)用后端服務的 /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("支付取消")
                            }
                        });
                    }
                })
            }

不難看出上面的代碼在調(diào)用接口成功后返回了微信支付需要的一系列參數(shù);在小程序前端我使用的是uniapp的uni.requestPayment方法調(diào)取微信支付,該方法需要的參數(shù)也在后端接口進行了返回,至此微信小程序一整套的支付流程就結束了。

5.小程序微信支付退款

退款和支付類似也一樣有一個notify_url回調(diào)地址,代碼如下:

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: "操作成功",
         });
     });

    //微信支付退款回調(diào)通知
    router.post('/notify_refund', async (req, res) => {
        try {
            // 申請的APIv3
            let key = '3SdsdfdfGK2Yuehi67UH3xxxxxxxxx';
            let {
                ciphertext,
                associated_data,
                nonce
            } = req.body.resource;
            // 解密回調(diào)信息
            const result = pay.decipher_gcm(ciphertext, associated_data, nonce, key);
            // logger.info("解密回調(diào)參數(shù) 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,//從退款中狀態(tài)修改為退款成功狀態(tài)
                            refundOrderNumber: result.out_refund_no
                        }
                    })
                    res.status(200);
                    res.send({
                        code: 'SUCCESS',
                        message: "成功",
                    });
                }
            }
            
        } catch (error) {
            res.status(500);
            res.send({
                code: 'FAIL',
                message: "失敗",
            });
        }
    });

小程序前端調(diào)用/order/wx/refund接口,服務端在微信支付退款回調(diào)通知里處理訂單狀態(tài),至此小程序微信支付退款也完成了。
如果文檔內(nèi)有描述有誤的地方還請各位指出,謝謝!本篇over~

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

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容