Vue3+Typescript+Node.js實現微信端公眾號H5支付(JSAPI v3)教程--各種填坑

老呂4519發表於2021-10-27

----微信支付文件,不得不說,挺亂!(吐槽截止)

功能背景

微信公眾號中,點選選單或者掃碼,開啟公眾號中的H5頁面,進行支付。

 

一、技術棧

前端:Vue:3.0.0,typescript:3.9.3,axios,vant,weixin-jsapi(微信官方wxjsdk)

後端:Koa,wxpay-3(不錯的apiv3封裝 https://github.com/yangfuhe/node-wxpay),axios

 

二、微信公眾平臺配置

1. 申請公眾號。

2. 公眾號設定:功能設定,JS介面安全域名(前端呼叫JSSDK,呼叫微信開放JS介面時使用),網頁授權域名(使用者授權,獲取openID前,需要獲取code,整個過程中需要一個回撥頁面,此頁面所在域名)。

    注意:a.這兩個域名可以一樣,根據實際情況使用。。一般是一樣;目前教程中這裡是同一個域名,就是前端所在的域名,比如:wxpay.test.cn,不要有http字首;

               b.需要把下載檔案放置到域名所在檔案下,保證wxpay.test.cn/MP_verify_***.txt可訪問。

3. 設定與開發:基本配置--公眾號開發資訊,記住AppID,AppSecret(獲取access_token和openID時使用),IP白名單(微信開發者工具中,獲取access_token時使用)。

 

三、微信商戶平臺配置

1. 申請商戶號。

2. 我的產品:開通JSAPI支付。

3. 開發配置:JSAPI支付,新增支付授權目錄。此配置是前端支付頁面URL路徑。目前教程中與上面的兩個域名一樣(wxpay.test.cn)。

4. AppID賬號管理:與公眾號關聯,即上面的公眾號AppID繫結。申請關聯後,要前往公眾號:廣告與服務--微信支付,商戶號管理,同意關聯。這樣,公眾號與商戶號才能繫結。

5. 我的賬號:賬戶設定--API安全,申請API證書,API證書管理--證書序列號,設定API祕鑰(其實沒用,因為是用的後面的v3祕鑰),設定APIv3祕鑰。

 

四、前端開發

1. 新增JSSDK模組,npm install weixin-jsapi -s

2. 建立PayTest.vue頁面,就一個支付按鈕。

<template>
  <div id="paytest">
    <van-button round block type="primary" @click="doPay">支付</van-button>
    <div v-html="msg" style="white-space: pre-wrap;"></div>
  </div>
</template>

3. 新增邏輯<script lang="ts">import { Vue } from "vue-class-component";

import { Action } from "vuex-class";
import wx from "weixin-jsapi";

export default class PayTest extends Vue {
  @Action("setOpenID") private setOpenID: any;
  private appID = "wx46adeb36e3e622ad"; //微信公眾號appID,可做成配置項
  private msg = '';

  public created() {
    //判斷本地是否已存openID   
    if (!this.$store.state.openID) {
      //如果未存,則要通過授權,回撥頁面,獲取code,然後獲取openID,儲存本地
      this.getWxAuth();
    }

    //初始化wx的jssdk的config
    this.initWxConfig();
  }

  //使用者授權,回撥,獲取openID
  private getWxAuth() {
    //官方參考文件:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
    if (!this.$route.query.code) {
      //微信授權,授權後重定向到本頁面
      const localUrl = window.location.href;
      alert(localUrl);
      window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${this.appID}&redirect_uri=${localUrl}&response_type=code&scope=snsapi_userinfo&state=STATE&connect_redirect=1#wechat_redirect`;
    } else {
      //如果已經授權,獲取code引數,通過後端獲取openID,返回前端,儲存本地快取
      const url = "/public/wxPort/getWxOpenId";
      this.axios.get(url, { params: { code: this.$route.query.code } })
        .then((res: any) => {
          //openID儲存本地
          if (res.status.code === 1) {
            this.setOpenID(res.data.openID);
            this.msg += `---授權成功,openID:${res.data.openID}\n`;
          } else {
            //丟擲錯誤
            this.msg += `---獲取openID失敗:${JSON.stringify(res)}\n`;
          }
        }).catch((err: any) => {
          this.msg += `---獲取openID失敗err:${JSON.stringify(err)}\n`;
        });
    }
  }
  //初始化wx JSSDK
  private initWxConfig() {
    //後端獲取access_token和ticket,返回簽名資訊,初始化wx.config
    const url = "/public/wxPort/getTicket";
    this.axios.get(url).then((res: any) => {
      if (res.status.code === 1) {
        this.msg += `---獲取 ticket成功,返回結果:${JSON.stringify(res.data)}\n`;

        //官方參考文件:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#1
        //初始化驗證jssdk
        wx.config({
          debug: true, // 這裡一般在測試階段先用ture,等打包給後臺的時候就改回false,
          appId: this.appID, // 必填,公眾號的唯一標識
          timestamp: res.data.timestamp, // 必填,生成簽名的時間戳
          nonceStr: res.data.nonceStr, // 必填,生成簽名的隨機串
          signature: res.data.signature, // 必填,簽名
          jsApiList: ["chooseWXPay"], // 必填,需要使用的JS介面列表
        });

        //通過ready介面處理成功驗證
        wx.ready(() => {
          this.msg += `---初始化wx.config成功\n`;
          wx.checkJsApi({
            jsApiList: ['chooseWXPay'], // 需要檢測的JS介面列表,所有JS介面列表見附錄2,
            success: (res: any) => {
              // 以鍵值對的形式返回,可用的api值true,不可用為false
              // 如:{"checkResult":{"chooseWXPay":true},"errMsg":"checkJsApi:ok"}
              this.msg += `---檢查wx.checkJsApi[chooseWXPay]成功:${JSON.stringify(res)}}\n`;
            }
          });
        });

        //通過error介面處理失敗驗證
        wx.error((err: any) => {
          this.msg += `---wx介面失敗:${JSON.stringify(err)}}\n`;
        });
      } else {
        //丟擲錯誤
        this.msg += `---獲取ticket失敗,返回結果:${JSON.stringify(res)}\n`;
      }
    }).catch((err: any) => {
      this.msg += `---獲取ticket失敗err,返回結果:${JSON.stringify(err)}\n`;
    });
  }

  //支付按鈕
  private doPay() {
    //先是後端使用者下單,下完單之後,前端再調取微信支付
    const url = "/public/wxPort/prepay";
    this.axios.get(url, { params: { openID: this.$store.state.openID } })
      .then((res: any) => {
        if (res.status.code === 1) {
          this.msg += `---統一下單成功,返回結果:${JSON.stringify(res.data)}\n`;
          const _that = this;
          wx.chooseWXPay({
            timestamp: res.data.timestamp, // 支付簽名時間戳,注意微信jssdk中的所有使用timestamp欄位均為小寫。但最新版的支付後臺生成簽名使用的timeStamp欄位名需大寫其中的S字元
            nonceStr: res.data.nonceStr, // 支付簽名隨機串,不長於 32 位
            package: "prepay_id=" + res.data.prepayID, // 統一支付介面返回的prepay_id引數值,提交格式如:prepay_id=\*\*\*)
            signType: "RSA", // 微信支付V3的傳入RSA,微信支付V2的傳入格式與V2統一下單的簽名格式保持一致
            paySign: res.data.paySign, // 支付簽名
            success: function (res: any) {
              _that.msg += `---chooseWXPay成功,返回結果:${JSON.stringify(res)}\n`;
            },
            // 支付取消回撥函式
            cancel: function (res: any) {
              _that.msg += `---chooseWXPay取消,返回結果:${JSON.stringify(res)}\n`;
            },
            // 支付失敗回撥函式
            fail: function (res: any) {
              _that.msg += `---chooseWXPay失敗,返回結果:${JSON.stringify(res)}\n`;
            },
          });
        } else {
          //丟擲錯誤
          this.msg += `---統一下單失敗,返回結果:${JSON.stringify(res)}\n`;
        }
      })
      .catch((err: any) => {
        this.msg += `---統一下單失敗err,返回結果:${JSON.stringify(err)}\n`;
      });
  }
}
</script>

 

四、後端開發

需要安裝外掛:npm install wxpay-v3 -s

//以下變數可以在config中統一配置
//公眾號appID
const appID = 'wx46adeb36e3e622ad';
//公眾號AppSecret
const appSecret = '5229c2220748d7a3c3cfe1028fda01c7';
//商戶號mchID
const mchID = '1615227157';
//商戶號API證書管理--證書序列號
const serialNo = '37CED47F994ED5F8B44766290CE7B979CE2CEFD3';
//商戶號API安全--APIv3金鑰
const apiv3PrivateKey = '01234567890123456789012345678901';
//商戶號API證書,祕鑰,也可以將祕鑰中的文字複製過來
const privateKey = require('fs').readFileSync(Path.join(__dirname, 'apiclient_key.pem')).toString();
//微信公眾號登入授權,獲取使用者的openID,access_token等資訊
    public static async getWxOpenId(ctx: any) {
        //官方參考文件:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
        let code = ctx.query.code; //獲取code值
        let url = 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=' + appID + '&secret=' + appSecret + '&code=' + code + '&grant_type=authorization_code';
        let res: any = await axios.get(url);
        //錯誤時{"errcode":40029,"errmsg":"invalid code"}
        //正確時
        // {
        //     "access_token":"ACCESS_TOKEN",
        //     "expires_in":7200,
        //     "refresh_token":"REFRESH_TOKEN",
        //     "openid":"OPENID",
        //     "scope":"SCOPE" 
        //   }
        if (res.status === 200) {
            if ((res.data.errcode && res.data.errcode.length > 0) || !res.data.openid) {
                ctx.body = {
                    status: StatusCode.ErrorCustome(800, '獲取微信使用者授權openID失敗:' + JSON.stringify(res.data)),
                }
            } else {
                ctx.body = {
                    status: StatusCode.Success('獲取資料成功'),
                    data: {
                        openID: res.data.openid
                    }
                }
            }
        } else {
            ctx.body = {
                status: StatusCode.ErrorCustome(800, '獲取微信使用者授權openID失敗:' + res.data)
            }
        }
    }
//獲取JSSDK的access_token和ticket
    public static async getTicket(ctx: any) {
        //官方參考文件:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62
        //獲取access_token,強烈建議儲存資料庫或快取,根據過期時間判斷是否要重新獲取    
        //獲取jsapi_ticketn,強烈建議儲存資料庫或快取,根據過期時間判斷是否要重新獲取
        try {
            let tempTicket = '';//此處從資料庫或快取獲取,如果未儲存或過期,要重新獲取。此處程式碼省略
            if (!tempTicket) {
                //獲取access_token
                let tokenUrl = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appID}&secret=${appSecret}`;
                let tokenRes: any = await axios.get(tokenUrl);
                if (!tokenRes || tokenRes.status != 200 || tokenRes.data.errcode || !tokenRes.data.access_token) {
                    ctx.body = {
                        status: StatusCode.ErrorCustome(800, '獲取微信access_token失敗:' + JSON.stringify(tokenRes.data))
                    }
                    return;
                }

                //獲取票據
                let ticketUrl = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${tokenRes.data.access_token}&type=jsapi`;
                let ticketRes: any = await axios.get(ticketUrl);
                if (!ticketRes || ticketRes.status != 200 || ticketRes.data.errcode != 0 || !ticketRes.data.ticket) {
                    ctx.body = {
                        status: StatusCode.ErrorCustome(800, '獲取微信ticket失敗:' + JSON.stringify(ticketRes))
                    }
                    return;
                }
                tempTicket = ticketRes.data.ticket;
            }

            //生成簽名
            let timestamp = Math.floor(Date.now() / 1000);
            let nonceStr = WXPortController.generateNonceStr();
            let obj = {
                jsapi_ticket: tempTicket,
                noncestr: nonceStr,
                timestamp: timestamp,
                url: ctx.header.referer  //url必須是呼叫JS介面頁面的完整URL
            }
            //按照ASCII碼從小到大排序
            let signStr = WXPortController.raw(obj);

            // hash加密
            const crypto = require('crypto');
            let shasum = crypto.createHash('sha1');
            shasum.update(signStr);
            let signature = shasum.digest("hex");

            ctx.body = {
                status: StatusCode.Success('獲取資料成功'),
                data: {
                    timestamp,
                    nonceStr,
                    signature
                }
            }
        }
        catch (err) {
            ctx.throw(err.message);
        }
    }
//微信統一下單
    public static async prepay(ctx: any) {
        try {
            //呼叫wxpay-v3的外掛
            const Payment = require('wxpay-v3');
            const paymnetTemp: any = new Payment({
                appid: appID,
                mchid: mchID,
                private_key: privateKey,
                serial_no: serialNo,
                apiv3_private_key: apiv3PrivateKey,
            });
            let res = await paymnetTemp.jsapi({
                description: '測試支付',
                out_trade_no: Date.now().toString(),
                notify_url: 'http://gzh.zhongguoysd.top',//非同步接收微信支付結果通知的回撥地址
                amount: {
                    total: 1
                },
                payer: {
                    openid: ctx.query.openID
                },

            });

            const timestamp = Math.floor(Date.now() / 1000);
            const nonceStr = WXPortController.generateNonceStr();
            if (res.status === 200 && res.data) {
                let prepayIDRes = JSON.parse(res.data);
                if (prepayIDRes) {
                    //生成支付簽名
                    let str = `${appID}\n${timestamp}\n${nonceStr}\nprepay_id=${prepayIDRes.prepay_id}\n`;
                    let paySign = paymnetTemp.rsaSign(str, privateKey, 'SHA256withRSA');
                    ctx.body = {
                        status: StatusCode.Success('獲取資料成功'),
                        data: {
                            prepayID: prepayIDRes.prepay_id,
                            paySign,
                            timestamp,
                            nonceStr
                        }
                    }
                    return;
                }
            } else {
                ctx.body = {
                    status: StatusCode.ErrorCustome(800, '獲取prepayID失敗:' + JSON.stringify(res.data)),
                }
            }
        }
        catch (err) {
            ctx.throw(err.message);
        }
    }
    //生成隨機字串
    public static generateNonceStr(length = 32) {
        const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        let noceStr = '', maxPos = chars.length;
        while (length--) noceStr += chars[Math.random() * maxPos | 0];
        return noceStr;
    }

    //將物件按照asscii序列化為字串
    public static raw(args: any) {
        var keys = Object.keys(args);
        keys = keys.sort()
        var newArgs: any = {};
        keys.forEach(function (key) {
            newArgs[key] = args[key];
        });
        var string = '';
        for (var k in newArgs) {
            string += '&' + k + '=' + newArgs[k];
        }
        string = string.substr(1);
        return string;
    };

 

