[iOS]贝聊 IAP 实战之订单绑定

作者: e2f2d779c022 | 来源:发表于2017-12-21 09:51 被阅读2117次

大家好,我是贝聊科技 的 iOS 工程师 @NewPan

注意:文章中讨论的 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,然后从这个 paymentTransacionpayment 中将保存的订单号取出来,那么就能实现我们自己的订单号和苹果的订单一一映射,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 上都有源码。

我的文章集合索引

你还可以关注我自己维护的简书专题 iOS开发心得。这个专题的文章都是实打实的干货。如果你有问题,除了在文章最后留言,还可以在微博 @盼盼_HKbuy上给我留言,以及访问我的 Github

相关文章

网友评论

  • snowimba:简单的订单绑定其实还是可以实现的。在Purchasing调用的时候再去和服务器创建订单,Purchasing状态下transaction里面虽然没啥暴露的唯一ID,但是打印一下发现有个内部属性,下面有个uuid,这个uuid是每个transaction唯一的,不变的,完全可以把这个uuid一起传给服务器生成订单,然后在Purchased之后去服务器验证的时候再把当前transaction里面的uuid取出传给服务器,这样就做到绑定验证了。但是感觉意义不是很大。
    snowimba:@NewPan 这个可以好好研究一下。
    e2f2d779c022:@snowimba 这个 UUID 在所有 iOS 系统上表现都一样吗?
  • starfox寒流:看着好累,终于看完了
  • hellenten:楼主文章写的很棒,已经关注你的账号
  • 这个优秀瓜:我有几个问题想请教一下。

    第一个关于刷单的问题:
    有人恶意使用同一个receipt-data(收据)重复刷单,这种情况如何处理比较好。

    第二个是关于receipt的问题,以下的receipt都是针对消耗型内购:
    1、receipt是在交易finish后就在手机沙盒中清除了吗?
    2、向苹果服务器发送receipt进行订单验证时,返回信息中(in-app字段)包含了很多笔订单的数据,为什么包含这么多笔?
    3、手机沙盒中的receipt会在什么情况下改变?
    这个优秀瓜:@醉卧沙场yzy 之前遇到过一个receipt-data被重复发送过来进行验证,而且还是可以验证通过的。这种情况我让服务端对发过来的receipt-data的订单号做了记录,接收到验证过的receipt-data则不再验证。并在客户端提示用户联系客服。至于为什么要提示他联系客服,是因为有可能因为网络环境用户确实没有收到验证结果
    025c838455c0:请问这个要怎么处理呢
    025c838455c0:你好,关于刷单的问题你有研究过吗,我们现在遇到了伪造的 receipt-data,发到服务端校验是可以通过的
  • Young__Li:正要做IAP, 楼主的文章很到位,很用心,学习了。 感谢
  • 85fc5d7de887:applicationUserName我这边用起来还是挺靠谱的,ios9-ios11情况下没有发现丢失的问题。
    另外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和区服,因为这个值对于同一个用户是永远不会变的。
    Persistent丧心病狂:这个值是苹果检验不正常的交易的一个值,需要用苹果提供的加密方式加密后赋值,我们这里存放的是用户的id
  • ReevesGoo:您好,请教几个问题,最近被IAP整的头大。问题1:为什么我的收据凭证里只有最后一笔交易的记录,我有好几个交易都没有验证完毕呢,但取的最后的收据信息receipt通过苹果服务器验证返回的只有最后一条交易的信息,之前的交易收据貌似丢了。问题2:我验证完成需要finish的时候,[[SKPaymentQueue defaultQueue] transactions]一直返回为空,没有一次是有值的,困惑不解。求教
  • Sakura_Yin:这篇文章挺好的, 就是在forin循环的时候不要移除forin对象里的数组元素, 会报错
    e2f2d779c022:@Sakura_Yin 好的,谢谢提醒,明天改一下。
    Sakura_Yin:@NewPan BLWalletKeyChainStore.m中, - (void)bl_savePaymentTransactionModels:(NSArray<BLPaymentTransactionModel *> *)models
    forUser:(nonnull NSString *)userid 这个方法, 遍历 modelsExisted 的时候, 进行了[modelsExisted removeObject:modelExisted], 这个不行, 最好换成enumerateObjectsUsingBlock, 这个可以在遍历的时候移除元素
    e2f2d779c022:哪里?
  • 春泥Fu::joy: 之前没接触过IAP,一下子看3篇,到后面还是有点晕
    春泥Fu:@NewPan :relaxed: 嗯嗯,这两天看了你的源码跟文章,感觉学到了很多,考虑得也比较全面,因为现在IAP这块要重新梳理,发现之前的方式有很多漏洞,谢谢了!
    e2f2d779c022:@春泥Fu 再晕一遍。
  • 吃糖侠:不绑定不行么 直接拿苹果给的transactionID做订单号
    我认为oid的作用 就两个 一个是对应到商品 一个是对应到人物关系
    这两个作用 transactionID也是可以直接做到啊
    因为你这个处理逻辑是后创建订单
    那干嘛还要创建这个订单呢 直接把凭证给服务端 让服务端自己做订单创建和绑定不就好了
  • 小bug啦:applicationUsername为nil的情况怎么复现呀,我测试的时候,即使卸载,我看applicationUsername还是有数据呀
  • 西木柚子:写的很棒,博主棒棒哒:smiley:
    e2f2d779c022:@西木柚子 谢谢支持.
  • 377841418262:楼主,我们现在遇到一个问题,是想把自动续费的订阅式内购,跟自己的订单系统结合起来,之前参考SO的文章,貌似没有一个能够比较好的解决方法。核心问题就是,自动续费是否成功这个没有办法获取,也没有办法跟订单绑定在一起。不知道楼主有没有在这方面有研究?
    e2f2d779c022:不好意思,我没做过订阅式,估计也有一堆坑,可能你只能自己去趟了。可以的话把趟坑的过程记录分享一下,其他同行就不用趟一样的坑了。
  • 辣条包子:线上情况复杂,丢单确实没法完全避免。
    我目前做的处理一个是IAP时只允许串行交易,用loading动画过渡,完成一笔交易就finish,避免未完成当前交易,导致无法进行下一次交易,苹果的弹窗提示会让用户产生很不好的感受;
    再就是持久化的时候只存入到了沙盒里,一方面是用Keychain处理逻辑会增加较多,卸载app基本是用户的放弃行为。一方面是支付是一种强意识的行为,用户选择购买道具还是会比较慎重对待,极端的支付中卸载app的情况不做考虑,客户端对断网的情况做好数据处理就可以接受。applicationUsername这个确实挺坑的,不过还是凑合用了,正常情况毕竟是大多数不是嘛...:joy: :joy:
    其实做了这些处理也都是尽量降低丢单率,人工客服处理还是必不可少,苹果爸爸这些年改变了那么多功能,IAP这一块还真是没怎么变...
    e2f2d779c022:@辣条包子 我觉得用户行为分析可以和支付流程相互脱离,所以我们选择了这样的实现。除非你们在统计生成订单的成功率,你觉得呢?
    e2f2d779c022:@辣条包子 你的评论很棒。使用 keychain 也不会复杂很多。IAP 真的是万年不变坑。
    辣条包子:楼主的交易后生成订单的方式,确实有点打破惯性思维,不过个人不太认可,功能上可能会如楼主所说有很大便利,但数据统计这一部分会重点分析用户的支付行为,只有在生成订单后才能把这些打点上报和订单进行绑定。不过这些也都是看产品的具体需求了:flushed:
  • Callmewenxi:近期也在折腾IAP的问题,无数坑, applicationUsername为nil的情况这个是最坑的,支付流程和你们的一样,先向自己的服务器创建订单,然后再发起支付.
    有一个场景,支付成功时苹果回调的applicationUsername一定为nil的:
    只要在点击购买后立马断网,这时候扣费请求其实已经发出,最后也扣费成功了,只是用户断网了无法收到苹果回调而已,如果用户重启APP(恢复网络后进入后台再回到前台也会收到苹果回调支付成功,原因未明),就会收到苹果回调这一笔订单,但是applicationUsername一定为nil.
    开始也有考虑你们所说的"粗放式的验证",后来觉得这样处理不太好,然后采用这样的一个方案:在接收到苹果回调时,如果发现applicationUsername为nil的话,就将这个订单放到一个待组装订单号的队列中,根据商品id重新向服务器生成订单,不过这样也一样会有充错单的可能性

    还有就是在持久化时,并没有使用Keychain,我是直接写在沙盒的,只要没有finish掉苹果的订单,就算卸载App,苹果会一直给你回调那个订单的,重新写入沙盒就可以了.
    e2f2d779c022:@Callmewenxi 并没有说完全避免了错单, 第三篇文章也有分析我们采用的方案的缺陷, 而且我们测试也测出来了, 但是都是非常极端的情况下导致的.
    Callmewenxi:@NewPan 你们现在采用的流程也不能保证不出现错单问题,有些极端情况还是回错单的,例如:用户点击购买后断网了,没有收到苹果订单回调,然后卸载App,重装App后,收到苹果回调,这个时候的applicationUsername为空,无法知道这个订单是谁的:joy:
    e2f2d779c022:你的评论很有价值, applicationUsername == nil 这个是最大的坑. 我们后来采用的验证流程就能避免充错单的问题, 持久化到 keychain 有综合考虑换设备的问题. 谢谢支持.
  • Benyi_Peng:退出登录的时候,可以取消transaction的吧。
    之前做过内购,依稀记得有个cancelTransaction的API。
    只要退出登录的时候,把原来账号的交易全取消掉,最后的方案就很不错了。
    e2f2d779c022:@Benyi_Peng 已经找过文档,没有看到有你说描述的 api,再一个,产品是不会同意取消用户的交易的。

本文标题:[iOS]贝聊 IAP 实战之订单绑定

本文链接:https://www.haomeiwen.com/subject/krtgixtx.html