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

1. 前期準(zhǔn)備

需要用到的資料和賬號

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

· 商戶號(mchid)

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

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

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

image.png

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

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

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

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

3.插件引入和使用

根目錄安裝:

npm install wechatpay-node-v3 fs

fs用于讀取公鑰和私鑰證書堂竟。將下載好的證書放置項(xiàng)目內(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服務(wù)是基于Express應(yīng)用框架搭建。

3.小程序服務(wù)端預(yù)設(shè)微信下單數(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),//向上取整解決科學(xué)計(jì)數(shù)法問題
                currency: "CNY"
            },
            payer: {
                openid: userId
            }
        };
        const result = await pay.transactions_jsapi(params);
       
         //訂單詳情再次支付不再生成訂單
        if(!req.body.orderNumber ){
            let obj = {
                orderNumber: orderNumber, //訂單號
                createdTime: tools.createdTime(), //訂單時(shí)間
                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ù)對應(yīng)的地址漱牵; 該url是當(dāng)用戶成功支付之后微信服務(wù)器就會向這個(gè)回調(diào)url發(fā)送支付結(jié)果的信息,一般我們是在這個(gè)回調(diào)url里面進(jìn)行一些支付成功之后的業(yè)務(wù)處理疚漆,而且這個(gè)回調(diào)url是需要ssl證書認(rèn)證的也就是https酣胀,且在鏈接后面不能攜帶參數(shù)。url示例:
https://lxxxxx.cn/web/api/notify_order

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

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

例如我在項(xiàng)目里的回調(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
                        }
                    })
                    //刪除購物車對應(yīng)商品
                    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/> 
                    下單時(shí)間:${orderInfo.createdTime}<br/> 
                    訂單金額:${orderInfo.money}元<br/> 
                    收貨人:${orderInfo.address.contactName},${orderInfo.address.mobile}<br/> 
                    收貨地址:${orderInfo.address.mainAddress + orderInfo.address.detailAddress}<br/> 
                    送達(dá)時(shí)間:${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)這個(gè)時(shí)候就可以修改成支付完成狀態(tài)了;最后發(fā)送郵件給商家告知有一筆新訂單夷狰。

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

//微信支付 調(diào)用后端服務(wù)的 /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ù)也在后端接口進(jìn)行了返回沼头,至此微信小程序一整套的支付流程就結(jié)束了爷绘。

5.小程序微信支付退款

退款和支付類似也一樣有一個(gè)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接口进倍,服務(wù)端在微信支付退款回調(diào)通知里處理訂單狀態(tài)土至,至此小程序微信支付退款也完成了。
如果文檔內(nèi)有描述有誤的地方還請各位指出猾昆,謝謝陶因!本篇over~

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市垂蜗,隨后出現(xiàn)的幾起案子楷扬,更是在濱河造成了極大的恐慌,老刑警劉巖么抗,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毅否,死亡現(xiàn)場離奇詭異,居然都是意外死亡蝇刀,警方通過查閱死者的電腦和手機(jī)螟加,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人捆探,你說我怎么就攤上這事然爆。” “怎么了黍图?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵曾雕,是天一觀的道長。 經(jīng)常有香客問我助被,道長剖张,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任揩环,我火速辦了婚禮搔弄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘丰滑。我一直安慰自己顾犹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布褒墨。 她就那樣靜靜地躺著炫刷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪郁妈。 梳的紋絲不亂的頭發(fā)上浑玛,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機(jī)與錄音圃庭,去河邊找鬼锄奢。 笑死失晴,一個(gè)胖子當(dāng)著我的面吹牛剧腻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播涂屁,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼书在,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拆又?” 一聲冷哼從身側(cè)響起儒旬,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎帖族,沒想到半個(gè)月后栈源,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡竖般,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年甚垦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡艰亮,死狀恐怖闭翩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情迄埃,我是刑警寧澤疗韵,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站侄非,受9級特大地震影響蕉汪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜逞怨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一肤无、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧骇钦,春花似錦宛渐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鳞仙,卻和暖如春寇蚊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背棍好。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工仗岸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人借笙。 一個(gè)月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓扒怖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親业稼。 傳聞我的和親對象是個(gè)殘疾皇子盗痒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容