前言
在苹果应用内购(后文以 IAP 代替)的开发中,踩过很多坑,所以写个文章分享一下经验教训,也吐槽一下苹果相当拙劣的接口
一些基本概念
- 商品类型,IAP 支持 4 种类型,本文只涉及以下三种
- CONSUMABLE:消耗品,比如游戏内的道具,游戏币也属于消耗品
- NON_RENEWING:非自动续费的订阅,比如会员资格,按月购买的,到期后再买一个月,如果不买,就不再是会员了,这就是非自动续费的订阅类商品
- AUTO_RENEWABLE:自动续费的订阅,还是以会员资格来举例,可以每次买一个月,到期后再买一个月;也可以开启自动订阅,这样每次会员将要到期时,苹果会自动发起续期交易,就不需要用户打开 App 来手工购买了,这就是自动续费的订阅
- receipt:收据,客户端完成购买后,会发送一个收据给服务端,该收据包含了交易的一些信息;如果是自动续费的订阅,在自动续费完成后,也会产生收据
- transaction:交易,这对应着一次购买,或者是一次自动续费
苹果的接口
最基本的就是收据的验证接口,服务端在拿到客户端提交的收据后,调用收据验证接口验证该收据,如果收据有效,会返回该收据关联的交易信息
这个接口的设计我觉得是有问题的,首先是返回了太多无意义的字段,其次是跟交易相关的两个字段 in_app 和 lastest_receipt_info,我觉得完全应该合并成一个
理论上来说,一个用户的收据,只应返回该用户的交易,但实际上我遇到过收据校验成功,但是不返回任何交易,也遇到过返回了其他用户(甚至不止一个其他用户)的交易
实际上问题还不止这些,这涉及到我们对苹果的定位,我们希望它仅仅是个支付工具,就像支付宝一样,但苹果自己的定位却不是一个支付工具,而是一个订单系统,我想这也是 IAP 对接很容易掉坑里的主要原因
此外,苹果还有服务端的通知,这也是本文将会涉及的内容
IAP 交易和验证
基本流程
- 客户端从 AppStore 获取商品信息
- 用户选择商品并完成购买
- 客户端从 AppStore 获得一个收据
- 客户端把收据发送到服务端
- 服务端调用 AppStore 的收据验证接口进行验证
- 验证通过,服务端根据收据里的交易信息,进行订单的发货处理
- 服务端向客户端返回收据处理结果
- 客户端调用 AppStore 的接口通知收据已处理
这是一次购买的基本处理流程,如果是自动续费成功,客户端会从 AppStore 获得一个收据,直接将该收据提交给服务端进行验证即可
这里建议客户端同时提交收据和交易 id,即 transaction_id 到服务端,这样可以简化服务端处理逻辑
如果客户端未提交交易 id,服务端需要自行判断需要处理的是哪一笔交易,我建议如下处理
- 将 in_app 和 lastest_receipt_info 里的交易合并去重,去重的逻辑是相同的 product_id+original_transaction_id+transaction_id 则视为同一笔交易
- 按商品类型分组
- 取每一组的购买时间最近的一笔交易
按苹果的文档,我们应该对所有交易进行处理,但实际业务中遇到过收据关联了其他用户的交易的情况,所以我建议还是找到最新的交易进行处理
要注意的是,不要认为 in_app 和 lastest_receipt_info 里的交易是有序的,应该按 purchase_date_ms 字段进行排序
订阅类商品
基本上 IAP 的坑都是订阅类商品的坑,较普遍的情况是自动续费到哪个用户账户的问题
从前面的流程里我们可以发现,只有交易完成,服务器才能接收到客户端发送的收据,这时,收据是否一定归属于当前提交收据的用户呢?答案是不一定
比如以下场景
- 用户 A 购买了自动续费类订阅,比如视频网站的按月付费的 Vip
- 在 Vip 到期前 1 天,苹果自动从该用户绑定的信用卡扣费成功
- A 用户这段时间一直未登陆帐号
- A 的 Vip 自动续费成功后,其使用同一部手机,登陆了 B 帐号
- 此时,App 端获取到 AppStore 的收据并以 B 的身份提交收据到服务端,服务端到底该给 A 增加 Vip 有效期还是给 B 增加?
又比如
- A 购买了自动续费的月付 Vip
- 第二个月 Vip 还未到期时,A 取消了自动续费
- 该用户在同一部手机登录了 B 帐号,并用 B 帐号购买了自动续费的 季付 Vip
- 在 该 季付的 Vip 到期前一天,苹果扣费成功
- 此时该用户登录了 A 帐号,客户端获取了季付 Vip 成功的收据并提交服务端
可见,这里有一个关键信息是服务端不具备的,就是一笔已发生的交易,到底是归属那个帐号?
我想,这也是 IAP 容易掉坑的主要原因
苹果的处理逻辑
苹果并没有把自己当做一个第三方支付服务提供商,它实际上既做了支付服务,也实现了订单服务,我们可以在苹果查询到自己购买过的所有 App,以及 App 内交易
本来这样也没有问题,但问题是苹果的交易是基于 Apple Id 来的,同一个 Apple Id,在同一个应用就是一个用户,这样就有问题了
大多数 App,其实都允许同一个 Apple Id 注册多个帐号,这样在苹果的订单系统看来是同一个用户的订单,在我们的 App 里其实是不同用户的订单,而苹果的订单信息里是不包含 App 内帐号信息的
如果是购买消耗品类型商品,很少能触发收据被其他登录帐号提交的场景;但是在自动续费类商品上就很容易发生,如何确定交易的归属,其实是个复杂的问题
我的解决方案:根据交易的 original_transaction_id 找到该收据对应的用户账号,如果该账号还在自动续费中,则该交易归属此用户;如果找不到或者不在自动续费中,则交易属于当前提交收据的用户
苹果后台通知
在接入苹果后台通知前,自动续费类商品续期成功,我们有两种方式来触发服务端的处理
- 客户端从 AppStore 获得交易成功的收据,并发送到服务端
- 服务端定时轮询收据验证接口,将快要到期的收据拿去验证
这样的缺点就是不够及时,影响用户体验
此外,如果用户退款,是不能依赖服务端轮询的,此时苹果后台通知可以说是唯一的触发手段。
那么,如何判断交易已退款?可以根据 cancellation_date_ms 字段是否有值来判断