苹果IAP开发中的那些坑和掉单问题相信很多iOS App的开发者,特别是⼿游开发者,都接触过苹果⽀付IAP(In-App Purchase)。相信使⽤了IAP的App,都经历过“掉单”问题。
什么是“掉单”呢?简⾔之就是⽤户付款买⾦币,钱扣了,⾦币却没到账。
掉单⼀旦发⽣,⽤户通常会很愤怒地来客服。然后客服只能开发⼈员把⾦币给⽤户⼿动加上。
显然,伤害⽤户的体验,特别是伤害付费⽤户的体验,是⼀件相当糟糕的事情。
我们在微爱App的开发过程中,IAP⽀付的掉单问题也困扰了我们很久。直到去年第四季度的⼀次优化,才算是彻底解决了掉单问题。
掉单是如何产⽣的呢?这需要从IAP⽀付的技术流程说起。
IAP同国内的⽀付宝、⽀付都是⽤于⽀付的平台接⼝,但它们在⽀付的技术流程上却有着本质的不同。
⽀付宝和⽀付在⽀付流程上⾮常相似(顺便说⼀下,⽀付早期的API设计甚⾄跟⽀付宝API在参数命名上都保持⼀致),如果忽略掉它们之间的细微差别,那么它们的⽀付流程⼤体上可以按如下描述:
第三⽅App在前端发起⽀付请求;
前端跳转到⽀付宝()客户端,⽤户完成⽀付;
前端跳回第三⽅App;
在第3步的同时⽀付宝()服务器回调第三⽅App的服务器,在这个回调中第三⽅App服务器完成订单验证和发货操作(给⽤户加⾦币)。
注:⽬前的⽀付在最开始还多⼀步向平台获取prepayid的过程,不过这不是我们讨论的重点,我们暂时忽略它。
⽽IAP的⽀付流程完全不同:
App调⽤IAP接⼝在前端发起⽀付请求,⽤户在前端完成⽀付;
⽤户完成后在前端回调App;
如果App是⼀个纯客户端应⽤(没有服务器,⽐如单机⼿游),那么App取到票据(receipt),然后直接在前端进⾏本地验证。如果验证成功则⽴即在前端完成发货。⽀付流程到此结束。
儿童节朋友圈
如果App有服务器,那么它需要将票据(receipt)传给App服务器。
App服务器将receipt发给苹果App Store服务器去验证。如果验证成功,App服务器完成发货。
那么在上述流程中,可能发⽣掉单的环节是哪些呢?
在⽀付宝和⽀付中,如果第4步回调发⽣错误(⽐如⽹络超时、App服务器处理异常),那么就会发⽣掉单。在前3步,⽤户已被扣款,但由于回调错误⽽没有给⽤户发货。在这个流程中,防⽌掉单的措施主要是⽀付宝和⽀付服务器在检测到回调发⽣失败后,会再次重试回调。通常重试间隔会越来越⼤,并设定⼀个最⼤的重试次数。虽然在连续失败达到最⼤重试次数之后,最终⽀付宝和⽀付服务器会放弃回调重试(发⽣掉单),但这种概率极⼩。
⽽在IAP的⽀付流程中,在⽤户扣款之后,在第4步和第5步,都很有可能发⽣错误(⽐如⽹络超时、App崩溃、App服务器处理异常、App Store服务器异常),尤其是⽹络错误,从⽽发⽣掉单。第4步主要是客户端到App服务器之间的⽹络错误,由于移动客户端经常处于弱⽹环境,所以这种错误就很容易出现。第5步主要是App服务器到App Store服务器之间的⽹络错误,由于国内的服务器与App Store服务器之间⽹络延迟通常很⾼,这种错误也⽐较容易出现。另外,App Store服务器还偶尔会返回503,这也是造成发货失败的原因之⼀。
钟楚红个人资料
在IAP中能够防⽌掉单问题的⽅式,是利⽤事务机制。IAP中的每次⽀付⾏为被抽象成⼀个事务(SKPaymentTransaction),只有事务被正常结束(finishTransaction:)该次⽀付⾏为才算完成。即使⼀次⽀付中途被中断,这次事务也并没有丢失。假设⽀付没有完成App就退出了(⽐如突然崩溃了),那么当下次App重启之后(调⽤了addTransactionObserver:),之前被中断的事务会接着进⾏。
但是,IAP提供的这种基本的事务机制,对于⽀付流程的完整性只能提供⼀个⽐较弱的保证。它的缺点有以下⼏个:
如果⽤户扣款成功后由于⽹络原因⽽导致App客户端同App服务器通信失败(App未⽴即退出),那么当前⽀付就没能正常发货。⽽IAP 的事务机制⼀般要等到App退出后下次启动才能恢复(IAP要在addTransactionObserver:被调⽤后恢复上次事务,⽽addTransactionObserver:在App⽣命周期内⼀般只调⽤⼀次)。这样,在⽤户扣款成功后直到App下次启动,都没有机会让掉单的订单得到恢复。这段时间可能会⽐较长。
如果错误发⽣在第5步App服务器和App Store服务器之间,那么情况同上,也要等到下次App启动才有可能恢复事务,得到重试发货的机会。
如果获取票据(receipt)使⽤的是iOS 7.0之前的接⼝(通过SKPaymentTransaction的transactionRec
eipt属性,该属性已经从iOS 7.0开始过期),那么会产⽣更多问题。⽐如连续产⽣了多个未完成的订单事务,那么有可能只能恢复最后⼀个。
在任何⼯程性的系统中,失败和错误都不可避免。好的技术⽅案不仅能在正常的情况下保证逻辑正确,还应该能保证在系统发⽣错误的时候让系统有机会从错误状态中恢复。在⽀付宝和⽀付中,错误恢复主要由平台服务器负责(重试回调),App开发者承担的任务较少;⽽在IAP中,错误恢复很⼤程度上要依赖App开发者来完成。App开发者要确保App客户端和App服务器之间有⼀个更强的通信通道。这样看来,IAP⽐⽀付宝或⽀付更容易发⽣掉单现象,也就不⾜为奇了。
为了应对IAP⽀付流程中的上述缺陷,我们在优化中考虑了如下的关键点:
⾃动重试的发货任务。
使⽤App Receipt来代替transactionReceipt。
发货任务的重启动不直接依赖IAP的事务机制。
下⾯分别详细介绍⼀下这⼏个点。
⾸先是⾃动重试的发货任务。正常情况下,在⽤户完成付款后(即事务状态变为SKPaymentTransacti
onStatePurchased时),发货任务被启动。发货任务⼀旦启动,将会不断重试,直到发货成功。因此,启动后的发货任务可能处于两种不同的状态:
等待服务器响应的状态。发货任务已经向服务器提交了发货请求,正在等待服务器响应。
等待重试的状态。由于上⼀次发货请求失败了,当前正在等待在⼀段时间之后重新提交发货请求。
另外,即使是发⽣连续多次⽀付⾏为,程序逻辑也要保证发货任务同时不会启动多次。考虑到这些因素,发货任务的启动逻辑可以按如下设计:
如果没有任务在执⾏,那么启动发货任务;
如果有发货任务在执⾏,那么看它当前处于什么状态:
如果有变化,则取消原来的发货请求,并⽴即发起⼀次新的请求;
如果⽆变化,则什么也不做,静等之前任务执⾏。
如果正在等待重试,那么取消重试任务,⽴即开始⼀次新的请求;
如果正在等待服务器响应,那么再看App Receipt有⽆新的变化:
只有当某次发货请求执⾏成功后,App客户端才调⽤finishTransaction:将发货任务结束掉,并不再重试;否则,发货任务就等待⼀段时间重新进⼊上述启动发货任务的逻辑。
在上述发货任务的实现逻辑中,涉及到异步编程以及如何取消⼀个异步任务。有关异步编程中需要注意的事项,请参见笔者的系列⽂章《iOS和Android开发中的异步处理》。
第2点是使⽤App Receipt来代替transactionReceipt。这也正是苹果官⽅所强烈建议的,并且从iOS 7.0开始transactionReceipt已经被置为过期接⼝。实际上,App Receipt本⾝并不仅仅⽤于IAP订单的验证,还⽤于App本⾝的验证。你可以利⽤App Receipt的客户端本地验证来确保⽤户只能使⽤从App Store下载到的你的App版本(实际上这也是苹果希望App开发者去做的)。如果你的App需要付费才能下载,那么这个检验就⾮常有意义。
当利⽤App Receipt来验证IAP订单时,我们需要验证的是在App Reciept中所包含的IAP receipt列表(in_app节点)。与iOS 7.0之前的⽅式相⽐,这种⽅式的明显区别是:它包含⼀个IAP receipt列表⽽不是仅仅⼀个IAP receipt。这使它本⾝带有某种程度的⾃动修复的特性。如果⽤户某次⽀付没有被正确完成也没有后续被成功恢复,那么当他在同⼀个⼿机设备上产⽣下⼀次⽀付⾏为时,App Receipt中就会包含前后两次⽀付的IAP receipt,这就能让上次失败的订单⼀并恢复。
第3点,发货任务的重启动不直接依赖IAP的事务机制。按照正常的IAP事务机制,如果⽤户已经付款
成功,但最终没有发货成功(finishTransaction:最终没有被调⽤),那么下次App启动后在SKPaymentQueue的addTransactionObserver:调⽤
后,paymentQueue:updatedTransactions:会⾃动被回调,从⽽使得之前未完成的事务得以继续。但是,这⼀机制是否⼀如既往地如苹果宣称的那样值得信赖,我个⼈是持怀疑态度的。在我们以前使⽤IAP的过程中,我们总是会碰到⼀些⽆法被IAP的事务机制恢复的情况。在iOS 平台提供的API中,总是存在⼀些令⼈不安的设计(实际上其它平台上也不乏这样的例⼦),这也算是其中之⼀。
我们采取的策略是,把⾃动重试的发货任务的执⾏状态在客户端持久化下来。当下次App启动时,我们可以依赖之前持久化的发货任务状态来重启发货任务,⽽不必依赖苹果的事务机制来重启任务。注意:我们之前描述的发货任务的启动逻辑已经可以确保发货任务同时不会启动多次。
需要注意,我们这样⼀种脱离IAP事务的设计,会影响我们最终对于结束事务时的处理。通常情况下,由
paymentQueue:updatedTransactions:回调所启动的发货任务,由回调接⼝已经传进来了需要处理的SKPaymentTransaction实例,这样在发货成功后打算结束事务时,我们便很⾃然能拿到需要结束的SKPaymentTransaction实例。
然⽽,在我们⾃⼰控制下启动的发货任务,在任务结束时我们只能拿到transactionIdentifier,没有现成的SKPaymentTransaction实例可以供我们传给finishTransaction:接⼝。但这算不上⼀个难题,我们可以遍历SKPaymentQueue的transactions列表,通过对⽐transactionIdentifier来到SKPaymentTransaction实例。
经过上述对IAP实现的优化,我们⼏乎再也没有碰到过毫⽆缘由的掉单现象。
在这篇⽂章最后,我再把IAP开发中值得开发者关⼼的其它⼀些问题,补充说明⼀下。
IAP接⼝的安全问题
Apple iOS in-app purchases hacked; everything is free.
虽然苹果官⽅宣称在iOS 6中解决了这个漏洞,但有些借此盈利的⼈⼿⾥应该还保留有旧的iOS版本,也许淘宝上那些可以打折为游戏充值的店铺今天仍然在利⽤这个漏洞(虽然没有直接证据,但很值得怀疑)。
作为iOS开发者,应该尽量做到:
使⽤最新的App Receipt验证⽅式,停⽤旧的transactionReceipt的验证⽅式。
使⽤服务器验证receipt⽽不是客户端本地验证。
App客户端和App服务器之间的通信通道要加密。
李晓杰朋友的酒SKProductsRequest接⼝的问题
提交IAP⽀付请求(SKPaymentQueue的addPayment:接⼝),需要传⼊⼀个SKPayment实例。这个实例可以从productIdentifier创建(SKPayment的类⽅法paymentWithProductIdentifier:),但是这个接⼝从iOS 5.0开始过期了。
童装品牌排名按苹果的建议,应该使⽤SKPayment的paymentWithProduct:⽅法来创建SKPayment实例。然⽽这个接⼝需要传⼊⼀个SKProduct实例。⽽要获取⼀个SKProduct实例,必须先使⽤SKProductsRequest向App Store查询产品信息。苹果建议的购买流程是,先使⽤SKProductsRequest查询到所有售卖物品的产品信息(以SKProduct实例来表达),然后再展⽰商店购买页⾯。
这个获取SKProduct实例的过程,苹果官⽅之所以如此设计,可能是为了确保App内的商店购买页⾯对于商品的展⽰与iTunesConnect 后台的配置保持完全⼀致。但是,SKProductsRequest查询的过程会增加好⼏秒的耗时(在国内经常在5秒以上)。这会导致商店购买页⾯本⾝显⽰出来⾮常慢。
因此,最终我们决定还是使⽤已经过期的SKPayment的paymentWithProductIdentifier:来创建⼀个S
KPayment实例。这样可以做到先将商店购买UI快速展⽰出来。
关于退款的订单
⽤户退款的订单有可能依然在App Receipt中出现,因此App服务器实现验证的时候需要能够识别出已经被退款的订单,不⾄于给退款的订单发货(甚⾄发两次货)。
春寒 梅尧臣被退款订单的唯⼀标识是:它带有⼀个cancellation_date字段。
验证请求的结果返回
iOS 7新的App Receipt在验证完毕后,App Store服务器返回的验证结果(status)所表达的含义发⽣了变化。如果返回status=0,那么只是表⽰整个App的票据验证通过,并不表⽰票据中所包含的每个IAP receipt都有效。甚⾄有可能App Receipt中根本不包含任何IAP receipt,status也可以是0。
另外,由于App Receipt可能包含多个IAP receipt,因此App服务器并不能保证所有IAP receipt⼀次性都发货成功。
所以,在设计发货请求的响应参数的时候,⼀定要能够区分出如下⼏种case:
空调定频和变频有什么区别全部发货成功;
部分发货成功;
根本不存在IAP receipt
验证请求延迟⼤
App服务器在国内连接App Store服务器进⾏票据验证时,⽹络延迟较⼤。⼀般最低也要200多ms,⽽在⼤的时候能超过7s。
因此,如果有条件,建议给App Store服务器的验证请求加上国际代理(⽐如使⽤HTTP CONNECT tunneling),降低请求延迟。
总之,IAP和⽀付宝、⽀付的机制完全不同,它的API之所以这样设计,可能是为了同时⽀持纯客户端和有服务器的客户端。但IAP ⽬前的这种实现机制,确实给App开发者带来了挑战。它需要我们更加谨慎,每⼀步的程序逻辑都更多地考虑容错,才能实现出⼀个稳定的⽀付⽅案。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论