微信支付之H5支付踩坑记录

4,850 阅读3分钟

很久之前其实已经接触过微信支付,当时应用在小程序上,小程序代遇到的场景比较单一,因此没有遇到什么大的问题。这次做项目,需要在H5页面引入微信支付,通过不同场景的测试发现了几个坑,记录一下也供大家参考。

微信支付类型

查看微信支付开发文档,微信支付主要有以下几种类型:

  • 付款码支付
  • JSAPI支付
  • Native支付
  • APP支付
  • H5支付
  • 小程序支付
  • 人脸支付

我们公司的项目为从微信公众号或者APP webview进入的H5页面,因此会用到上面的JSAPI支付和H5支付,这两种支付场景也是比较常见的。JSAPI支付为在微信环境内打开直接调起微信支付,H5支付为非微信环境打开,H5支付获跳转支付跳转url(参数名“mweb_url”),商户通过mweb_url调起微信支付中间页,这meb_url链接可以拼接支付后返回的redirect_url,这就是后面会讲到的坑点所在。

如何接入微信支付

使用微信JS_SDK的步骤简单总结如下:

  • 去微信公众平台设置“js接口安全域名”
  • 加载微信js-sdk
  • 通过config接口注入权限验证配置
  • 通过ready接口处理成功验证
  • 通过error接口处理失败验证

安装微信js-sdk模块

jweixin-wechat模块封装了微信js-sdk的很多方法,我们不必要再次引入微信官方的jsskd,直接安装此模块即可调用。

yarn add jweixin-wechat

生成微信签名

