前端开发在微信支付遇到的坑

3,403 阅读14分钟

笔者最近需求包含了微信支付的流程,遇到了不少坑,故做一个记录,为以后避坑可用。

一、知己知彼,微信支付种类

当产品说,别人组也支持了微信支付,咱们组也支持一下吧。需求单上也列了微信H5支付。

这时候要留意啦,需求单上的支付场景是不是没有考虑周全。

微信支付常见种类:

  • H5支付,主要用于触屏版的手机浏览器(注意不是微信的浏览器)请求微信支付的场景。可以从外部浏览器唤起微信支付。
  • JSAPI支付,主要在微信内打开网页时,可以调用微信支付完成下单购买的流程。
  • APP支付,移动端应用APP中集成开放SDK调起微信支付模块来完成支付。(移动端内也可以嵌入H5支付,但体验没有原生支付好,官网不推荐这种做法)
  • Native支付,是指商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。适用于PC网站、实体店单品或订单、媒体广告支付等场景。
  • 付款码支付,用户出示微信钱包中的条码、二维码,商家通过扫描用户条码即可完成收款。
  • 小程序支付,商户已有微信小程序,用户通过好友分享或扫描二维码在微信内打开小程序时,可以调用微信支付完成下单购买的流程。

此次笔者需求单上是H5支付,安卓客户端也嵌入H5支付,IOS客户端是苹果原生IAP支付。

开发前没有考虑到的

  • 微信浏览器的JSAPI支付(能怎么办,加班咯),
  • PC和桌面端打开时候的Native支付(toast提示在移动端支付,不做)。

二、开发前的准备

可参考微信支付开发文档,以下列重点。

  1. 开发前要有一个商户号,可在 微信公众平台 注册,并认证通过。还要有一个微信公众号,为了做微信内JSAPI支付用。

  2. 认证通过以后,可在微信公众平台申请开通微信支付。

    以上两步公司一般已经申请完,直接找相应的人对接就行

  3. 开通H5支付权限

    前往微信支付商户平台—>产品中心—>开发配置—>H5支付,设置后一般10分钟内生效。 开通完之后,在“支付配置”添加H5支付域名,注意:域名必须通过ICP备案,域名填写格式不包含http://或https://

  4. 开通JSAPI支付权限

    JSAPI支付依托于微信公众号,且要企业认证,才有网页授权。微信公众号要单独在微信公众平台登录设置。

    前往微信支付商户平台—>产品中心—>开发配置—>JSAPI支付,设置后一般5分钟内生效。

    4.1 设置支付目录

    • 商户最后请求拉起微信支付收银台的页面地址我们称之为“支付目录”,例如:www.weixin.com/pay.php。

    • 商户实际的支付目录必须和在微信支付商户平台设置的一致,否则会报错“当前页面的URL未注册:”

    • 如果支付授权目录设置为顶级域名(例如:www.weixin.com/ ),那么只校验顶级域名,不校验后缀;

    • 如果支付授权目录设置为多级目录,就会进行全匹配,例如设置支付授权目录为www.weixin.com/abc/123/,则实…

    • 配置域名时,一定要注意协议头,http 和 https完全不一样,如果开发过程中,一直遇到“当前页面的URL未注册:”这个问题的时候,一定要来检查这个授权域名

    4.2 设置网页授权域名

    如果看不到网页授权,看看公众号是否开通企业认证。服务号必须通过微信认证。

    • 开发JSAPI支付时,在统一下单接口中要求必传用户openid,而获取openid则需要您在公众平台设置获取openid的域名,只有被设置过的域名才是一个有效的获取openid的域名,否则将获取失败。

    • 授权域名设置说明:登录微信公众平台-->公众号设置-->功能设置

    图片中的“下载文件”,一定要放在线上服务器web根目录下。不然域名无法添加。

    以上的基础配置一定要配置好,不然很容易出现一些问题。尤其网页授权这一步,经常会看不到这个配置,首先公众号要企业认证的才可以。然后域名一定要完全写对,最好自己都要过目以下,让别人添加,容易出现写错的坑。授权的域名数量一般有限,此次开发过程碰到换域名的需求,刚好数量达到上限,只能把原来的删除掉,添加新的域名。然后就碰到各种奇奇怪怪的事情。以上写的请细品。

三、基础工具准备

  • 记录下公众号的ppid(wx开头的),JSAPI支付获取code用;还有app_secret,一般丢给后端就行。
  • 公众号的web开发者工具,一定要添加开发者的微信号,这样才方便调试。
  • 微信开发者工具很多坑,开发的时候各种问题,还是要借助测试机(真机)调试。

