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

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

    功能背景

    微信公众号中,点击菜单或者扫码,打开公众号中的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}
    `;
              } else {
                //抛出错误
                this.msg += `---获取openID失败:${JSON.stringify(res)}
    `;
              }
            }).catch((err: any) => {
              this.msg += `---获取openID失败err:${JSON.stringify(err)}
    `;
            });
        }
      }
      //初始化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)}
    `;
    
            //官方参考文档: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成功
    `;
              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)}}
    `;
                }
              });
            });
    
            //通过error接口处理失败验证
            wx.error((err: any) => {
              this.msg += `---wx接口失败:${JSON.stringify(err)}}
    `;
            });
          } else {
            //抛出错误
            this.msg += `---获取ticket失败,返回结果:${JSON.stringify(res)}
    `;
          }
        }).catch((err: any) => {
          this.msg += `---获取ticket失败err,返回结果:${JSON.stringify(err)}
    `;
        });
      }
    
      //支付按钮
      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)}
    `;
              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)}
    `;
                },
                // 支付取消回调函数
                cancel: function (res: any) {
                  _that.msg += `---chooseWXPay取消,返回结果:${JSON.stringify(res)}
    `;
                },
                // 支付失败回调函数
                fail: function (res: any) {
                  _that.msg += `---chooseWXPay失败,返回结果:${JSON.stringify(res)}
    `;
                },
              });
            } else {
              //抛出错误
              this.msg += `---统一下单失败,返回结果:${JSON.stringify(res)}
    `;
            }
          })
          .catch((err: any) => {
            this.msg += `---统一下单失败err,返回结果:${JSON.stringify(err)}
    `;
          });
      }
    }
    </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}
    ${timestamp}
    ${nonceStr}
    prepay_id=${prepayIDRes.prepay_id}
    `;
                        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} ${timestamp} ${nonceStr} prepay_id=${prepayIDRes.prepay_id} `;

        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) {
        // 支付成功后的回调函数
      }
    });

     6. {“errcode”:10164,“errmsg”:“invalid ip ***.***.***.***,not in whitelist rid:***....”},白名单问题。

       解决:公众号,设置与开发,基本配置:IP白名单,把提示的invalid ip加进去。

    六、结束语

    简单的微信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的旧版我也看了一部分,没有试,就不发了。大家有任何问题,请留言。

    (文章为老吕原创,转载请注明出处)

  • 相关阅读:
    Java Web总结十Jsp
    当前结果
    QFontMetrics的一个问题
    设想的用户交互流程
    多视图工作
    改进函数、变量的表示
    接口测试Session/Cookie笔记(二)
    接口测试笔记(一)
    创业公司心力交瘁
    禅道BUG管理工具使用链接存储
  • 原文地址:https://www.cnblogs.com/laolv4519/p/15471866.html
Copyright © 2011-2022 走看看