yaoningvital / blog

my blog
31 stars 4 forks source link

微信公众号支付和H5支付开发总结 #28

Open yaoningvital opened 6 years ago

yaoningvital commented 6 years ago

公司项目2.9.0版本中涉及到了增加微信支付方式:公众号支付和H5支付。现在把这段时间开发过程中遇到的问题,及相关思考总结记录一下。

一、公众号支付

1、什么是公众号支付?

公众号支付就是:商户已有H5商城网站,用户通过消息或扫描二维码在微信内打开网页时,调用微信支付完成下单购买的流程。 说白了就是:在微信浏览器中打开H5网站,调用微信浏览器中自带的微信对象的支付方法来完成支付。

2、公众号支付流程中前端需要做的工作?

1、首先判断web应用当前是否运行在微信环境中,如果是,调微信的公众号微信网页授权机制中“用户同意授权,获取code”的接口,目的是拿到用于获取openid时用的code。

(1)通过isInWeiXinAPP = $window.navigator.userAgent.toLowerCase().search(/MicroMessenger/i) > -1来判断web应用当前是否运行在微信环境中。

(2) 微信网页授权机制中“用户同意授权,获取code”的接口为:https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect。其中:

appid为你使用的公众号的appid。这个公众号需要是服务号,并且开通微信支付功能。在开通微信支付功能的时候,会需要你填写公司的相关信息,包括主体信息、银行账号等。开通成功后,会拿到一个商户号。最终通过公众号支付完成的支付,钱是进入了这个商户号对应的银行账号,也就是当时申请开通微信支付时填写的银行账号。

redirect_uri是回调页面地址。即,调完这个接口后,微信会回到这个页面地址,把code作为参数放到这个回调页面地址的url中。

response_type应该是取固定值code。

scope有两种取值:snsapi_base和snsapi_userinfo。 以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)。 以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。

在项目中,我发起支付的页面是payStyle,在用户点击微信支付后,我就先判断当前是否运行在微信环境中,如果是,我就调上面的获取code的微信提供的接口,也就是直接将页面的url变为这个接口地址。接口中的回调页面地址redirect_uri我设定的还是payStyle。

2、微信回调回来以后,拿到code,调server端的一个接口getOpenIdForIkangGuoBin,获取open_id。

微信回调回来的地址类似于:http://newuat.im.ikang.com/?code=011oqnmr1FMo8n0gycnr1PyDmr1oqnmk&state=123#/appointment/payStyle。我拿到code,然后调server端的一个接口,实际上server端是拿着我传给他的code去调用了微信的另一个接口,微信会将这个微信用户针对这个公众号的openid返回给server端,server端再把这个openid返给我。

3、在前端拿到openid之后,调用server端的一个接口getPaymentData,获取调用微信对象的支付方法所需要的一系列参数。

4、在拿到上一步的参数之后,两秒之后,再调用微信官方提供的callpay方法,如下:

 callpay() {
    if (typeof WeixinJSBridge == "undefined") { //eslint-disable-line no-undef, angular/module-getter, angular/di
      var self = this;
      if (this.$document[0].addEventListener) {
        this.$document[0].addEventListener('WeixinJSBridgeReady', self.jsApiCall, false);
      } else if (this.$document[0].attachEvent) {
        this.$document[0].attachEvent('WeixinJSBridgeReady', self.jsApiCall);
        this.$document[0].attachEvent('onWeixinJSBridgeReady', self.jsApiCall);
      }
    } else {
      this.jsApiCall();
    }
  }

  jsApiCall() {
    this.isLoading = false;
    var self = this;
    WeixinJSBridge.invoke( //eslint-disable-line no-undef, angular/module-getter, angular/di
      'getBrandWCPayRequest', {
        "appId": self.paramsObj.appId,     //公众号名称,由商户传入
        "timeStamp": self.paramsObj.timeStamp,         //时间戳,自1970年以来的秒数
        "nonceStr": self.paramsObj.nonceStr, //随机串
        "package": self.paramsObj.package,  // 订单详情扩展字符串,统一下单接口返回的prepay_id参数值,提交格式如:prepay_id=***
        "signType": self.paramsObj.signType,         //微信签名方式:
        "paySign": self.paramsObj.paySign //微信签名
      },
      function (res) {
        if (res.err_msg == "get_brand_wcpay_request:ok") { // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回    ok,但并不保证它绝对可靠。
          self.$window.sessionStorage.setItem('payLeave', false);
          self.$state.go('paySuccess');
        }
      }
    );
  }