五、填坑問題

1. {"errMsg":"config:fail,Error:系統錯誤,錯誤碼:40048,invalid url domain"}

    解決:公眾號設定:功能設定,JS介面安全域名(前端呼叫JSSDK,呼叫微信開放JS介面時使用)

2. 獲取使用者授權時,報錯頁面:redirect_uri引數錯誤

    解決:公眾號設定:功能設定,網頁授權域名(使用者授權,獲取openID前,需要獲取code,整個過程中需要一個回撥頁面,此頁面所在域名)。

3. {"errMsg":"config:fail,Error:系統錯誤,錯誤碼:63002,invalid signature"}

    解決:商戶號,開發配置:JSAPI支付,新增支付授權目錄。此配置是前端支付頁面URL路徑。

4. {errorCode=72002, errorMsg=mchid is not bind appid}或者 "商戶號與appid不匹配"

    解決:商戶號, AppID賬號管理:與公眾號關聯,即上面的公眾號AppID繫結。申請關聯後,要前往公眾號:廣告與服務--微信支付,商戶號管理,同意關聯。這樣,公眾號與商戶號才能繫結。

5. 支付驗證簽名失敗

    這裡造成的原因很多:【官方詳細參考:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml】

    5.1 統一下單採用jsapi v3,所以wx.chooseWXPay中的signType要用RSA。官方參考連結:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#58

    5.2 wx.chooseWXPay中paySign簽名生成要用SHA256withRSA。官方參考連結:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_4.shtml

    5.3 paySign中的簽名串要完整,尤其是【prepay_id=】不能丟:`${appID}\n${timestamp}\n${nonceStr}\nprepay_id=${prepayIDRes.prepay_id}\n`;

    5.4 驗證簽名是否正確的工具(很有用!很有用!):連結:https://pan.baidu.com/s/1ixOAnYyZVW13dFr0jWVpvw    提取碼:wujv

    5.5 如果用舊版v2的JSAPI,那簽名方式signType就與v3版不同,主要是MD5的方式,注意要與統一下單的簽名方式保持一致:

