iOS—处理苹果内购(IAP)掉单的坑

作者: 笑谈红尘乱离人 | 来源:发表于2017-02-18 12:54 被阅读5529次

她现在已经一岁多了,亲手把她从小带到大的感觉,真是酸甜苦辣五味俱全啊。如果按照人类的年龄来计算,她应该相当于二十四五岁,正直风华正茂的年龄。有时候恨她,没错,她比较特别,很会赚钱,很能干,略污很可爱。

扯远了,说正事吧。苹果手机端软件免费+应用内购买的模式已经被证明是最有效的盈利模式,所以实现内购功能可能是很多开发者必做的工作和必备的技能。相信很多接入了苹果应用内支付IAP(In-App Purchase)功能的应用都可能遇到过「掉单」的问题,「掉单」的意思就是用户扣款了但是平台没有给 TA 充值虚拟货币,这会让用户很苦恼愤,严重影响产品的体验,甚至公司的信誉度,我们偶尔会接到用户的投诉电话。凡是涉及到钱的事,都是很敏感的,所以需要格外的谨慎,掉单不可怕,可怕的是不去处理它。

我司产品(社交软件)在这个问题上困扰已久,一直没有花时间去处理,也腾不出更多的时间,都是在迭代新版本。趁着公司开发管理制度上的改变,每一个新版本都会留出时间去解决一些技术上的问题,为的就是减少产品的 bug ,优化提升性能,让产品更加完美。问题既然存在很久了,现在回头看看以前写的代码逻辑,有种很不可思议的惊讶:这 TMD 是我写的代码?绝逼不可能……这逻辑明显问题很多啊……肯定是哪个二逼写的……,一脸懵逼。好吧,其实都是自己挖过的坑,现在是时候填了。今天,不,就现在,咱们来聊聊如何解决IAP「掉单」的事儿。

首先来整理一下IAP支付的过程:

  1. 调用IAP接口发起支付
  2. 支付成功,获取App Receipt票据,调用充值接口验证
  3. 验证通过,给用户充值虚拟货币并回调给 App

这里需要了解一下IAP支付的机制,每次支付行为或每笔交易被认为是一个SKPaymentTransation,只有当SKPaymentTransationfinishTransaction:,这次支付(交易)行为才算是正常结束了。即使这次支付途中被中断,其实也并没有丢失。假设支付没有完成 App 就退出了(比如崩溃),那么当下次 App 重启之后,只要设置了监听addTransactionObserver:,之前被中断的支付就会接着进行。

再来看看「掉单」可能会出现在哪些环节:

  • 第1步,这个过程中 App 进程因为某种原因被 kill 了,其实支付行为还在系统后台进行着,苹果自己做的,很有可能扣款成功。但是这时候没法为用户充值虚拟货币。
  • 第2步,App 端与自己服务器端通信失败;自己服务器端与 AppStore 服务器之间的通信失败。

在了解了IAP支付机制之后,上面两种情况就有方法去解决了。

针对第一种情况,可以在 App 一启动就设置监听,如果有未完成的支付,则会回调- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions;这个方法,在这个方法里调用接口充值。
至于第二种情况,App 端需要做接口重试,设置一个重试的逻辑。

下面来描述一下我的具体做法。

我使用了RMStore,稍作了一些修改。新建一个数据表(字段有:transactionIdentifier、productIdentifier、uid、transactionDate(支付时间)、rechargeDate(充值到账时间)、state(「已支付」和「已充值」)),存每一笔交易。

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions;这个方法里,首先根据SKPaymentTransactiontransactionIdentifier去本地数据表中判断这条交易:

  • 如果没有,则在数据表中创建一条交易,状态为「已支付」,接着去调用充值接口;
  • 如果有这条交易,并且状态为「已支付」,则不需要创建该条交易,只需要去调用充值接口;
  • 如果有这条交易,并且状态为「已充值」(虚拟货币到了用户账上),则不需要创建,也不需要去充值,直接结束交易(finishTransaction:)。

充值接口的设置,App 端需要传这些参数:交易id、商品id、用户uid、票据,该接口在服务器端需要做一些操作:transactionIdentifier需要入库,和App Receipt对应起来,每次访问接口都要先做去重判断,如果是已经验证通过的transactionIdentifier,也返回成功。只要是访问成功,该接口都需要带上transactionIdentifier返回给 App 端,可能不止一个,因为一个App Receipt票据验证成功后返回的交易列表可能有多个,如下图:

