前言
在之前写的 苹果内购(IAP)从入门到精通(3)- 商品充值流程(非订阅型)中,已经简单介绍了整个消耗型商品(非订阅型的商品还有其他类型,但消耗型商品最常见)的充值流程。大致流程为:
添加监听(addTransactionObserver)-> 添加新的交易(addPayment)-> 付款 -> 成功返回Purchased状态(SKPaymentTransactionStatePurchased)-> 校验支付票据 -> 校验成功则下发道具 -> 结束交易(finishTransaction)
但结合实际业务并且线上可以稳定使用,还有很多地方需要处理,以及一些异常情况需要注意。
以下我将结合我个人的业务经验,阐释一些实际业务场景与IAP的结合,以及一些线上大量数据验证出来的一些异常情况 。
1. App启动时以及发起新的充值之前,需要检查是否有未完成的交易
在之前写的 # 苹果内购(IAP)从入门到精通(5)- 掉单处理、防hook以及一些坑 的4.(4)点,我简单说明了需要在 App启动时
和 发起新的支付之前
开启支付队列监听。
这两个场景处理后,若有已付款但未完成(没有调用finishTransaction)的交易,则会在交易队列(SKPaymentQueue)返回给开发者,开发者先处理这笔交易的票据校验;若返回空的交易数组,那么再继续创建新的交易。
需要注意的是,addTransactionObserver添加多次同一个监听对象,实际只是添加的一个。所以在这两个场景里,要想每次调用后都能收到交易队列的返回结果,需按照 启动时调用addTransactionObserver,发起新的支付之前调用restoreCompletedTransactions
来检查交易队列。
有一些团队觉得可以不用检查交易队列,认为掉单的概率很低。但我们结合线上经验发现,因为苹果的接口请求失败,或者用户主动在付款过程中杀死进程(比如loading卡死了),很容易导致掉单。所以这里建议一定要在这两个位置都检查一遍。
那么为什么是 “先调用addTransactionObserver,后调用estoreCompletedTransactions
” 呢?以下是几种调用方式分别会造成怎样的场景的举例:
- 场景一:无未完成的交易 –> 启动调用addTransactionObserver –> 不会回调任何消息 –> 支付前调用addTransactionObserver –> 不会回调任何信息 –>
流程卡死
- 场景二:无未完成的交易 –> 启动时不做操作 –> 支付前调用restoreCompletedTransactions –> 不会回调任何消息 –>
流程卡死
- 场景三:无未完成的交易 –> 启动时不做操作 –> 支付前调用addTransactionObserver –> 不会回调任何消息 –>
流程卡死
-
场景四:无未完成的交易 –> 启动addTransactionObserver –> 不会回调任何消息 –> 支付前不判断是否有未完成交易,发起一笔新的交易 –>
正常进行
这个场景看似没啥问题,但如果结合实际APP业务中的订单号匹配,可能会导致
订单号与TransactionId不匹配
的问题。比如:-
“订单A-6元商品-TransactionA”购买成功 –> 校验票据前网络中断,TransactionA暂时搁置 –> 再次创建新订单“订单B-6元商品” –> 交易队列此时发起订单B的支付,会优先回调Purchased状态的TransactionA –> 校验票据(若此时用的订单B的订单号去和TransctionA匹配,业务层可能会匹配错误)–> (假设成功)下一次购买再次发起“订单C-6元商品” –> 恢复正常充值流程
-
“订单A-6元商品-TransactionA”购买成功 –> 校验票据前网络中断,TransactionA暂时搁置 –> 再次创建新订单“订单B-12元商品” –> 正常充值付费12元并校验票据成功 –> 再次创建新订单“订单C-6元商品” –> 走到上一条的流程,即回调交易队列优先回调Purchased状态的TransactionA
-
- 场景五:无未完成的交易 –> 启动addTransactionObserver –> 不会回调任何消息 –> 支付前调用addTransactionObserver –> 回调空的交易数组,判断没有可恢复的交易 –> 发起一笔新的交易 –>
正常进行
- 场景六:有未完成的交易–>启动addTransactionObserver –> 回调未完成的交易,重新进行票据校验 –> 支付前调用addTransactionObserver –> 回调空的交易数组,判断没有可恢复的交易 –> 发起一笔新的交易 –>
正常进行
综上所述,按照 启动时调用addTransactionObserver,发起新的支付之前调用restoreCompletedTransactions 来检查交易队列,最能满足业务层的多种场景,且不会有bug。
Tips: 对于 restoreCompletedTransactions 这个方法,按照苹果官方文档上说的:
Use this method to restore finished transactions—that is, transactions for which you have already called finishTransaction:(使用此方法恢复已完成的事务,即您已为其调用 finishTransaction: 的事务。).
即这个一般用于恢复订阅型商品(的确恢复订阅型商品是用到这个方法)。但经过我这边测试,没有finish成功的消耗型商品的Transaction,也能够在调用这个方法后,通过交易队列返回。
2.缓存订单号的时机和位置
除了苹果自己的交易流水号TransactionId以外,大多数开发者都有属于自己(或公司)业务的订单号(下面以orderId代替)。若支付成功后,向苹果校验票据时,一般会通过将orderId和transaction去比对,判断票据里的这笔交易(Transaction),是否就是这个orderId对应的商品,若匹配成功,才会向用户下发实际购买的道具。
但因为支付完成的回调是异步的,所以一般而言,大家都会将orderId做缓存,以尽量做到精准匹配。大多数人会通过applicationUserName
来透传这个订单号,或者在本地缓存一个订单号。
在之前写的 # 苹果内购(IAP)从入门到精通(5)- 掉单处理、防hook以及一些坑 的第2点中,我说明了applicationUserName
在实际应用的某些场景中,很容易丢失导致为空的情况。
这个其实在沙盒环境下很好复现:你输入沙盒密码后,立马杀死app,等待几秒钟,系统层级会弹出“已完成支付”的弹窗,这个时候就表明你是实际付款成功了的
。
再次启动app,启动时检查交易队列,这时这笔交易因为你刚刚的操作没有调用finishTransction,所以交易队列会返回给你,但此时这个Transction对象里的applicationUserName
就变为空了。
这里直接说结论:建议在创建orderId的时候,就缓存orderId到Keychain里。
有些人习惯在Purchased状态回调后再缓存,因为这时候Transction对象已经有TransactionId了,可以一对一匹配。但如果是我上面说的那种场景,就无法缓存到;然后applicationUserName又为空了,就导致orderId丢失了,导致校验票据不通过而掉单。
3. 调用了finishTransaciton:但交易队列中依然存在的现象
通过抓包软件能发现,当代码调用finishTransaciton:时,实际上并不只是本地结束交易,而是会有一个苹果的网络请求,请求成功才算真正finished。而在线上环境下,苹果的接口很容易请求异常,比如网络不好的时候会请求失败。这时候即使调用了finishTransaciton:,但交易队列中依然存在这笔交易。
官方建议的调用finishTransaciton有两种时机:
- 交易状态为
SKPaymentTransactionStatePurchased
SKPaymentTransactionStateFailed
。
这种情况下,Purchased状态的交易,票据校验成功后,调用了finishTransaciton但却没有fished。
那么在下次调用restoreCompletedTransactions检查支付队列时,很可能会返回两次Purchased
状态的交易(都是同一个交易ID),这样就会进行两次票据校验。但因为上一次调用finish之前,票据已经校验通过了,所以这两次票据校验都是重复校验,这并不影响商品的下发,也不会导致掉单。前端只需要针对这种状态做一下界面提示即可。
Failed状态的交易,一般情况下(下面第4点是特例,待会儿会说到)还没有成功付款,所以即使finish失败,下一次检查交易队列时,这笔交易依然会返回Failed状态,此时待网络正常时成功finished即可。
4. 扣款成功但回调SKPaymentTransactionStateFailed状态
因为苹果接口不稳定的原因,线上环境下有一定概率出现“扣款成功了,但返回SKPaymentTransactionStateFailed
状态”的情况,而非返回Purchased状态的情况。而这种情况,一般也会伴随着上面所说的finishTransaciton失败的情况。
网络恢复正常后,下一次启动app或者发起充值之前,去检查未完成交易,这时交易队列会返回一条Purchased状态的交易,交易流水号(TransactionIdentifier)即为之前返回Failed状态的这条交易流水号,这时再重新发起票据校验即可。
再考虑业务层的orderId的情况。有些开发者不仅会在完成交易后删除掉缓存的orderId,也会在交易返回失败后,结束掉orderId。非以上特殊情况其实并不会出现问题。但若是当前描述的这个特殊情况的话,Failed时删除了orderId,并且重启后applicationUserName的透传参数也没了,那么当再次返回Purchased状态的这笔交易时,会因为orderId丢失而导致票据校验失败,从而导致掉单的情况。
因此,这里建议:缓存的orderId只在票据校验成功的时候清除,其他地方不要操作
。如果发起新的充值,旧的orderId也被新的覆盖掉了。
参考:
https://juejin.cn/post/7068899869527638030/