参与签名的字段包括有效的 jsapi_ticket),noncestr(随机字符串,由开发者随机生成),timestamp(由开发者生成的当前时间戳),url(当前网页的URL,不包含#及其后面部分)。如果当前签名的url域名与在微信公众号后台设置域名的不一致,将会导致签名失败。

  // 加载jsapi_ticket
  const getCacheTicket = () => {
    const jsticket = JSON.parse(localStorage.getItem(localKey.jsticketKey));
    const now = new Date().getTime();
    if (jsticket && now < jsticket.expire) {
      return jsticket;
    }
    return null;
  };
  
  // 生成签名
  const wxConfig = option =>
    new Promise(function(resolve, reject) {
      const jsApiList = [
        'chooseWXPay',
        'updateAppMessageShareData',
        'updateTimelineShareData',
        'onMenuShareAppMessage',
        'onMenuShareTimeline',
        'onMenuShareQQ',
        'onMenuShareQZone'
      ];

      const { nonceStr, signature, timestamp } = option;

      wx.config({
        debug: false,
        appId: WX_APPID,
        timestamp: timestamp,
        nonceStr: nonceStr,
        signature: signature,
        jsApiList: jsApiList
      });
      wx.ready(resolve);
      wx.error(reject);
    });
    
    // 生成签名所需字段
    const wxSign = ticket => {
      const nonceStr = Math.random()
        .toString(36)
        .substring(2);
      const jsapiTicket = ticket;
      const timestamp = new Date().getTime();
      const url = location.href;

      const signstr = `jsapi_ticket=${jsapiTicket}&noncestr=${nonceStr}&timestamp=${timestamp}&url=${url}`;
      const signature = sha1(signstr);
      return { nonceStr, signature, timestamp };
    };
  
    // wxInit
    const wxInit = () =>
      Promise.all([loadTicket(), checkJSBridge()])
        .then(res => {
          const [ticketres] = res;
          const option = wxSign(ticketres.ticket);

          return wxConfig(option);
        })
        .catch(err => {
          //   Sentry.captureException(err);
          console.log(err);
        });

封装微信支付函数

  const wxPay = paydata =>
    new Promise(function(resolve, reject) {
      const callbalk = {
        success: resolve,
        fail: reject,
        cancel: () => reject(new Error({ errMsg: '支付取消' }))
      };
      const wxData = {
        appId: paydata.appId,
        timestamp: paydata.timestamp,
        nonceStr: paydata.nonceStr,
        package: paydata.package,
        signType: paydata.signType || 'MD5',
        paySign: paydata.paySign
      };
      const pay = Object.assign({}, wxData, callbalk);
      wx.chooseWXPay(pay);
    });

踩坑

配置好签名和封装好微信支付后,我们就可以开始正式调用微信支付了。

jssdk支付

在微信内的H5支付其实没什么大问题,调起微信支付后,可以立刻调用支付完成回调函数

await this.$wxPay(payres)
this.onPaySuccess(data)

h5支付

根据官方文档所说,正常流程用户支付完成后会返回至发起支付的页面,如需返回至指定页面,则可以在MWEB_URL后拼接上redirect_url参数(需要urlencode处理),来指定回调页面。

在项目中,我在重定向的链接同时拼接了订单参数,当重定向成功时获取订单参数执行查单操作以通知用户支付状态。经测试,发现四种状况:

  • 在QQ浏览器,小米浏览器,华为浏览器都能正常跳回并执行查单操
  • 谷歌浏览器,三星浏览器,redirect_url无效,但是在重定向到MWEB_URL之前自己刷新了一次当前页面
  • ios的浏览器在重定向到MWEB_URL之前和支付完成之后没有任何刷新动作,有一些ios浏览器支付完成虽然有重定向但是redirect_url在Safari重新打开,不在之前的浏览器打开
  • APP嵌入H5页面,Android的表现为第二种状况,而ios出现跳转到微信APP完成支付后没有跳回APP,对用户使用十分不友好

后面出现的三种状况使得我们无法获知用户什么时间点完成支付操作,因此出现的问题是我没办法知道具体在什么时候去查询订单状态

解决方案

1.面对谷歌浏览器及三星浏览器在支付跳转前刷新页面,支付操作完成不回调的情况,在项目中我们的解决方案是在跳转支付前将订单id存起来,n秒后弹起支付弹窗让用户手动执行查单操作。

    ...
    // 存储orderId
    this.$utils.setOrderId(orderId);
    ...
    //  获取orderId,查单
    const oId = this.$utils.getOrderId();
    setTimeout(() => {
        Toast.loading({
            duration: 0,
            message: '查询订单中'
        });
        this.checkIsPaid(data);
    }, 3000);
    
    // 封装的支付完成查单函数
    async checkIsPaid(id, type) {
        const payInfo = await this.$store.dispatch(getBillApi, { orderId: id });
        const { orderId, status } = payInfo;
        Toast.clear();
        // eslint-disable-next-line eqeqeq
        if (orderId == id && status == 20) {
          //支付成功
          this.onPaySuccess(data);
        } else {
          // 确认支付弹窗
          this.onH5Pay(orderId);
        }
    }

2.ios的浏览器在重定向到MWEB_URL之前和支付完成之后没有任何刷新动作或者在Safari重新打开回调redirect_url设为当前域名。经提醒测试,safari浏览器按正常设置回调url,可以正常返回,因此删除掉safari的状况。代码如下:

    if (this.$utils.isIOS()) {
        const rurl = 'xxx://'; // 当前网站域名
        newurl = `${mWebUrl}&redirect_url=${encodeURIComponent(rurl)}`;
        setTimeout(() => {
            // 确认支付弹窗
            this.onH5Pay(orderId);
        }, 2000);
    }

3.部分ios微信支付后没有跳转回App的,这里和APP配合,由APP那边设定好userAgent,通过判断userAgent判断是否在APP内及设备是否是ios,采取上面的第二种解决方案,整理后的代码如下:

    if (this.$utils.isIOS() || this.$utils.isApp() {
        const rurl = 'xxx://'; // 当前网站域名
        newurl = `${mWebUrl}&redirect_url=${encodeURIComponent(rurl)}`;
        if (this.$utils.isiPhone() || this.$utils.isiPad() || this.$utils.isiPod()) {
            setTimeout(() => {
                // 确认支付弹窗
                this.onH5Pay(orderId);
            }, 2000);
        }
    }

总结

微信H5支付问题的痛点主要是在不知何时去执行查单操作,且官方文档告诉我们redirect_url是不可信的,建议让用户去点击触发查询。面对不同的状况,我们通过设定定时器弹出支付弹窗让用户去主动查询。

官方文档示例