iOS内购这块的开发一直比较麻烦,除了各种购买选项的问题,最恶心的问题就是丢单问题。
丢单就是iOS内购过程中付了钱,但是App没有发货的问题。要解决丢单问题,先要梳理一下整个购买的过程:
- 调用服务器接口创建订单
- 调用内购的api完成购买
- 获取receipt、transactionIdentifier发送给服务器
- 本地服务器拿到receipt向苹果发起验证,并回调结果给App
以上就是整个购买的过程,现在我们根据每一步分析下可能出问题的点
1. 调用服务器接口创建订单
这一步的操作就是让服务器创建当前购买商品的订单,返回结果失败或者成功,这里基本不会出问题,成功就继续接下来的流程,失败的话,客户端返回失败的提醒就行。
2. 调用内购的api完成购买
绝大多数的问题都出在这里,在这里说一下我遇见的坑
- 客户在付完款之后,直接杀死了app。
- 在支付过程中,若当前账户里的钱不够,且没有绑定银行卡、微信或者支付宝,会触发一个绑定机制,但是用户在点击去绑定时候,
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
,会返回一个失败的transaction,当用户完成支付,又会返回一个成功的transaction。由于我们之前使用RMStore这个第三方插件,他在处理回调的时候有这样一个机制,每次发起购买RMStore都会把回调的block存储到RMAddPaymentParameters这个对象里面,当第一次错误回调返回时,会把RMAddPaymentParameters里面的block取出来并执行,执行之后,持有这个对象的数组会把这个对象移除,所以当第二次成功的回调返回时,再想去执行block,但是持有这个block的对象已经被移除了,导致了非常多的丢单。
对于第一个问题,内购的api有一个监听机制,在每次app刚启动的时候调用[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
方法,- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
,会回调所有未执行[[SKPaymentQueue defaultQueue] finishTransaction:tran];
的transaction,这个时候再去和服务器进行订单的验证。这里还有一个很麻烦的问题,在上个流程中,调用服务器创建了订单,会返回一个订单号,验证的时候一般都是把订单号,以及receipt,transactionid一起发送给服务器,但是这里是拿不到订单号的。针对这个问题,网上有人用applicationUsername
这个字段去存入订单号,但是也有一些人在使用这个字段的时候出现了Bug(App杀死后,获取这个字段的值为空,参见下面的引用),为了保险,这里还是不用字段。我的解决方案是在用户创建完订单之后,存储当前的订单信息,用户token等等,当购买完之后,再去取这个订单信息,完成验证,删除订单,这个方案的核心是永远只存储一个订单,若在购买的时候有未完成的订单,必须先验证之前的订单,验证完之后,再去发起新的购买,这样就能规避多个transaction不能匹配订单号的问题;
对于第二个问题,这个问题好解决,不用这个第三方就行,自己造个轮子,基本的购买逻辑很简单,没必要用这个插件。当- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
返回的transaction的state为failure时候,finish这个transaction,state为purchsed的时候,去验证。
这里还有一个问题,finishTransaction:的调用时机,一定要在与服务器完成验证之后,再去调用这个方法,因为在与服务器的验证的过程中,也是会出现错误的,如果再购买成功回调之后就调用这个方法,验证错误的订单也被finish了,下次去取transaction的时候,就取不到了,这样就造成了丢单问题。
3. 获取receipt、transactionIdentifier发送给服务器
这里的逻辑基本就是自己的了,跟苹果内购关系不大,把订单、receipt等信息发给服务器就行,唯一需要注意的问题就是网络错误,这个时候我会做一个轮循,在发生错误的时候,轮循几次,超过这个次数,认为验证失败,等到用户购买下一个商品、或者app重新启动,会重新验证这个订单(当然,这个情况是极少的!为了严谨,做了以上处理)
4. 本地服务器拿到receipt向苹果发起验证,并回调结果给App
终于到最后一步了,这里有一个地方需要注意一下,就是本地服务器与苹果服务器验证的方式,之前我们公司出了个问题,在调用/verifyReceip
这个接口去验证,苹果会返回一个数据status,它们认为status等于0就是完成购买了,但是这个字段的含义并非如此
For iOS 7 style app receipts, the status code is reflects the status of the app receipt as a whole. For example, if you send a valid app receipt that contains an expired subscription, the response is 0 because the receipt as a whole is valid.
它只是反映这个receipt是不是完整的,并不能证明这次购买完成。正确的做法应该是服务器拿到此次的transaction和receipt之后,解析receipt,拿当前的transaction和receipt里的transaction数组去比对,若数组中有这个transaction对应的transactionid,则认为购买成功。
苹果服务器返回的错误码中,我们只需留意21005即可,它代表验证服务器当前不可用(当然这种情况是级级级小的!为了严谨),处理的方式和网络错误的处理方式是一样的。