开发调试遇到的问题

通报:微信更新到7.0以后抓包公众号会有证书问题,抓包小程序直接不能打开
各位不用到处找了,也不用怀疑人生了,你没有问题、win10也没有问题、fiddler和Charles也没有问题,是因为微信更新了,不再从手机本地获取证书。
安卓系统 7.0 以下版本,不管微信任意版本,都会信任系统提供的证书
安卓系统 7.0 以上版本,微信 7.0 以下版本,微信会信任系统提供的证书
安卓系统 7.0 以上版本,微信 7.0 以上版本,微信只信任它自己配置的证书列表
以上规则是拷贝别人的,但确实亲测如此,搞得我都怀疑人生了,主要是前一天在win7上调得好好的,第二天换了个win10的系统,刚好微信又自动更新,突然就不能抓了,各种怀疑win10(这哥们儿躺枪了),各种怀疑人生都找不到原因,就问你怕不怕,就问你坑不坑。。。
意思是你要抓包调试的话,现阶段要么安卓7以下的手机,要么微信7.0以下的版本。。。

开发环境下,安卓手机微信内确实无法打开网站链接(测试小姐姐说她的测试机可以,反正我的就不行),微信开发者工具能够打开,但是链接无法跳转。按照以上的教程,将微信降级到7.0以下版本,测试无效;安卓7.0以下版本没去试。

开发环境下微信内无法打开网站链接,那JSAPI支付就没得调试,问了后端大佬,他也不知道,说只能在线上环境调试了(leader对线上调试表示很大的担忧)。怎么办?怎么办?忧愁的我掉了很多根头发。安卓测试机无法链接,下意识的以为IOS也是不行。

解决

一次意外的尝试在ios打开,发现居然可以在测试环境下打开链接。不过它有个条件,就是手机只能连dns,不能连代理(可用于抓包工具)。

所以开发调试时,只能借用IOS测试机。

四、H5支付

官网提醒H5支付不建议在APP端使用,但本次需求中,安卓APP端内却使用了,开发上遇到不少坑,体验也大打折扣。

需求

本次的需求是,点击付费商品的购买按钮,跳转到支付订单页,在支付订单页发起微信支付。若成功,则返回到支付订单页,toast提示成功购买,1s后跳转到商品详情页,并改变页面样式;若失败,则返回停留在支付订单页,toast提示支付失败;若取消支付,则返回停留在支付订单页,toast提示取消支付。(JSAPI支付也是一样,但需求单没写。此次的购买还包含了积分购买,考虑的情况会有点多,还有本次来不及做的账内支付。)

开发文档重点

参考开发文档,以下划重点:

先来了解下H5支付的回调页面。正常流程用户支付完成后会返回至发起支付的页面,如需返回至指定页面,则可以在MWEB_URL后拼接上redirect_url参数,来指定回调页面。

注意:

  • 需对redirect_url进行urlencode处理
  • 由于设置redirect_url后,回跳指定页面的操作可能发生在:
    1. 微信支付中间页调起微信收银台后超过5秒 (自动返回);
    2. 用户点击“取消支付“或支付完成后点“完成”按钮。 因此无法保证页面回跳时,支付流程已结束,所以商户设置的redirect_url地址不能自动执行查单操作,应让用户去点击按钮触发查单操作。回跳页面展示效果可参考下图。

以上的回跳操作,区分5s内和5s后的操作,表现效果完全不同。如果没有支付是否成功的提示弹框,则很难稳定的执行查单操作。

需求PK(一把辛酸泪)

官方文档推荐,redirect回来,展示回跳页面,让用户去点击按钮触发查单操作。

而笔者的需求单是返回来之后,直接toast提示得到结果。这样子的话,是无法准确的知道页面回跳回来的支付流程结果。

开发过程中,被5s内和5s后不同的表现折磨的想哭。开发了很长一段时间,还是没法完成想要的结果,导致开发流程上各种问题。

笔者元旦加了两天班。动之以情,晓之以理的说服产品,支付回来一定要加上支付确认提示页,否则是没法做支付。产品看在我连续加班的份上,终于改了这个需求。因此也终于在元旦加班的最后一天(到晚上10点多),完成了这个支付流程开发。

再次感谢产品小姐姐。

开发

本次需求的难点是,如何监听到微信成功支付或者取消支付后返回支付订单页,以此来做一些提示效果。

(一)、visibilitychange