App Receipt Verify Reponse.jpg
在充值成功的回调里需要找出每一个transactionIdentifier,根据这个transactionIdentifier去遍历SKPaymentQueuetransactions,找出对应的SKPaymentTransaction并结束交易finishTransaction:。还需要根据transactionIdentifier去更新数据表中这条交易的状态,改为「已充值」。然后继续遍历SKPaymentQueuetransactions,看有没有未完成的交易需要充值,如果有,都要去判断在本地数据表中是否已有记录。

写到这里,感觉应该可以解决大部分掉单的问题吧,花了几天的时间翻阅了不少资料,希望这个方案是可靠的。等待产品上线看效果~

2017-04-07 效果还不错,几乎没掉单了

参考文章:
苹果IAP开发中的那些坑和掉单问题
iOS Apple内购及掉单问题
iOS内购你看我就够了(一)
真·iOS内购的完整流程
关于AppStore IAP的新旧Receipt
iOS内购你看我就够了(埋坑篇)
2016年最新版App内购买详细指南
iOS内购丢单处理及实现

相关文章

网友评论

  • fb69e982796d:请问如果使用本地数据库存储数据,若用户把app卸载掉,那用户怎么处理第二种情况的掉单呢?
  • 白天不懂夜的黑_8ca8:希望作者能给一个demo,让我们更好的理解
  • 徐小鸿同学:你好请问, [[SKPaymentQueue defaultQueue]addTransactionObserver:self]; 这个方法你在appdelegate 那里添加了观察者,我自己封装的方法类里面也添加 [[SKPaymentQueue defaultQueue]addTransactionObserver:self];在调用充值的功能的时候 发现两边的观察者也会跑 - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
    这个回调 你这边是否也会出现这样的问题
    笑谈红尘乱离人:@徐小鸿同学 一样的啊,都是单例,你只要保证只有一个观察就行
    徐小鸿同学:@笑谈红尘乱离人 你是基于RMStore 中实现 一个观察者?因为我是自己写的方法并没有使用RMStore
    笑谈红尘乱离人:@徐小鸿同学 全局只需要一个地方添加[[SKPaymentQueue defaultQueue]addTransactionObserver:self];即可
  • 暖风惜人:急急急急
    PINKAPINKA:作者就不能回复一下吗
  • 暖风惜人:你好。我们现在遇到内购的问题就是,我们现在内购的时候,点击任意一个商品,得到返回的recepit总是有一个之前的订单信息,而且内购成功得到的东西一直是那个之前的单子的,我们也每次在- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
    进行finish了,还是一直会有那个原来的订单信息
    笑谈红尘乱离人:如果调用了finishTransaction还出现这个订单,那就是苹果的坑了。你确定还会出现么,transactionIdentifier是否一样
  • BetterMan_ad93:你好,慕名而来。:blush: 请教一个问题,我在货品信息里面添加了applicationUsername,但是有几率 回调的时候applicationUsername为空,请问一下 大家有没有遇到这个问题。
    笑谈红尘乱离人:你如果想要以applicationUsername来作为一个标识,恭喜你采坑了,这个字段没啥卵用,就是你碰到的偶尔为nil。
  • Mapple_9649:请教一下:如果在服务器验证成功才finishTransaction话,我们遇到个问题,某些异常情况导致服务端没有验证成功,那么就不会finishTransaction,那再次购买同样商品(比如再充值同一个金额),会出现“您已经购买此App内购项目 此项目将免费恢复”的情况,按照你的方案,我感觉应该也会有,你是否遇到呢?
    笑谈红尘乱离人:@Mapple_9649 遇到过的,碰到这种情况就需要去把之前未完成的先完成,好像可以通过SKPaymentDownload去获得
  • OC笔记:楼主,你好,按照你的逻辑,在监听函数updatedTransactions中,根据苹果返回的结果来判断是否支付成功。如果updatedTransactions返回失败了,你怎么处理呢,还会注销掉此次交易(finishTransaction)吗?
    笑谈红尘乱离人:嗯,直接finishTransaction了
  • 4a070dd92c5a:请问有遇到in_app 为空的情况吗? 是怎么处理的
    PINKAPINKA:我也遇到了 然后并不知道怎么解决
    4a070dd92c5a:@笑谈红尘乱离人 问题是用户确实的付款 有扣款截图,每周总有那个2个,我们是做直播的 很影响用户体验,用户充值金额都不小 真蛋疼。
    笑谈红尘乱离人:@Rafe_92d5 暂时没遇到,如果为空的话,应该可以不处理的,不是有效的凭证。
  • 昵称无法描述本人身份:请教一个问题:『一个App Receipt票据验证成功后返回的交易列表可能有多,客户端接收处理完成的交易,根据这个transactionIdentifier去遍历SKPaymentQueue的transactions,找出对应的SKPaymentTransaction并结束交易finishTransaction。』这个过程客户端会将交易完成的订单再告诉苹果吗?
    我目前遇到的情况是:服务端的处理机制是第一次处理一个transaction将结果返回给客户端,发现苹果还会继续发这笔交易的 receipt ,这时服务端不会再去处理,也不会再返回给客户端交易完成,发现苹果还在继续发这笔单子。比较疑问的是当服务端告诉客户端这笔交易完成时,客户端还会去告诉苹果吗?是不是客户端没有成功告诉苹果,苹果会不断发 receipt ?
    昵称无法描述本人身份:@笑谈红尘乱离人 看来只有 finishTransaction 后才不会继续发这笔单子,有可能是 客户端 自己处理的这个机制。目前发现了一个问题是走正常一个支付流程后苹果却没有扣钱,不清楚是不是没有 finishTransaction 的原因?而在这笔订单苹果一直在发 receipt 。
    笑谈红尘乱离人:@昵称无法描述本人身份 那个过程需要自己写代码去把已支付的订单finish掉。『服务端的处理机制是第一次处理一个transaction将结果返回给客户端,发现苹果还会继续发这笔交易的 receipt 』,这说明你没有结束交易,也就是没调用finishTransaction方法,只有调用了finishTransaction方法才不会继续发这笔单子。

    你的疑问我不是很明白,“当服务端告诉客户端这笔交易完成时”,这个只是网络请求的回调吧,就是你发起请求,你们的服务器回调给客户端交易完成了,客户端并不会什么操作,更不会告诉苹果;正确的应该是,你们服务端告诉客户端交易完成,这时候你去遍历SKPaymentQueue的transactions,找到相应的SKPaymentTransaction去执行finishTransaction操作即可,之后苹果就不会发receipt了。
  • zhangyin:如果防止掉单的验证都必须要客户端上的参与,那么如果客户端手机的网络不好,(连接不上Appstore的接口),那么掉单的问题就还是无法防止,有没有从server端出发来解决这个问题的途径?
    笑谈红尘乱离人:你说的连接不上AppStore的接口,如果在支付成功之前,那没关系。如果在支付成功之后出现了闪退,之后重启无论怎么样都无法连接AppStore,从而无法恢复支付凭证,那么可以在支付成功之后把这一次的支付信息(时间戳、uid、金额等,只要能辨别即可)根据某种机制告知给服务端,仅告知此用户已支付即可。如果长时间没给此用户充值虚拟币,则可以进行人工干预。

    掉单这个问题没法绝对防止,但是可以做到让掉单出现的概率极小极小。
    笑谈红尘乱离人:出现掉单的情况,肯定是用户已经在客户端里支付了。而要去解决掉单,则必须要支付凭证,没有支付凭证的话,服务端无法辨别是否是正常的支付。而支付凭证这个东西,又是只有在客户端里生成的,没有其他途径。所以解决掉单必须要有客户端的参与,离开一个端都不行。
  • Hengry:后台向AppStore验证receipt成功后,返回结果中出现in_app有多个订单信息,这怎么处理呢
    PINKAPINKA:支付之后 客户端也要去验证是否支付成功吗? 我们只是服务器去验证 有多个订单信息的时候她就说服务器不知道应该处理那个订单 所以给支付失败了 正确的应该是怎么样子的啊?help
    Hengry:@笑谈红尘乱离人 谢谢,明白。vip购买可以使用应用内购消耗类型,应该可以通过苹果审核吧!我的vip是分月购买的
    笑谈红尘乱离人:@DevHank 一个票据里返回多个订单信息,有两种情况,第一:之前有未完成交易的订单和你当前完成的订单合并为一个;第二:之前有多个订单未完成交易;这里说的完成交易就是调用finishTransaxtion:。只需要遍历一下,然后每个都finish就行。
  • imimbluer:请教一下,你的这个方案,上线后效果如何?后没有遇到其他什么问题
    白天不懂夜的黑_8ca8:要是能有demo就更好了
    笑谈红尘乱离人:@imimbluer 很少遇到掉单了,效果还不错

本文标题:iOS—处理苹果内购(IAP)掉单的坑

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