Node.js和uni-app實現(xiàn)微信小程序支付

前言

自己實現(xiàn)一個帶支付功能的小程序摄职,前端使用uniapp几颜,后端使用Node.js,將實現(xiàn)微信小程序支付功能的全流程詳細記錄下來穷娱。使用的是全新的微信支付 APIv3

用戶付款流程

  1. 如圖1绑蔫,用戶通過分享或掃描二維碼進入商戶小程序,用戶選擇購買泵额,完成選購流程配深。

  2. 如圖3,調起微信支付控件嫁盲,用戶開始輸入支付密碼篓叶。


    WX20240827-202827.png
  3. 如圖4,密碼驗證通過羞秤,支付成功缸托。商戶后臺得到支付成功的通知。

  4. 如圖5瘾蛋,返回商戶小程序俐镐,顯示購買成功。

  5. 如圖6哺哼,微信支付公眾號下發(fā)支付憑證佩抹。


    WX20240827-202944.png

業(yè)務流程圖

6_2.png

具體操作流程

  • 用戶通過分享或掃描二維碼進入商戶小程序,用戶選擇購買取董,完成選購流程
  • 調用 wx.login 獲取用戶臨時登錄憑證code棍苹,發(fā)送到后端服務器換取openId
  • 在下單時,小程序需要將購買的商品Id甲葬,商品數(shù)量廊勃,以及用戶的openId傳送到服務器
  • 服務器在接收到商品Id懈贺、商品數(shù)量经窖、openId后坡垫,生成服務期訂單數(shù)據(jù),同時經(jīng)過一定的簽名算法画侣,向微信支付發(fā)送請求冰悠,獲取預付單信息prepay_id,同時將獲取的數(shù)據(jù)再次進行相應規(guī)則的簽名配乱,向小程序端響應必要的信息
  • 小程序端在獲取對應的參數(shù)后溉卓,調用wx.requestPayment發(fā)起微信支付,喚醒支付工作臺搬泥,進行支付
  • 接下來的一些列操作都是由用戶來操作的包括了微信支付密碼桑寨,指紋等驗證,確認支付之后執(zhí)行鑒權調起支付
  • 鑒權調起支付:在微信后臺進行鑒權忿檩,微信后臺直接返回給前端支付的結果尉尾,前端收到返回數(shù)據(jù)后對支付結果進行展示
  • 推送支付結果:微信后臺在給前端返回支付的結果后,也會向后臺也返回一個支付結果燥透,后臺通過這個支付結果來更新訂單的狀態(tài)

準備工作

  • appid: 微信公眾平臺分配的小程序AppID沙咏。
  • secret: 微信公眾平臺分配的小程序AppSecret。
  • mchid: 微信支付商戶號班套。
  • mchKey: 微信支付商戶密鑰肢藐。
  • serial_no: 商戶API證書序列號。
  • publicKey: 公鑰吱韭。
  • privateKey: 秘鑰吆豹。
  • 微信支付文檔

前端 uni-app 實現(xiàn)

<template>
    <view style="display: flex;align-items: center;justify-content: center;height: 100vh;">
        <button @click="wechatPay">微信支付</button>
    </view>