第一个想到的是,visibilitychange这个原生事件,参考 MDN,当其选项卡的内容变得可见或被隐藏时,会在文档上触发 visibilitychange (能见度更改)事件。有个库vue-visibility-change可以使用。

但它有一些问题:

  1. visibilitychange原生事件兼容性不太好,Safari无法使用,即便是用上了vue-visibility-change,结果也好不到哪去。还有在安卓客户端,5s内结束微信操作,visibilitychange的触发效果能够看到,5s后则没法看到,需要借助JsBridge,让安卓客户端告诉web页面显现。即便有了JsBridge的辅助,第一次进入支付订单页,也能看到JsBridge触发弹起的toast。主要看webview页面中web页的加载情况,加载的慢,那么toast则看不到(加载出来前已经toast结束),加载的快,则能看到不想要看到的toast提示。
  2. 除了从微信支付页面返回来能被监听到,页面从后台切换回来也仍然能被监听到,那这样子考虑的情况就相对比较多。
  3. 因为没有支付确认弹框,5s内和5s后成功支付和取消支付后返回页面的表现不一样,再考虑到安卓和IOS的环境,以及多端不同的浏览器的兼容表现,各种奇奇怪怪的现象数不胜数。

最终放弃这个方案。

(二)、history.length

代码的支付,集合在支付订单页,而支付订单页是单独另开的一个页面。进入到微信页面,再返回支付页面,history会不断累加。因此可以在history.length(以下简称hLlength)上做文章。

从商品进入到支付订单页,因为支付订单页是新开页面,所以hLlength是1,进入到微信页面,hLlength再加1,最后返回到订单页。

所以理论上只要判断hLlength变为2之后,就可以弹出确认支付弹框。

理想很丰满,现实很骨感。奇怪的事情还是发生了。以下为移动端浏览器的测试表现。

  • 进入新页面时,Safari的hLlength为2,而Chrome以及Chrome内核的hLlength表现正常为1。

  • 跳到微信支付页,Safari和Chromeh的Llength能加1,OPPO手机自带浏览器的hLlength却不能加1。

  • 安卓客户端内webview的hLlength表现正常。

因为不同的浏览器hLlength的表现不一样,无法预料没有测试到的浏览器会有什么样的表现,因此这个方案也被PASS。

(三)、redirect参数标志

H5支付的回调页面。正常流程用户支付完成后会返回至发起支付的页面,如需返回至指定页面,则可以在MWEB_URL后拼接上redirect_url参数,来指定回调页面。

如果没有支付确认弹框,则从支付回来到页面表现不一样,所以需要有一个支付确认弹框,来发送一次查单操作,以此来做支付是否完成到判断。触发弹框的操作放在了created生命周期中,如果不指定redirect_url,返回到最开始发起支付到页面,因为是老页面,所以没法继续触发created钩子。所以可以通过指定redirect_url,并加上is_redirect=1这个参数来表示从微信页面回来,跳到新页面,以此来触发created钩子,通过判断is_redirect=1来打开支付确认弹框。

   computed:{
    	// 从微信支付成功,取消返回
        isRedirectFromWechat() {
            const is_redirect = location.search.match(/is_redirect=(\d+)/);
            return !!(is_redirect && is_redirect[1] === "1");
        }
    },
    // 从微信支付页面返回,因为通过redirect_url返回,所以会是一个新页面,能够触发created钩子
    created() {
    	if(this.isRedirectFromWechat) {
            this.$modal.show();
            // TOOD
        }    	
    }

这个方案能够稳定打开支付确认弹框。

但有一些副作用:

  • 就支付回来的url会带上is_redirect=1,如果刷新这个页面,又会继续触发确认弹框。
  • history的栈是支付订单页——微信中间页——支付订单页,如果取消弹框后不做任何处理,点击返回按钮,会回到微信支付页,在安卓客户端返回支付订单页,右上角还多了一个关闭按钮(与最开始的页面不一致)。

因此可以在支付确认弹框做一些文章。

  1. 点击“未支付”取消确认弹框,仍停留在订单支付页面。那直接history.go(-2);,完美回到一开始的支付订单页,毫无副作用。
  2. 点击“已支付”,发送一次查单请求,如果成功,则toast提示“成功支付”,1s后返回商品详情页,如果失败,则toast提示“支付失败”,仍停留在支付订单页。“支付失败”的操作与步骤1一样,成功则延时一定时间后(给toast时间),然后关闭当前页面。
   // 延迟跳转到商品详情页
   async delayToPage(options) {
       await sleep(TIMEOUT_DELAY);
       closeCurrentPage(options);
   }

