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

7,670

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

注意:文章中讨论的 IAP 是指使用苹果内购购买消耗性的项目。

这次为大家带来我司 IAP 的实现过程详解,鉴于支付功能的重要性以及复杂性,文章会很长,而且支付验证的细节也关系重大,所以这个主题会包含三篇。

第一篇:[iOS]贝聊 IAP 实战之满地是坑,这一篇是支付基础知识的讲解,主要会详细介绍 IAP,同时也会对比支付宝和微信支付,从而引出 IAP 的坑和注意点。

第二篇:[iOS]贝聊 IAP 实战之见坑填坑,这一篇是高潮性的一篇,主要针对第一篇文章中分析出的 IAP 的问题进行具体解决。

第三篇:[iOS]贝聊 IAP 实战之订单绑定,这一篇是关键性的一篇,主要讲述作者探索将自己服务器生成的订单号绑定到 IAP 上的过程。

不用担心,我从来不会只讲原理不留源码,我已经将我司的源码整理出来,你使用时只需要拽到工程中就可以了,下面开始我们的内容 。

源码在这里。

上两篇文章已经针对 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 账户中,如下图所示。

所以我们在用户退出登录的时候需要去检查他是否有未完成交易,如果有就要给个警告。但是还是没办法彻底解决掉这个问题,但是考虑到这个结果是用户的行为导致的,而且出现这个问题的几率不大,暂时就这样处理。

如果你确实有这方面的担心,那就应该采用上面说的粗放式的验证,粗放式的验证是不存在这个问题的。