wx.chooseWXPay({
  timestamp: 0, // 支付簽名時間戳,注意微信jssdk中的所有使用timestamp欄位均為小寫。但最新版的支付後臺生成簽名使用的timeStamp欄位名需大寫其中的S字元
  nonceStr: '', // 支付簽名隨機串,不長於 32 位
  package: '', // 統一支付介面返回的prepay_id引數值,提交格式如:prepay_id=\*\*\*)
  signType: '', // 微信支付V3的傳入RSA,微信支付V2的傳入格式與V2統一下單的簽名格式保持一致
  paySign: '', // 支付簽名
  success: function (res) {
    // 支付成功後的回撥函式
  }
});

 

六、結束語

簡單的微信JSAPI v3版本的教程基本完工,至於後期的退款等功能,wxpay-v3外掛都已封裝,直接呼叫即可。

整體來說,初次研究微信支付時,下手很亂,因為文件寫的就很亂,新版和舊版在官網上並未很好的分割。

最終還是看這個:(按照文件的順序,一層一層的扒皮)

1. 先看統一下單:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml,裡面有未知的引數openid

2. 然後看如何獲取openid:https://pay.weixin.qq.com/wiki/doc/apiv3/terms_definition/chapter1_1_3.shtml#part-3

3. 統一下單獲取perpay_id後,再看JSAPI調起支付:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_4.shtml

4. 調起支付使用JSSDK的方式,首先要初始配置JSSDK:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#1  步驟1-5,保證wx.config是配置成功的

5. wx.config中的簽名,要看附錄1,JS-SDK使用許可權簽名演算法:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#62

6. JSSDK所有介面,要看附錄2,https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#63,目前支付用chooseWXPay

7. 配置JSSDK完成後,要呼叫支付wx.chooseWXPay要看: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#58

8. wx.chooseWXPay裡面有個paySign簽名,如何生成簽名,就回到JSAPI調起支付的頁面下方,有【JSAPI調起支付的引數需要按照簽名規則進行簽名計算】,即:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_4.shtml

 

基本上要看的JSAPI v3的文件就上面這些,v2的舊版我也看了一部分,沒有試,就不發了。大家有任何問題,請留言。

(文章為老呂原創,轉載請註明出處)

相關文章