</template>
<script setup>
    import { ref } from 'vue'
    const openId = ref(null)

    /**
     * 微信支付
     */
    const wechatPay = async () => {
        console.log('wechatPay',openId.value);
        if (!openId.value) {
            await wechatLogin()
        }
        await submitOrder()
    }
    
    /**
     * 微信登錄
     */
    function wechatLogin() {
        return new Promise((resolve, reject) => {
            // 1、獲取臨時登錄憑證 code
            uni.login({
                provider: "weixin",
                success: ({ code }) => {
                    // 2理盆、將獲取到的 code 發(fā)送到后端瞻讽,用于后端向微信獲取 openId 。
                    uni.request({
                        url: 'http://yourdomain.com/api/weChatMp/login',
                        method: 'POST',
                        data: { code },
                        success: (loginRes) => {
                            // 3熏挎、將后端返回的 openId 進行保存
                            openId.value = loginRes.data.data.openId
                            resolve(loginRes)
                        },
                        fail: () => {
                            reject('登錄失斔儆隆!')
                            uni.showToast({
                                title: '登錄失斂补铡烦磁!',
                                icon: 'error',
                                duration: 1500
                            });
                        }
                    })
                },
                fail: () => {
                    reject('登錄出錯')
                    uni.showToast({
                        title: '登錄出錯!',
                        icon: 'error',
                        duration: 1500
                    });
                }
            })
        })
    }

    /**
     * 提交訂單并支付
     */
    function submitOrder() {
        return new Promise((resolve, reject) => {
            // 1哼勇、將 openId 以及相應需要的商品信息發(fā)送到后端
            uni.request({
                url: 'http://yourdomain.com/api/weChatMp/createOrder',
                method: 'POST',
                data: {
                    openId: openId.value, // 后臺返回的 openId
                    productNum: 2, // 假設這是商品數(shù)量
                    productId: '12345678', // 假設這是商品 id都伪,
                    productName: '小米13Pro白色6G版', // 假設這是商品名稱,
                    productPrice: '9.9' // 假設這是商品價格积担,
                },
                success(ores) {
                    // 2陨晶、將后端返回的信息填寫到 wx.requestPayment,喚起微信小程序支付
                    const result = ores.data.data
                    uni.requestPayment({
                        provider: 'wxpay',
                        // orderInfo: result,
                        nonceStr: result.nonceStr, // 隨機字符串,長度為32個字符以下
                        package: result.package, // 統(tǒng)一下單接口返回的 prepay_id 參數(shù)值先誉,提交格式如:prepay_id=***
                        paySign: result.paySign, // 簽名湿刽,具體見微信支付文檔
                        signType: result.signType,  // 簽名算法,應與后臺下單時的值一致
                        timeStamp: result.timeStamp, // 時間戳褐耳,從 1970 年 1 月 1 日 00:00:00 至今的秒數(shù)诈闺,即當前的時間
                        success(payRes) {
                            console.log('支付成功', payRes);
                            uni.showModal({
                                title: '支付提示',
                                content: '支付成功',
                                showCancel: false,
                            })
                            resolve(payRes)
                        },
                        fail(payErr) {
                            if(payErr?.errMsg === 'requestPayment:fail cancel'){
                                reject('支付已取消!')
                                uni.showToast({
                                    title: '支付已取消铃芦!',
                                    icon: 'error',
                                    duration: 1500
                                });
                            }else{
                                console.log('支付失敗雅镊,請重新嘗試!', payErr);
                                reject('支付失敗刃滓,請重新嘗試仁烹!')
                                uni.showModal({
                                    title: '支付提示',
                                    content: '支付失敗,請重新嘗試',
                                    showCancel: false,
                                })
                            }
                        }
                    });
                },
                fail(err) {
                    reject('訂單出錯咧虎!', err)
                    uni.showToast({
                        title: '訂單出錯晃危!',
                        icon: 'error',
                        duration: 1500
                    });
                }
            })
        })
    }
</script>

后端 Nodejs 實現(xiàn)

// 微信支付
const express = require('express'); // 導入 Express 模塊
const cors = require('cors'); // 導入 CORS 模塊,用于處理跨域請求
const fs = require('node:fs'); // node內置模塊老客,用于文件系統(tǒng)操作僚饭。
const path = require('node:path');//node內置模塊,用于處理文件路徑胧砰。
const axios = require('axios'); // 導入 Axios 模塊鳍鸵,用于發(fā)起 HTTP 請求
const app = express(); // 創(chuàng)建 Express 應用實例
const WxPay = require('wechatpay-node-v3'); // 導入微信支付模塊
const bodyParser = require('body-parser');  // 中間件,用于解析req.body
const crypto = require('crypto'); // 加密模塊

app.use(cors()); // 使用 CORS 中間件解決跨越請求
app.use(bodyParser.json());  // 使用中間件解析JSON數(shù)據(jù)

// 配置靜態(tài)文件服務尉间,使得/public路徑下的文件可以直接訪問偿乖,如果沒有請手動創(chuàng)建
app.use('/public', express.static(path.join(__dirname, 'public')));

const port = 3000; // 設置應用監(jiān)聽的端口號

// 微信小程序配置信息
const wxConfig = {
  appid: 'your_app_id',
  secret: 'your_app_secret',
  mchid: 'your_mch_id', //商戶號
  serial_no: 'your_serial_no', //API證書 - 商戶API證書序列號 
  publicKey: fs
    .readFileSync(`${__dirname}/certificate/apiclient_cert.pem`)
    .toString(), // 公鑰
  privateKey: fs
    .readFileSync(`${__dirname}/certificate/apiclient_key.pem`)
    .toString(), // 秘鑰
  apiV3Key: 'your_apiV3Key', //apiV3 秘鑰值
  mchKey: 'your_mch_key', // 微信支付商戶密鑰
}

const wechatPay = new WxPay({
  appid: wxConfig.appid,
  mchid: wxConfig.mchid,
  publicKey: wxConfig.publicKey, // 公鑰
  privateKey: wxConfig.privateKey, // 秘鑰
});

// 微信用戶登陸
app.post('/api/weChatMp/login', async (req, res) => {

  const code = req.body?.code

  if (!code) {
    throw new Error('code參數(shù)不能為空')
  }

  const url = `https://api.weixin.qq.com/sns/jscode2session`;

  const response = await axios({
    method: "GET",
    url,
    params: {
      appid: wxConfig.appid,
      secret: wxConfig.secret,
      js_code: code,
      grant_type: 'authorization_code',
    },
  });

  if (response?.errcode) {
    throw new Error(JSON.stringify(response))
  }

  res.send({
    msg: '獲取openid成功',
    data: {
      openId: response.data.openid,
    },
  })
})