(1)为什么要延迟两秒才调用callpay?因为在开发中,最开始的时候我没有设置这个延时,拿到接口的相关参数后就直接调callpay,但是发现有的时候能发起支付成功,有的时候不能。后来发现是因为有的时候,执行callpay的时候,WeixinJSBridge对象还没加载完,在检测WeixinJSBridge对象时检测不到,typeof WeixinJSBridge=='undefined',然后会去检测this.$document[0].addEventListener是否存在,检测结果是存在的,于是就会执行this.$document[0].addEventListener('WeixinJSBridgeReady', self.jsApiCall, false);,即给document添加了一个事件'WeixinJSBridgeReady',当'WeixinJSBridgeReady'事件触发的时候,去执行jsApiCall。这个代码是微信官方提供的,我不知道当WeixinJSBridge对象加载完毕之后是否会触发document对象上的'WeixinJSBridgeReady'事件,但我实际测试的时候是没有触发这个'WeixinJSBridgeReady'事件,也没有执行jsApiCall这个方法的。所以,我采取的方法是直接延迟两秒钟后再调用callpay方法,它就会直接去调用jsApiCall方法。

关于这个问题,我在网上查了一下,发现有很多人踩过这个坑。有的人说公众号支付通过WeixinJSBridge这个对象来发起支付不是一个推荐的方法,建议用JS-SDK。这个就是我原来做微信支付时采用的方法。

(2)公众号支付调用的是WeixinJSBridge.invoke这个方法。

二、H5支付

1、什么是H5支付?

H5支付是指商户在微信客户端外的移动端网页展示商品或服务,用户在前述页面确认使用微信支付时,商户发起本服务呼起微信客户端进行支付。 主要用于触屏版的手机浏览器请求微信支付的场景。可以方便地从外部浏览器唤起微信支付。 提醒:H5支付不建议在APP端使用,如需要在APP中使用微信支付,请接APP支付。

2、申请入口

登录商户平台-->产品中心-->我的产品-->支付产品-->H5支付

3、官方体验链接

微信官方体验链接:http://wxpay.wxutil.com/mch/pay/h5.v2.php,请在微信外浏览器打开

4、前端需要做的工作

1、判断当前环境是不是微信环境,如果不是微信环境,就走H5支付。

判断方法同前公众号支付。

2、调server端的一个接口getPaymentData,拿到一个前端接下来要跳转的地址:mweburl。

在这个接口中,我会传给server端下面的参数:

this.getSignData = {
        appCode: this.globals.appCode, // 'ikapp-web-dev'
        orderNum: this.orderNum,
        payType: 'WEIXIN',
        terminalType: 'H5',
        redirectUrl: encodeURIComponent(this.$window.location.protocol + '//' + this.$window.location.host + '/#/appointment/payStyle?H5Redirect=1')
      };

其中appCode在统一支付端代表的是一个商户号。redirectUrl表示的是支付完成之后回调的页面地址。这里需要对redirect_url进行urlencode处理。在这里,这个回调地址我写的还是payStyle,也就是说支付完成后还是回到发起支付的这个页面,但是这时会弹出一个弹窗,让用户选择是否已经支付成功。也就是下面这一步。

3、回调回来后,让用户去点击按钮触发查单操作。

为什么我这里不直接回调到paySuccess页面呢?因为微信这里回调的操作并不是发生在支付成功之后,这里回调指定页面的操作可能发生在: 1、微信支付中间页调起微信收银台后超过5秒; 2、用户点击“取消支付”或支付完成后点“完成”按钮。 也就是说,回调回来,有可能是已经支付成功了,也有可能是取消支付了,也可能是支付失败了,或者可能根本就没发起支付。比如说,我在PC上Chrome浏览器中模拟手机浏览器,我发起H5支付,因为PC上是不可能调起微信实现H5支付的,所以根本就没有发起支付,但是还是会最终回调到这个回调页面。

因此,无法保证页面回跳时,支付流程已结束,所以我这里设置的redirect_uri地址不能是paySuccess页面,而应该像下面的页面一样弹窗,让用户手动去选择。

如果: 1、用户确实已经支付了,用户在弹窗中点击了“已完成支付”,用户点击“已完成支付”时,我会去调server端的一个判断当前订单是否已经支付的接口,如果接口告诉我这个订单已经支付,那么我就跳到paySuccess页面; 2、如果用户确实支付了,点击了“已完成支付”,我调server端接口返回的是还没有支付(可能会出现这种情况,因为状态可能会有延时),那么我会弹出一个提示“您的订单还未完成支付,如您已支付完成,请稍后查询。”,然后页面跳转到订单列表页。 3、如果用户没有支付,点击了“已完成支付”,后面的操作同上。 4、如果用户没有支付,点击了“取消”,那么还是留在payStyle页面,让用户可以选择其他的支付方式完成支付。

H5支付回调页面

payStyle

三、采用JS-SDK实现公众号支付

下面再来总结一下JS-SDK这个方法。

步骤一: 在公众号后台配置相关参数“网页授权域名”、“JS接口安全域名”,在商户后台配置“支付授权目录”。

参数名称 配置地址 意义 可配置个数
网页授权域名 公众号后台-公众号设置-功能设置 在获取code的接口中配置的redirect_uri必须在这个域名下 1个
JS接口安全域名 公众号后台-公众号设置-功能设置 调用微信开放的JS接口的页面必须在此域名下 3个
支付授权目录 产品中心-开发配置-支付授权目录 调起微信支付的页面所在的目录 5个

步骤二:在需要调用JS接口的页面引入JS-SDK: <script src="https://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>

步骤三:通过config接口注入权限验证配置。 所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用,目前Android微信客户端不支持pushState的H5新特性,所以使用pushState来实现web app的页面会导致签名失败,此问题会在Android6.2中修复)。

wx.config({
    debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
    appId: '', // 必填,公众号的唯一标识
    timestamp: , // 必填,生成签名的时间戳
    nonceStr: '', // 必填,生成签名的随机串
    signature: '',// 必填,签名
    jsApiList: [] // 必填,需要使用的JS接口列表
});

步骤四:通过ready接口处理成功验证。

          wx.ready(function () {
            wx.checkJsApi({
              jsApiList: ['chooseWXPay'], // 需要检测的JS接口列表,所有JS接口列表见附录2,
              success: function () {
                // 以键值对的形式返回,可用的api值true,不可用为false
                // 如:{"checkResult":{"chooseImage":true},"errMsg":"checkJsApi:ok"}
              }
            });
          });

config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。

步骤五:通过error接口处理失败验证。

wx.error(function(res){
    // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
});

四、关于在前端做一个微信支付的模块,当有别的web应用需要开通微信支付功能的时候能调用该模块,从而快速实现微信支付功能的设想。

最开始我用JS-SDK实现了微信支付的时候,公司就经常提出这样的需求:某一个web应用现在需要开通微信支付,能不能利用我之前的微信支付的代码来快速实现?当时我真的没想明白,有没有实现这种需求的可能性?今天结合最近做的公众号支付和H5支付好好思考了一下。

首先,我想做一个微信支付的公共模块,具体是一个怎样的应用场景?应该是有了一个web应用,比如说域名是m.abc.com,然后希望开通微信支付功能。

1、那么首先它一定是没有自己的服务号的。因为如果它有自己对应的服务号,那么就可以开通微信支付功能,就可以在自己公众号的后台配置“网页授权域名”等,完全可以自己开发,没必要用现有的我的这个微信支付模块。就是因为它没有自己的服务号,他们想用我的服务号来实现。

2、如果要复用我的微信支付模块,有一个前提是这个系统的微信支付需要支付到我现在的微信支付对应的商户号中。因为,由上一点,他们要复用我的服务号,因为一个服务号开通微信支付功能后会有一个商户号,一个商户号对应一个银行账号。也就是说,如果别的web应用要复用我的微信支付模块,钱是会打到跟我们同一个商户号中,同一个银行账号中。

3、既然是要用同一个公众号来实现微信支付,那么因为公众号后台中“网页授权域名”只能配置一个,网页授权域名的意义是当进行网页授权获取用户openid时,调微信接口获取code时的回调页面必须在这个域名下。也就是说,新的web应用发起微信支付的页面可以是在自己的系统中,在自己的域名下的页面,但是获得code后的回调页面必须是我的web应用中的页面,比如说我的域名是 m.publicModule.com,那么在我的公众号中“网页授权域名”就是m.publicModule.com,m.abc.com这个web应用的发起支付的页面可以在它自己的系统中,但是回调页面必须在m.publicModule.com下。所以说,我可以做一个公共的回调页面,假设这个公共的回调页面为m.publicModule.com/weiXinPay。那么在这个回调页面中需要做一些什么事情呢?下面先列举一下我的系统中这个页面做了哪些事情。(以公众号支付为例)