而关闭当前页面的代码可以封装在一起。

// 关闭浏览器页面
const closeWindow = () => {
   if (window.close) {
       window.close();
       return;
   }
   window.opener = null;
   const t = window.open('', '_self', '');
   t.close();
}
 // 关闭当前页面
const closeCurrentPage = ({ isSuccess } = {}) => {
   // 客户端
   if (isPlatform() ) {
       // 现金支付成功,直接关闭支付订单页,返回商品详情页
       if (isSuccess) {
           window.location.href = 'schema://close_webview';
       }
       /**
        *  安卓微信取消支付后的效果,微信支付成功返回后则不处理
        *  安卓的支付订单页是一个webview,最下面是订单页,中间是微信支付页,上面是跳转回来的订单页
        *  现金操作跳转到微信支付 `window.location.href = mweb_url;` history 加1;
        *  跳转到微信页面,history再加1;
        *  取消微信支付,重定向到支付页面,history再加1,则总的为3;
        *  通过history.go(-2)能跳转到最初的支付页面。
        */
       else {
           history.go(-2);
       }

       return;
   }

   // h5 浏览器 支付成功,直接关闭支付订单页,返回商品详情页
   if (isSuccess) {
       closeWindow();
       return;
   }

   // 其他乱七八糟的浏览器,直接回退两步
   history.go(-2);
}; 

至此,微信h5支付关键代码已经完成。

(四)、localStorage

codereview的时候,有同事推荐也可以通过localStorage做状态变化的记录,没去测试过,有机会再去尝试。

五、JSAPI支付

微信内的JSAPI支付,依托于微信公众号,配置回看第二部分。

开发步骤为:

  1. 引导用户进入授权页面同意授权,获取code

    在created钩子函数中,触发getCode。

     const getCode = (url) => {
        // 获取code
        const redirect_uri = url || window.location.href;
        let state = parseInt(Math.random() * 1000);
        // APP_ID 公众号中以wx开头的那一串代号
        const path = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${APP_ID}&redirect_uri=${encodeURIComponent(redirect_uri)}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`;
    
        window.location.replace(path);
    };
    
  2. 通过code换取网页授权access_token

    将上一步获取到的code值(在url的参数中获取),传给后端,让后端做处理,前端会有跨域。

    获取code后,请求以下链接获取access_token: api.weixin.qq.com/sns/oauth2/…

  3. 后端经过一系列处理,获取到参与签名的参数为:appId、timeStamp、nonceStr、package、signType、paySign。

    接着前端调用以下方法,即可唤起微信浏览器内的支付。

    function onBridgeReady(){
       // WeixinJSBridge 为微信的内置对象,可在微信浏览器中直接使用
       WeixinJSBridge.invoke(
          'getBrandWCPayRequest', {
             "appId":"wx2421b1c4370ec43b",     //公众号名称,由商户传入     
             "timeStamp":"1395712654",         //时间戳,自1970年以来的秒数     
             "nonceStr":"e61463f8efa94090b1f366cccfbbb444", //随机串     
             "package":"prepay_id=u802345jgfjsdfgsdg888",     
             "signType":"MD5",         //微信签名方式:     
             "paySign":"70EA570631E4BB79628FBCA90534C63FF7FADD89" //微信签名 
          },
          function(res){
          // get_brand_wcpay_request:cancel	支付过程中用户取消
          // get_brand_wcpay_request:fail	支付失败
          if(res.err_msg == "get_brand_wcpay_request:ok" ){
          // 使用以上方式判断前端返回,微信团队郑重提示:
                //res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
          }  
       }); 
    }
    
    if (typeof WeixinJSBridge == "undefined"){
       if( document.addEventListener ){
           document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
       }else if (document.attachEvent){
           document.attachEvent('WeixinJSBridgeReady', onBridgeReady); 
           document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
       }
    }else{
       onBridgeReady();
    }
    

JSAPI支付因其拥有回调函数,所以在支付的效果上,很可控。微信成功支付后,直接history.go(-1);,即可直接回到商品详情页。取消或失败则继续停留在当前页。

六、开发中遇到的问题

问题的类型和原因可以查看官方文档,遇到的问题无非就是配置出了问题,请回看第二点,然后就是redirect_url配置也出错导致一些问题。其它遇到的问题,直接找后端开发的小伙伴吧。

只要第二步配置好了,开发就能很顺畅。不然遇到的,大部分都是配置不对的问题。