// 創(chuàng)建訂單
app.post('/api/weChatMp/createOrder', async (req, res) => {

  const openId = req.body.openId // openId
  const productId = req.body.productId // 假設這是商品 id,
  const productName = req.body.productName // 假設這是商品名稱哲嘲,
  let productPrice = req.body.productPrice // 假設這是商品價格
  const productNum = req.body.productNum || 1 // 假設這是商品數(shù)量

  if (!openId || !openId || !productId || !productName || !productPrice) {
    throw new Error('必要參數(shù)不能為空')
  }

  if (typeof productPrice === 'string') {
    productPrice = Number(productPrice)
  }
  productPrice = Math.round(productPrice * 100); // 將元轉換為分

  const wechatPay = new WxPay({
    appid: wxConfig.appid,
    mchid: wxConfig.mchid,
    publicKey: wxConfig.publicKey, // 公鑰
    privateKey: wxConfig.privateKey, // 秘鑰
  });

  const params = {
    description: `購買商品:${productName} x${productNum}`, // 商品描述
    out_trade_no: Date.now().toString(), // 商戶訂單號
    notify_url: 'http://yourdomain.com/api/weChatMp/notify', // 通知地址
    amount: { // 訂單金額信息
      total: productPrice, // 訂單總金額贪薪,單位為分。
    },
    payer: { //【支付者】 支付者信息眠副。
      openid: openId, // 【用戶標識】 用戶在普通商戶AppID下的唯一標識画切。 下單前需獲取到用戶的OpenID
    },
    scene_info: { // 【場景信息】 支付場景描述
      payer_client_ip: '14.23.150.211', // 【用戶終端IP】 用戶的客戶端IP,支持IPv4和IPv6兩種格式的IP地址囱怕。
    },
  };

  // 調用小程序下單接口
  const result = await wechatPay.transactions_jsapi(params);

  res.send(result)
})

// 通知回調處理
app.get('/api/weChatMp/notify', async (req, res) => {

  res.send({
    msg: "支付成功",
    data: null
  })
})

// 監(jiān)聽端口
app.listen(port, () => {
  console.log(`Example app listening on port ${port}`, `is open url http://127.0.0.1:${port}`)
})

效果演示

效果演示
WechatIMG379.jpg
WechatIMG378.jpg
1.gif

結尾

通過以上步驟霍弹,你可以實現(xiàn)uni-app和Node.js環(huán)境下的微信小程序支付功能。確保所有配置信息正確無誤娃弓,并且按照微信支付的官方文檔進行操作典格。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市台丛,隨后出現(xiàn)的幾起案子耍缴,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,406評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件防嗡,死亡現(xiàn)場離奇詭異变汪,居然都是意外死亡,警方通過查閱死者的電腦和手機本鸣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來硅蹦,“玉大人荣德,你說我怎么就攤上這事⊥郏” “怎么了涮瞻?”我有些...
    開封第一講書人閱讀 167,815評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長假褪。 經(jīng)常有香客問我署咽,道長,這世上最難降的妖魔是什么生音? 我笑而不...
    開封第一講書人閱讀 59,537評論 1 296
  • 正文 為了忘掉前任宁否,我火速辦了婚禮,結果婚禮上缀遍,老公的妹妹穿的比我還像新娘慕匠。我一直安慰自己,他們只是感情好域醇,可當我...
    茶點故事閱讀 68,536評論 6 397
  • 文/花漫 我一把揭開白布台谊。 她就那樣靜靜地躺著,像睡著了一般譬挚。 火紅的嫁衣襯著肌膚如雪锅铅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,184評論 1 308
  • 那天减宣,我揣著相機與錄音盐须,去河邊找鬼。 笑死漆腌,一個胖子當著我的面吹牛丰歌,可吹牛的內容都是我干的。 我是一名探鬼主播屉凯,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼立帖,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了悠砚?” 一聲冷哼從身側響起晓勇,我...
    開封第一講書人閱讀 39,668評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后绑咱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绰筛,經(jīng)...
    沈念sama閱讀 46,212評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,299評論 3 340
  • 正文 我和宋清朗相戀三年描融,在試婚紗的時候發(fā)現(xiàn)自己被綠了铝噩。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,438評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡窿克,死狀恐怖骏庸,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情年叮,我是刑警寧澤具被,帶...
    沈念sama閱讀 36,128評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站只损,受9級特大地震影響一姿,放射性物質發(fā)生泄漏。R本人自食惡果不足惜跃惫,卻給世界環(huán)境...
    茶點故事閱讀 41,807評論 3 333
  • 文/蒙蒙 一叮叹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爆存,春花似錦衬横、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拇泣,卻和暖如春噪叙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背霉翔。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評論 1 272
  • 我被黑心中介騙來泰國打工睁蕾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人债朵。 一個月前我還...
    沈念sama閱讀 48,827評論 3 376
  • 正文 我出身青樓子眶,卻偏偏與公主長得像,于是被迫代替她去往敵國和親序芦。 傳聞我的和親對象是個殘疾皇子臭杰,可洞房花燭夜當晚...
    茶點故事閱讀 45,446評論 2 359

推薦閱讀更多精彩內容