• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

Vue3+Typescript+Node.js实现微信端公众号H5支付(JSAPI v3)教程--各种填坑 ...

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

----微信支付文档,不得不说,挺乱!(吐槽截止)

功能背景

微信公众号中,点击菜单或者扫码,打开公众号中的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中的签名串要完整,尤其是【pr


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
TypeScript-类型断言发布时间:2022-07-18
下一篇:
解决基于TypeScript 的 RN项目相对路径引入组件的问题发布时间:2022-07-18
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap