前言
之前项目有用到了一些第三方支付,包括 paypal, google iap, stripe, apple iap, 还有国内的 alipay。其中每个支付类型都有一些坑,本章讲的是使用paypal 第三方支付的时候,遇到的一些问题,以及那时候是怎么去解决的。
交易记录查询不到
之前客服有反馈一个情况,就是突然间有一天,很多初次付款的 paypal 用户,都在反馈他们付款成功了,但是没有升级上来,后面查了一下 log, 发现在收到 paypal 的 PAYMENT.SALE.COMPETED 的 webhook 的时候,我们马上去查交易记录。但是只有查到一条循环创建的记录,没有查到交易成功的记录,所以我们程序就误认为付款其实还没有成功,就没有再继续下去了, 但是其实这时候交易是有成功的。
因为当我过了不到一分钟再去人工查这个交易记录的接口的时候,就会发现已经变成两条了。 一条是循环建立的记录,另一条就是交易记录。
而且最开始的 webhook 的 data 里面,也可以看到 resource 里面也有新订单的信息了,但是那时候查的时候,transactions 交易记录里面就是只有一条?
解决方法:
在 COMPLETED 阶段的 webhook,虽然有可能交易记录还没有更新过来,但是这时候 webhook 里面已经有新的交易订单号了,因此遇到这种情况的话,其实应该去判断 webhook 里面的 resource 里面有没有当前最新的交易单号,如果有的话,那么其实就是合理的,就是应该升级了。
循环创建和交易完成的webhook顺序颠倒了
今天运营又反馈了一个问题, 就是有一个用户paypal 付款成功了,但是有没有升级上来。后面查了一下,竟然不是之前一直反馈的那个交易记录没有更新的问题。而是另一个奇葩的问题,就是 webhook 的循序颠倒了。
1 | [2019-01-06 21:09:23] Paypal.DEBUG: Event Type: PAYMENT.SALE.COMPLETED [] [] |
可以看到竟然是 PAYMENT.SALE.COMPLETED 这个 webhook 先过来,然后 BILLING.SUBSCRIPTION.CREATED 这个再过来。这样顺序就颠倒了,这样就会导致 completed 的时候,根本就找不到 循环订单的记录 ???
解决方法:
就是如果收到 completed 事件,但是在表里面找不到循环id的时候,这时候就要手动触发 subscription create 的事件,让它插入到订单循环表中,然后再继续进行 completed 的操作,这样就可以了。无论是首次支付 completed 先到的情况,还是往期循环,但是订单循环表里面没有记录,都可以用这种方式来处理。
然后如果接下来 subscription create 过来的时候,如果循环已经存在了,那么直接更新它的下一次付款时间就行了
webhook 延迟很久才过来
今天有出现过一个paypal的情况,就是 有些用户反馈付款成功之后,好几个小时才升级成功。后面查了一下log,发现确实是这样子,以这个为例: I-HV93GMPJxxxx, 他在03:05 分的时候就付完款了,03:07 分的时候,就收到 sale.completed 的 webhook 了,但是这时候查询交易记录的时候,只有一条,就是循环建立那一条,根本没有支付成功的那一条。一直到 06:11 ,也就是过了3个小时之后,又过来了 sale.completed webhook。
但是从支付完之后,到升级上来,webhook 延迟了 3个小时,这样太坑爹了,严重影响用户体验。
解决方法:
跟第一种的方法一样,如果交易记录查不到也没有关系,只要 webhook 有带有效订单号,那么就可以直接插入了,这样就可以解决这个问题。
订阅设置的坑
之前有出现过一种情况,就是有一个 paypal 的循环用户,他的上个月,因为卡里面没有钱,所以上个月没有扣款,然后这个月卡里有钱了之后,paypal就再给他扣了一笔款(相当于这个月扣了两笔)。后面查了一下,发现在他的循环订单中,有一个配置:
就是这个配置 “将失败的付款添加到下一个结帐期” 为 yes。 导致 paypal 会将这个月失败的付款,添加到下个月的结账期。 导致下个月扣了两笔钱。
后面查了一下 paypal 的api ,发现确实有个api 是可以设置这个项: 这里
1 | /** |
默认是 NO, 但是之前我们在创建 plan 的时候,这个值其实是设置为 yes 的
1 | $merchantPreferences->setReturnUrl($returnUrl) |
导致订阅了这个用户,就会附加上这个配置。 而且用户如果已经订阅了之后,还不能在 paypal 后台改: 这边。
那么能不能直接改这个 plan 的设置呢:
1 | public function getPlan(){ |
但是我试了一下,虽然调用好像成功了,但是没有效果???
所以只能在新建的 plan 订单中,将他设置为 NO
1 | $merchantPreferences->setReturnUrl($returnUrl) |
通过 EC-token 请求循环的接口失败
之前有遇到一种情况,就是循环支付的return接口的时候,通过 EC-token 去得到交易记录的时候, 这个请求在很偶然的情况下会失败,而且没有任何的 response 的值,catch 不到东西。
1 | https://api.paypal.com/v1/payments/billing-agreements/EC-1TP65860SG56xxxxx/agreement-execute |
这样就没办法在库里面通过这个EC-token,去得到对应是循环 Id,因此这一单循环就没有着落了。因为 token 只有在支付结束返回的 return 接口才会带, 这边会变得很麻烦,所以目前只增加错误日志 log。但是后面还是 catch 不到东西,就好像这个请求丢了似的。
解决方法:
就是将当下没有请求成功的token保存起来,然后在 ipn 或者 webhook 过来校验 recurringId 找不到的时候,再进行请求。保存 token ,我用的是 redis 的 有序集合 sorted set。 通过这个有序集合可以在进行筛选的时候,去掉已经过期的token, 假设过期时间是 10 分钟:
1 | // 存放 paypal token 的有序集合const |
主要就三个方法,将token入集合, 在集合里面删除token,获取当前有效的token,并去掉过期的token。接下来就是具体的应用了,有两个地方会用到,一个是 return 接口方法的时候:
1 | if ($token) { |
分别在请求之前添加,请求成功之后删除。还有一个就是当 ipn 或者 webhook 过来的时候,如果找不到的话,就进行再次请求:
1 | if ($order == null) { |
这样子就大大的提高了成功率。事实上,这样做之后,就再也没有出现这种情况了。
支付成功,completed 没有过来,只过来 update 状态
最近经常有出现过 paypal 支付成功了,但是却没有升级上来的情况,后面发现原来是没有 completed 事件,而是改成 update 事件了。
解决方法:
通过观察我们发现, 当为 update 状态的时候,有一种情况,一种是如果用户没有支付完成的时候,这时候订单号其实是循环单号,而不是真正的订单号,只有当用户支付成功之后,这时候如果 update 状态过来,这时候的订单号才是真正的订单号。
所以我们这么改: 如果是 update 事件,并且当前的 order id 不是循环 id 的话,那么说明就是有效订单,那么就要升级上来, 这时候就不要再去等 completed 的 webhook 了,直接升级, 因为有可能 completed 的 webhook 会非常慢。
1 | //如果是Updated,orderid不等于recurringid,则认为是支付成功的。 |
关于 paypal 的循环支付的 return 接口返回两次的问题
之前有出现了一种情况,就是: 在 paypal 的 return 接口的时候, 有时候会发现 paypal 会调用两次。导致会出现两种情况:
- 第一次和第二次相隔的时间会比较长的时候,这时候在第二单的时候, 我的 EC-TOKEN 已经被用掉了,接下来已经找不到对应的订单了
- 第一次和第二次的时间差不多,第二次到的时候,第一次可能还没有执行完。但是这时候第二次就会在请求 agreement 的时候,就会报这个错,结果页面就直接跳到错误页面了
第二种情况就会出现这个 400 错误:
1 | { |
其实处理方式很简单,先把第一次 return 过来的数据,先保存下来(保存到 redis),如果第二个过来的时候,找不到订单的话,直接通过 token 作为 key,从 redis 里面取出数据,然后继续执行接下来的流程。
支付成功,completed 没有过来,但是过来 update 状态的问题
最近经常有出现过 paypal 支付了,但是却没有升级上来的情况,后面发现原来是在检查交易记录的时候,发现没有 completed 事件,而是 改成 update 事件了。而且这个 update 事件的 transaction id,有可能是循环单号,也有可能是支付单号。
所以后面如果是收到 updated 的情况,如果当前的订单号,不是循环单号,而是真正的支付单号的话,那么就说明其实用户已经支付成功了,这时候其实就应该升级了。不应该再去等待 completed 过来,因为有可能 completed 过来会非常慢。
代码如下:
1 | //如果是Updated,orderid不等于recurringid,则认为是支付成功的。 |
因为浏览器插件无法付款的情况
之前客服有反馈了一个问题,就是有一个用户他没法用PayPal付款,点击PayPal后就显示我们产品的支付失败页面。
出现这个页面有两种页面:
- 一种就是有到PayPal的页面去付款,只不过付款失败了,recurringReturn 接口校验的时候,失败了,所以返回这个页面
- 另一种就是根本没有到PayPal的页面去,而是直接返回这个错误页面,这种情况就是浏览器插件屏蔽了PayPal的页面请求,导致直接返回这个页面。
后面查了一下,发现根本请求就没有到我们的服务器,所以是第二种情况,应该是浏览器插件屏蔽了PayPal的页面跳转,所以有让客服跟这个用户说下,让他屏蔽掉浏览器插件再试试