注意:文章中讨论的 IAP 是指使用苹果内购购买消耗性的项目。
这次为大家带来我司 IAP 的实现过程详解,鉴于支付功能的重要性以及复杂性,文章会很长,而且支付验证的细节也关系重大,所以这个主题会包含三篇。
第一篇:[iOS]贝聊 IAP 实战之满地是坑,这一篇是支付基础知识的讲解,主要会详细介绍 IAP,同时也会对比支付宝和微信支付,从而引出 IAP 的坑和注意点。
第二篇:[iOS]贝聊 IAP 实战之见坑填坑,这一篇是高潮性的一篇,主要针对第一篇文章中分析出的 IAP 的问题进行具体解决。
第三篇:[iOS]贝聊 IAP 实战之订单绑定,这一篇是关键性的一篇,主要讲述作者探索将自己服务器生成的订单号绑定到 IAP 上的过程。
不用担心,我从来不会只讲原理不留源码,我已经将我司的源码整理出来,你使用时只需要拽到工程中就可以了,下面开始我们的内容 。
作者写了一个给 iPhone X 去掉刘海的 APP,而且其他 iPhone 也可以玩,有兴趣的话去 App Store 看看。点击前往。
上两篇文章已经针对 IAP 的九个大的问题中的八个问题进行了详细的讲解,如果你没有看上一篇文章,建议你先去看一下再回来,因为这三篇文章是循序渐进的。上一篇文章解决了第一篇文章提出的九个问题中的八个,还剩下一个,这一个问题相当关键,所以单独用一篇文章来讲解。
01.为什么如此关键?
到现在为止,是不是感觉所有的问题都运筹帷幄,心里有数了?
那只是假象,show me the code
,编程不是纸上谈兵,而是需要亲自动手实践,细节是魔鬼。有位前辈说:“同样是一个 for 循环,你写在这里只值 5 毛钱,但是我写在那里就值 5 万块”。当然这不是炫耀,而是想夸张的表达编程中细节的重要性。
前两篇讲的内容已经可以串起来一个相对严谨的支付流程了。但是要把整个流程串起来,还差了关键的一步,而这一步并非易事,至少作者走这一步就非常不容易。
这一步是什么呢?就是要将公司服务器生成的订单号 orderNo
绑定到苹果的交易 paymentTransaction
上。第一篇文章中说了,苹果的规范是用一个 product
生成一个 payment
,然后将这个 payment
推入到 paymentQueue
之中,最后我们成为交易事务的监听者,在监听方法里拿到交易的 paymentTransaction
,我们放进去一个苹果的 payment
实例,最后得到的是一个 paymentTransaction
。
问题来了,我们最后拿到的是一个 paymentTransaction
,苹果只告诉我们
哪一个 paymentTransaction
成功了,而我们根本就没法将我们自己的订单号绑定到这个成功的 paymentTransaction
上,从而建立映射,正确的去后台验证这个订单。
而将我们自己的订单映射到 paymentTransaction
又是必须的,下面就一起来看看这揪心的最后一步是怎么走的。
02. 堪当大任的 applicationUsername?
我不相信苹果会连这个问题都没想到,于是就去找文档, paymentTransaction
里有一个 payment
,这个 payment
就是我们自己用 product
创建的,但是 payment
的所有属性都是 readonly
的,没法更改。好在有一个 SKMutablePayment
,这个家伙的有些属性是 readwrite
的,其中有一个属性叫做 applicationUsername
。
var applicationUsername: String
An opaque identifier for the user’s account on your system.
这是一个 iOS 7 以后才有的属性,可以允许我们自己往 payment
里保存一个字符串类型的数据。
这不就刚好嘛,我就说苹果不可能连这么简单的需求都想不到。好,就用这个属性就 OK 了。当用户点击购买的时候,首先去后台生成一笔交易,然后拿到交易订单号 orderNo
,然后将这个订单号保存到 payment
上面,然后在苹果支付成功的回调中获取到 paymentTransacion
,然后从这个 paymentTransacion
的 payment
中将保存的订单号取出来,那么就能实现我们自己的订单号和苹果的订单一一映射,perfect!
作者刚开始就是按照这个原理去实现的,直到功亏一篑。
事情是这样的,作者公司的测试发现一旦某个订单未推入 keychain
中持久化,而是等重启的时候再去检查未持久化的交易然后将其推入持久化队列的时候,就会产生崩溃,从 bugly 后台看到的数据显示,是因为取 applicationUsername
的时候取不到。然后我就连上电脑测试,发现只要将 APP kill 掉,再次去取之前保存的 applicationUsername
的时候就是 nil
。说到底就是苹果根本就没有给我们存进去的信息做持久化,苹果自己的属性都有持久化,唯独 applicationUsername
没有。
“鸡肋鸡肋,食之无肉,弃之有味”,形象的表达了 applicationUsername
这个属性的尴尬。show must go on,还是得继续寻找这关键一环的解决方案。
03.充分利用 purchasing?
接下来我就尝试,既然苹果不给我们的 applicationUsername
属性做持久化,那能不能我们自己来做呢?
所有的交易都是有唯一的交易标识的,我们如果能将所有的交易在 purchasing
状态就存起来,那么当某笔交易是 purchased
的时候,我们就能以交易标识为引子去一堆之前保存的 purchasing
状态的 paymentTransaction
中找到对应的交易,然后取到我们之前持久化的 applicationUsername
。如果这样能行得通,那我们就又能把整个过程串起来了。
“理想很丰满,现实很骨感”。某笔交易状态还是 purchasing
时,支付系统还没有为这笔交易分配交易标识,所以就算是存了,也没有办法在那笔交易的状态变为 purchased
时从之前持久化的数据中找到存的数据。
这个方案也只能作罢。
04.粗放式验证?
从以上两个尝试再结合苹果后台不对账的风格,我们大致能体会到,IAP 的设计思想就是不想让我们能够将自己的订单关联到 IAP 的订单,这也符合苹果一贯想控制一切的作风。
在真正的解决方案浮出水面之前,作者规划了一种“粗放式的验证”来应对这种窘况,下面我们来讲一下什么叫做“粗放式验证”。
我们将进入 purchasing
的所有订单都持久化起来,然后此时虽然没有分配交易标识,但是产品标识还是有的。等某笔交易到了 purchased
的时候,我们用这个 purchased
的交易的产品标识去持久化的交易中将所有是这个产品标识的交易都取出来组成一个数组,然后任一取一笔进行验证,只要验证成功了,就算交易成功。
如果难以理解,那我们就对着上面这个图来看看。我们将自己的订单号存到交易里,然后将交易存起来,那么自己的订单号也得到了持久化。以后在 purchased
的时候去取任意一笔交易的时候(指定产品标识的),其实取的是我们后台生成的任意一个交易订单号(指定产品标识的),然后将已经完成的 IAP 交易和我们的订单号拼接组合起来进行验证。
这种方案确实是能达到我们验证的目的。但是对于有洁癖的同学来说,这个方案只能算是过渡方案,称不上完美,更谈不上优雅,所以只能叫做“粗放式的”。而且有一个没法避免的问题是,我们存的那么多 purchasing
状态的交易,只有少数能在使用以后删除,大多数都是无效的。但是我们又没有一个契机能去清理这个持久化数据,因为我们根本无从知道那个交易是有用的,哪个是无用的。所以我们只能全部保存,不敢清理,这样导致这个持久化数据越来越多,却没有清理的可能。
05.打破思维惯性
现在想明白了就会知道,以上的尝试迂迂回回,都是掉进了思维惯性里了。我们严苛遵循了古老的传统:先去自己服务器创建订单,再使用 IAP 交易。其实突破点就在这里,我们后端的一个同事提出,先去苹果那里交易,交易完成以后再去我们自己的服务器创建订单是否可行?
还记得第一篇文章中的这张图吗?
我们调转支付流程以后,应该变成下面这样。
我不做解释了,聪明的你一定知道这个微妙的区别带来的极大的便利。至此,订单绑定得到了优雅的解决。
06.方案缺陷分析
如果是按照这个逻辑来走的话,有一个很显而易见的逻辑缺陷,从 IAP 支付到我们去后台创建订单这个过程有苹果支付的和我们创建订单的延时。现在情景是用户 A 发起了支付,然后还未购买就退出了登录,然后用 B 账号登录了,然后 IAP 支付成功,我们将支付信息存进了以 B 的 userid
为 key 的账户中,这样就会导致我们去后台验证的时候会把钱充到 B 账户中,如下图所示。
所以我们在用户退出登录的时候需要去检查他是否有未完成交易,如果有就要给个警告。但是还是没办法彻底解决掉这个问题,但是考虑到这个结果是用户的行为导致的,而且出现这个问题的几率不大,暂时就这样处理。
如果你确实有这方面的担心,那就应该采用上面说的粗放式的验证,粗放式的验证是不存在这个问题的。
我的文章集合
下面这个链接是我所有文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 Github 地址,Github 上都有源码。
网友评论
第一个关于刷单的问题:
有人恶意使用同一个receipt-data(收据)重复刷单,这种情况如何处理比较好。
第二个是关于receipt的问题,以下的receipt都是针对消耗型内购:
1、receipt是在交易finish后就在手机沙盒中清除了吗?
2、向苹果服务器发送receipt进行订单验证时,返回信息中(in-app字段)包含了很多笔订单的数据,为什么包含这么多笔?
3、手机沙盒中的receipt会在什么情况下改变?
另外applicationUserName用来绑定订单号不太好。
参考一下这个文档里的Detecting Irregular Activity这一段
https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/RequestPayment.html
这个字段是用来检查Irregular Activity(不正常的交易)
In contrast, it would be very unusual for a single user on your server to buy coins multiple times, paying for each purchase from a different App Store account
也就是说一个游戏用户如果使用太多个itunes账号 ,或者一个itunes账号给太多个游戏用户充值,是一件非常不合理的事情,这种情况下不知道会不会有什么风险,既然苹果已经强调了这个风险。这个地方只适合放用户的用户id和区服,因为这个值对于同一个用户是永远不会变的。
forUser:(nonnull NSString *)userid 这个方法, 遍历 modelsExisted 的时候, 进行了[modelsExisted removeObject:modelExisted], 这个不行, 最好换成enumerateObjectsUsingBlock, 这个可以在遍历的时候移除元素
我认为oid的作用 就两个 一个是对应到商品 一个是对应到人物关系
这两个作用 transactionID也是可以直接做到啊
因为你这个处理逻辑是后创建订单
那干嘛还要创建这个订单呢 直接把凭证给服务端 让服务端自己做订单创建和绑定不就好了
我目前做的处理一个是IAP时只允许串行交易,用loading动画过渡,完成一笔交易就finish,避免未完成当前交易,导致无法进行下一次交易,苹果的弹窗提示会让用户产生很不好的感受;
再就是持久化的时候只存入到了沙盒里,一方面是用Keychain处理逻辑会增加较多,卸载app基本是用户的放弃行为。一方面是支付是一种强意识的行为,用户选择购买道具还是会比较慎重对待,极端的支付中卸载app的情况不做考虑,客户端对断网的情况做好数据处理就可以接受。applicationUsername这个确实挺坑的,不过还是凑合用了,正常情况毕竟是大多数不是嘛...
其实做了这些处理也都是尽量降低丢单率,人工客服处理还是必不可少,苹果爸爸这些年改变了那么多功能,IAP这一块还真是没怎么变...
有一个场景,支付成功时苹果回调的applicationUsername一定为nil的:
只要在点击购买后立马断网,这时候扣费请求其实已经发出,最后也扣费成功了,只是用户断网了无法收到苹果回调而已,如果用户重启APP(恢复网络后进入后台再回到前台也会收到苹果回调支付成功,原因未明),就会收到苹果回调这一笔订单,但是applicationUsername一定为nil.
开始也有考虑你们所说的"粗放式的验证",后来觉得这样处理不太好,然后采用这样的一个方案:在接收到苹果回调时,如果发现applicationUsername为nil的话,就将这个订单放到一个待组装订单号的队列中,根据商品id重新向服务器生成订单,不过这样也一样会有充错单的可能性
还有就是在持久化时,并没有使用Keychain,我是直接写在沙盒的,只要没有finish掉苹果的订单,就算卸载App,苹果会一直给你回调那个订单的,重新写入沙盒就可以了.
之前做过内购,依稀记得有个cancelTransaction的API。
只要退出登录的时候,把原来账号的交易全取消掉,最后的方案就很不错了。