(1)从url中拿到code。

(2)调server端的接口getOpenIdForIkangGuoBin拿到该微信用户针对该公众号的openid。这个接口传参和响应如下:

传参 响应
code openid

(3)调server端的支付接口getPaymentData,拿到调用微信对象的支付方法WeixinJSBridge.invoke需要的参数。这个接口传参和响应如下:

传参
appCode 'ikapp-web-dev'
orderNum this.orderNum
payType 'WEIXIN'
terminalType 'WAP'
openid data.results[0]
响应 说明
appId //公众号名称,由商户传入
timeStamp //时间戳,自1970年以来的秒数
nonceStr //随机串
package // 订单详情扩展字符串,统一下单接口返回的prepay_id参数值,提交格式如:prepay_id=***
signType //微信签名方式
paySign //微信签名

(4)调用WeixinJSBridge.invoke,实现公众号支付。

WeixinJSBridge.invoke( //eslint-disable-line no-undef, angular/module-getter, angular/di
      'getBrandWCPayRequest', {
        "appId": self.paramsObj.appId,     //公众号名称,由商户传入
        "timeStamp": self.paramsObj.timeStamp,         //时间戳,自1970年以来的秒数
        "nonceStr": self.paramsObj.nonceStr, //随机串
        "package": self.paramsObj.package,  // 订单详情扩展字符串,统一下单接口返回的prepay_id参数值,提交格式如:prepay_id=***
        "signType": self.paramsObj.signType,         //微信签名方式:
        "paySign": self.paramsObj.paySign //微信签名
      },
      function (res) {
        if (res.err_msg == "get_brand_wcpay_request:ok") { // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回    ok,但并不保证它绝对可靠。
          self.$window.sessionStorage.setItem('payLeave', false);
          self.$state.go('paySuccess');
        }
      }
    );

以上4个工作是我在我的回调页面中实现公众号支付所做的事情。 那么如果我把weiXinPay作为一个公用的回调页面的话,以上的工作中,有哪些工作是必须放在weiXinPay中做的?有哪些工作是应该放在各自的web应用中做的?

首先,第(1)个工作(从url中拿到code)肯定是在weiXinPay中完成的,拿到之后,可以跳转到别的web应用中,比如跳转到m.abc.com中的某个页面,将这个code作为url中的参数传过去。

第二,第(2)和第(3)个工作(调server端接口拿openid和调server端接口拿调微信支付接口所需参数)我觉得应该放在各自的系统中进行,不在公共模块中进行。比如说获取微信支付接口所需参数这个接口,里面有一些个性化的参数(如orderNum ),我觉得各个web应用系统是不能通用的,应放在各自的应用中处理。

第三,第(4)个工作(调用WeixinJSBridge.invoke,实现公众号支付)必须放在weiXinPay中完成。因为,这是调用了微信开放的JS接口,即微信支付接口,而微信规定调用微信开放的JS接口的页面的域名必须是“JS接口安全域名”中配置的域名。这个“JS接口安全域名”是在公众号后台配置的,最多配置3个。如果把这个工作放在各自的web应用系统中,那么最多配置3个web应用系统的域名,那就不能称之为公共模块了,为了能适用无数个web应用实现微信支付,这个工作必须在公共模块weiXinPay中完成。

另外还有一个原因:商户后台配置了一个“支付授权目录”,最多可以配置5个URL。微信规定,调用微信支付接口的页面必须在这个“支付授权目录”下。也就是说,如果要实现公用,可以同时让多个web应用都用这个公众号实现微信支付的话,这些web应用的调用微信支付接口的页面只能是一个,也就是weiXinPay。

由上可以看出,按这种设想的话,WeixinJSBridge.invoke方法需要的参数(appId、timeStamp、nonceStr、package、signType、paySign)需要由别的web应用页面跳转到公共页面weiXinPay时传过来。怎么传过来呢?目前我知道的只有通过url,作为url中的参数带过来。这里就有问题了,这些参数通过url传递安全吗?这些参数有时效性吗?

如果这些都没有问题的话,那么我觉得以上的设想,即多个不同域名的web应用通过一个公众号实现微信支付,支付到同一个微信公众号对应的商户号中理论上是可以实现的。

zwrqq2009 commented 6 years ago

你好,h5支付拼接了redirect_uri,但是手机上的chrome没跳回写的redirect_uri?你有遇到过这样的问题吗?