iOS内购掉单完美解决方案-转载|iOS内购掉单完美解决方案-转载 Keep 的方案
本文转载自 Keep 的内购解决方案 , 方案很好.
如果涉及版权问题, 请联系我本人删除根治顽疾:Keep客户端 In-App Purchase 掉单踩坑指南 简介 In-App Purchase(以下简称IAP)是苹果为开发者提供的应用内购服务。Keep于17年初接入In-App Purchase,功能上线后暴漏出严重的丢单问题,丢单概率大概在百分之一。丢单问题在多人多次优化后仍未能解决,成为Keep客户端的顽疾。直至最近的两次优化彻底根治了丢单问题。本文中笔者将循着Keep客户端解决IAP掉单问题的两次优化之旅跟大家分享排查问题的思路以及最终的方案。
历史问题 【iOS内购掉单完美解决方案-转载|iOS内购掉单完美解决方案-转载 Keep 的方案】由于IAP本身设计问题及开发者不恰当使用API导致IAP掉单是一个较为普遍的内购问题。同时,网上存在各种没有数据支撑的所谓的”解决方案”及各种”一站式解决掉单”的标题党,会对开发者产生一定的误导。盲目的引入这些方案在没有解决问题的同时平白增加了代码的复杂度。甚至有一些开发者表示IAP漏单是无法避免的,只能通过客服的介入来进行补单。
在之前的几次排查过程中,由于网上信息的误导,盲目接入了几种网上流传的解决方案。
- 本地化存储 :在收到IAP支付成功回调后,将业务订单号、receipt信息持久化,在app启动时遍历本地存储列表触发补单逻辑。
- 网络异常重试:app校验receipt信息http请求失败时,会触发重试逻辑,连续重试10次。
事实上,这两种方案并不能解决掉单问题,且都存在很大的问题:
- 本地化存储:完全是无效的冗余逻辑,平白增加代码复杂度。
- 网络异常重试:由于缺乏恰当的实现,并不能对补单提供有力的保障。
第一次优化 在翻阅IAP相关文档及明确了Keep客户端中存在的历史问题后,明确了从两个角度进行优化:
- 程序健壮性:提升程序健壮性避免由于网络、crash等导致的掉单。
- 补单实时性:在异常发生时,保障大部分用户实时快速完成补单。
程序健壮性
我们将IAP流程简化为如下:
![iOS内购掉单完美解决方案-转载|iOS内购掉单完美解决方案-转载 Keep 的方案](https://img.it610.com/image/info10/ebc0802412df449496f7b09b854fdaf6.jpg)
文章图片
IAP.png 从流程上来看,由于客户端导致的掉单有两种可能:
- 步骤一:用户支付成功,Apple回调客户端通信失败。
- 步骤二:Apple回调客户端后,客户端与server通信失败。
事务机制
事实上,对于以上两种掉单case,Apple已经为我们提供了合理的解决方案。
IAP中每一次支付行为都被抽象成一个事务(SKPaymentTransaction),只有事务被正常完结(调用finishTransaction:)本次支付行为才算完成。在每一次app启动时,通过调用addTransactionObserver:就会触发之前所有未完结的事务。详见:支付队列观察者。所以,由于事务机制的存在,我们只需做到以下两点就可以避免掉单:
- 对于每一个支付事务,在确保服务端处理完后再结束(finishTransaction:)该事务。
- App启动时,注册支付队列观察者(addTransactionObserver:)并添加相应补单逻辑。
- 本地化存储只能解决”客户端与server通信失败”的掉单场景。
- 本地化存储的数据会随着用户设备更换、app删除重装而丢失而Apple的事务机制不会。
所以第一个优化点在于:依靠Apple的事务机制,同时删除冗余的本地化存储方案。
结果
我们追踪了最近一个月内所有用户支付成功的订单中,通过Apple提供的事务机制恢复的订单。得到如下结论:通过事务机制恢复的订单占总支付成功订单的4.78‰。即,通过第一次优化我们将掉单率降低了4.78‰。
补单实时性
事务机制有一个明显的弊端:补单逻辑只有在app重启时才能触发。 重启app对于用户来说是一个很重的操作。我们希望添加某种机制更实时的为用户进行补单,于是我们引入了”网络异常重试逻辑”以提升补单效率,做为事务机制的一个补充。
网络异常重试
网络异常重试,主要是为了避免在付款成功后,用户网络状况发生变化(如,乘坐地铁进入隧道)导致与server通信失败的及时重试逻辑。
该方案本身没有太大问题,在较低的接入成本与影响面下可以很大程度的提升补单的实时性,是app启动时事务机制补单逻辑的一个很好的补充。不过,需要恰当的实现才能达到最优的效果。
之前Keep的实现方案是:初始化一个计数器,在网络请求失败的回调内累加计数器并触发重试逻辑,直至重试10次后放弃重试。
在实际测试过程中在断网的状况下,发出去的网络请求会立刻拿到失败回调,10次重试请求会在1s内发完。所以该方案能达到的效果被大打折扣。
解决方案当然是拉长重试间隔,另外,由于用户网络恢复的可能是随着时间逐渐递减的,为了避免频繁的重试我们不妨依次延长每一次重试的间隔。Keep目前的方案是以斐波那契数列来做为每一次重试的间隔,即10次重试的间隔分别是:
- 1,1,2,3,5,8,13,21,34,55
下表是包含首次校验receipt失败(checkOrder_Failed)在内的,触发网络异常重试逻辑的埋点:
PS:以下数据没有考虑在重试过程中app异常关闭或用户手动关闭app
事件 | 次数 |
---|---|
checkOrder_Failed | 332 |
checkOrder_Failed_Retry_1 | 166 |
checkOrder_Failed_Retry_2 | 127 |
checkOrder_Failed_Retry_3 | 122 |
checkOrder_Failed_Retry_4 | 103 |
checkOrder_Failed_Retry_5 | 85 |
checkOrder_Failed_Retry_6 | 69 |
checkOrder_Failed_Retry_7 | 52 |
checkOrder_Failed_Retry_8 | 34 |
checkOrder_Failed_Retry_9 | 23 |
checkOrder_Failed_Retry_10 | 15 |
本地化存储 & 补单实时性?
通过本地化存储我们可以在更多的时机来触发补单逻辑以提升补单实时性。如:网络切换、app前后台切换。这里需要权衡的点是:
- 本地化存储、网络切换,app前后台切换逻辑会影响到百分之百的用户(包括非付费用户),同时会有一定的开发维护成本。
- 在加入恰当的网络异常重试逻辑后,网络切换、app前后台切换的补单逻辑能帮助到的用户只有IAP付费用户的万分之几。
第二次优化 第一次优化上线不久:客服再次反馈IAP支付掉单问题。而且由于业务膨胀式的发展,虽然优化掉了约千分之五的掉单case每天掉单的数量反而在上升。
信息收集
收拾了一下心情,继续整理了接下来的工作思路:
- 通过埋点及用户的反馈信息分析用户掉单原因。
- 收集信息撰写TSI联系Apple寻求技术支持。
- 与同行进行沟通,如何解决掉单问题。
- 对于反馈用户的订单号,通过埋点查看走到了支付失败的回调中。
- TSI得到的反馈是:在收到用户支付成功请求后一定会对客户端下发支付成功的回调,且在我们没有调用(finishTransaction:)结束该事务的情况下,会持续在每一次app启动时调用支付成功回调。
- 在与国内两个直播平台的员工进行沟通后:得到的反馈是,的确有丢单的状况。多次排查无果后,主要措施是由人工客服介入补单来处理。
抽丝剥茧
在选择相信用户和苹果的基础上,以Apple的事务机制来套有两点是可以肯定的:
- 在用户支付成功后,Apple回调了支付成功的逻辑。
- 在处理该笔订单的过程中,客户端一定调用了(finishTransaction:)结束该事务。
终于,在转换思路后一个隐藏的bug浮出水面————在收到Apple支付成功回调后,客户端会首先校验业务OrderNo的合法性,如果orderNo为空,会直接调用(finishTransaction:)结束该事务,从而导致掉单!
业务OrderNo
Keep的业务实现逻辑是,在用户发起购买时会生成对应的OrderNo,OrderNo将在整个购买流程中进行透传,直至用户支付成功后的receipt校验。且整个支付流程中在与Apple交互的过程中,通过Apple”提供”(注意这里的引号)的SKPayment的applicationUsername来传递。
Iap支付的订单流转逻辑:
![iOS内购掉单完美解决方案-转载|iOS内购掉单完美解决方案-转载 Keep 的方案](https://img.it610.com/image/info10/583ce0dd1a5f4d3ebb102092f1da009e.jpg)
文章图片
IAP-LOGIC.png 所以问题的症结在于,我们使用applicationUsername透传OrderNo合理么?
那么Apple对于applicationUsername定义是什么?
Use this property to help the store detect irregular activity. For example, in a game, it would be unusual for dozens of different iTunes Store accounts to make purchases on behalf of the same in-game character.
The recommended implementation is to use a one-way hash of the user’s account name to calculate the value for this property.
Apple提供applicationUsername,是为了防止用户作弊而不是用于透传业务信息的。所以,归根结底产生bug的原因还是我们开发者滥用API(目前网上依然有很多IAP相关的讨论、博客都是使用applicationUsername来透传业务信息)。
复现及分析 在上述猜想的基础上,在线上环境下测试了一些边界情况成功复现了掉单case(必须为线上正式包,沙盒环境无法复现,testflight无法复现)。
复现步骤:
- itunes store 登入的appleId未绑定支付方式
- 发起支付
- 绑定支付方式并杀死keep app
- 在appStore完成完成支付
- 重启app
在这种case下,在应用内我们收到的回调状态是这样的:
- Purchasing (Keep app 发起,携带OrderNo)
- Failed(Keep app 发起,携带OrderNo)
- 用户绑定支付方式
- Purchasing(AppStore 发起,不携带OrderNo)
- Purchased(AppStore 发起,不携带OrderNo)
- Keep内发起的支付:创建了OrderNo,也完成了对于applicationUsername的赋值,但是由于用户没有绑定支付方式该笔订单以失败结束,所以我们会收到相应失败的回调。
- AppStore内发起的支付:用户支付成功了,但是并没有创建OrderNo,也没有完成对于applicationUsername的赋值,所以在Apple回调支付成功后,没有解析到OrderNo。调用(finishTransaction:)结束该事务后产生掉单。
KeepClient
- –客户端去掉OrderNo校验逻辑
- –校验接口的OrderNo改为非必传参数
- –支付网关层校验成功后, 发送mq消息给业务方
- –业务方收到消息后进行模拟提单
- –交易中心完结订单
- –交易中心回调业务方接口
- –业务方发放权益
![iOS内购掉单完美解决方案-转载|iOS内购掉单完美解决方案-转载 Keep 的方案](https://img.it610.com/image/info10/3696f89f82a741899b8507554370fa02.jpg)
文章图片
IAP-LOGIC2.png 结果 同样我们以记一个月所有用户支付成功的订单为样本。通过订单创建后置恢复的订单占总支付成功订单的5.25‰。即,通过此次优化IAP掉单率降低了5.25‰,完美解决了客户端的掉单问题。
推荐阅读
- 2020-04-07vue中Axios的封装和API接口的管理
- iOS中的Block
- 记录iOS生成分享图片的一些问题,根据UIView生成固定尺寸的分享图片
- 2019-08-29|2019-08-29 iOS13适配那点事
- Hacking|Hacking with iOS: SwiftUI Edition - SnowSeeker 项目(一)
- iOS面试题--基础
- 接口|axios接口报错-参数类型错误解决
- iOS|iOS 笔记之_时间戳 + DES 加密
- iOS,打Framework静态库
- 常用git命令总结