苹果IAP开发中的那些坑和掉单问题_苹果充值掉单怎么回事-程序员宅基地

技术标签: java  

相信很多iOS App的开发者,特别是手游开发者,都接触过苹果支付IAP(In-App Purchase)。相信使用了IAP的App,都经历过“掉单”问题。

什么是“掉单”呢?简言之就是用户付款买金币,钱扣了,金币却没到账。

掉单一旦发生,用户通常会很愤怒地来找客服。然后客服只能找开发人员把金币给用户手动加上。

显然,伤害用户的体验,特别是伤害付费用户的体验,是一件相当糟糕的事情。

我们在微爱App的开发过程中,IAP支付的掉单问题也困扰了我们很久。直到去年第四季度的一次优化,才算是彻底解决了掉单问题。

掉单是如何产生的呢?这需要从IAP支付的技术流程说起。

IAP同国内的支付宝、微信支付都是用于支付的平台接口,但它们在支付的技术流程上却有着本质的不同。

支付宝和微信支付在支付流程上非常相似(顺便说一下,微信支付早期的API设计甚至跟支付宝API在参数命名上都保持一致),如果忽略掉它们之间的细微差别,那么它们的支付流程大体上可以按如下描述:

  1. 第三方App在前端发起支付请求;

  2. 前端跳转到支付宝(微信)客户端,用户完成支付;

  3. 前端跳回第三方App;

  4. 在第3步的同时支付宝(微信)服务器回调第三方App的服务器,在这个回调中第三方App服务器完成订单验证和发货操作(给用户加金币)。

注:目前的微信支付在最开始还多一步向平台获取prepayid的过程,不过这不是我们讨论的重点,我们暂时忽略它。

而IAP的支付流程完全不同:

  1. App调用IAP接口在前端发起支付请求,用户在前端完成支付;

  2. 用户完成后在前端回调App;

  3. 如果App是一个纯客户端应用(没有服务器,比如单机手游),那么App取到票据(receipt),然后直接在前端进行本地验证。如果验证成功则立即在前端完成发货。支付流程到此结束。

  4. 如果App有服务器,那么它需要将票据(receipt)传给App服务器。

  5. 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的transactionReceipt属性,该属性已经从iOS 7.0开始过期),那么会产生更多问题。比如连续产生了多个未完成的订单事务,那么有可能只能恢复最后一个。

在任何工程性的系统中,失败和错误都不可避免。好的技术方案不仅能在正常的情况下保证逻辑正确,还应该能保证在系统发生错误的时候让系统有机会从错误状态中恢复。在支付宝和微信支付中,错误恢复主要由平台服务器负责(重试回调),App开发者承担的任务较少;而在IAP中,错误恢复很大程度上要依赖App开发者来完成。App开发者要确保App客户端和App服务器之间有一个更强的通信通道。这样看来,IAP比支付宝或微信支付更容易发生掉单现象,也就不足为奇了。

为了应对IAP支付流程中的上述缺陷,我们在优化中考虑了如下的关键点:

  • 自动重试的发货任务。

  • 使用App Receipt来代替transactionReceipt。

  • 发货任务的重启动不直接依赖IAP的事务机制。

下面分别详细介绍一下这几个点。

首先是自动重试的发货任务。正常情况下,在用户完成付款后(即事务状态变为SKPaymentTransactionStatePurchased时),发货任务被启动。发货任务一旦启动,将会不断重试,直到发货成功。因此,启动后的发货任务可能处于两种不同的状态:

  1. 等待服务器响应的状态。发货任务已经向服务器提交了发货请求,正在等待服务器响应。

  2. 等待重试的状态。由于上一次发货请求失败了,当前正在等待在一段时间之后重新提交发货请求。

另外,即使是发生连续多次支付行为,程序逻辑也要保证发货任务同时不会启动多次。考虑到这些因素,发货任务的启动逻辑可以按如下设计:

  • 如果没有任务在执行,那么启动发货任务;

  • 如果有发货任务在执行,那么看它当前处于什么状态:

    • 如果有变化,则取消原来的发货请求,并立即发起一次新的请求;

    • 如果无变化,则什么也不做,静等之前任务执行。

    • 如果正在等待重试,那么取消重试任务,立即开始一次新的请求;

    • 如果正在等待服务器响应,那么再看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接口的安全问题

早在2012年7月,IAP曾被俄罗斯的工程师ZonD80破解。比如,这篇早期的文章(http://www.zdnet.com/article/apple-ios-in-app-purchases-hacked-everything-is-free-video/)声称:

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:来创建一个SKPayment实例。这样可以做到先将商店购买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开发者带来了挑战。它需要我们更加谨慎,每一步的程序逻辑都更多地考虑容错,才能实现出一个稳定的支付方案。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/xtqve/article/details/52485278

智能推荐

15、Nuxt.js代理转发解决跨域问题_nuxtjs 跨域-程序员宅基地

文章浏览阅读340次。【代码】15、Nuxt.js代理转发解决跨域问题。_nuxtjs 跨域

如何在CKEditor5富文本编辑器中获取工具栏可用项目_ckeditor5获取默认工具栏-程序员宅基地

文章浏览阅读654次。CKEditor5富文本编辑器官方地址Vue 的一个更简单的解决方案 - 只需@ready="onReady"在ckeditor组件和onReady方法中监听:onReady(event) { console.log(Array.from(event.ui.componentFactory.names()));},在vue.js中简单通过下面的示例即可实现;import ClassicEditorfrom '@ckeditor/ckeditor5-build-classic';expor_ckeditor5获取默认工具栏

《一个小时,学会黑客技能》——以前BiliBili上面的那个视频中的那个网站,我终于又找到了_黑客教学视频在哪里可以找到?-程序员宅基地

文章浏览阅读1.8k次,点赞4次,收藏11次。可能是最好用的网上教程。_黑客教学视频在哪里可以找到?

探秘GA-BP:一款强大的遗传算法与反向传播神经网络集成工具-程序员宅基地

文章浏览阅读411次,点赞4次,收藏7次。探秘GA-BP:一款强大的遗传算法与反向传播神经网络集成工具项目地址:https://gitcode.com/Grootzz/GA-BP项目简介GA-BP 是一个开源项目,它结合了遗传算法(Genetic Algorithm, GA)和反向传播(Backpropagation, BP)神经网络,旨在为机器学习和优化问题提供一种创新的解决方案。由开发者Grootzz维护,该项目提供了Pytho..._ga bpnn csdn

8个Python必备的PyCharm插件_pycharm常见插件-程序员宅基地

文章浏览阅读2k次,点赞28次,收藏35次。大家好,在PyCharm中浏览插件列表并尝试很多人推荐的插件后,总结了几个瑰宝插件,它们各自以独特的方式帮助开发者快速、简便、愉悦地开发,接下来将逐个介绍它们。_pycharm常见插件

java面试题之volatile和synchronized的使用方法和区别_javasynchronized和volatile面试题-程序员宅基地

文章浏览阅读8.3k次,点赞11次,收藏36次。我们先来看一下Java 内存模型中的可见性、原子性和有序性。可见性:可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。原子性:原子是世界上的最小单位,具有不可分割性。synchronized块之间的操作就具备原子性。volatile关键字定义的变量就可以做到这一点,Java还有两个关键字能实现可见性,即synchronized和final。有序性..._javasynchronized和volatile面试题

随便推点

蚂蚁二面遭JVM调优灵魂拷问,逼得我啃透500页JVM实战笔记,成功上岸京东-程序员宅基地

文章浏览阅读505次,点赞5次,收藏12次。就写到这了,也算是给这段时间的面试做一个总结,查漏补缺,祝自己好运吧,也希望正在求职或者打算跳槽的 程序员看到这个文章能有一点点帮助或收获,我就心满意足了。多思考,多问为什么。希望小伙伴们早点收到满意的offer!越努力越幸运!金九银十已经过了,就目前国内的面试模式来讲,在面试前积极的准备面试,复习整个 Java 知识体系将变得非常重要,可以很负责任的说一句,复习准备的是否充分,将直接影响你入职的成功率。

序列号 java_JAVA序列号的serialVersionUID-程序员宅基地

文章浏览阅读411次。serialVersionUID 的规范 Serializable 和 ExternalizableJava类通过实现 java.io.Serializable 接口以启用其序列化功能。未实现此接口的类将无法进行序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。如果读者看过Serializable的源码,就会发现,他只是一个空的接口,里面什么东西都没有。Serializable接口没有方..._serialversionuid 序列号

推出 TensorFlow 图神经网络 (GNNs)-程序员宅基地

文章浏览阅读1.1k次。发布人:Sibon Li、Jan Pfeifer、Bryan Perozzi 和 Douglas Yarrington日前,我们很高兴发布了 TensorFlow 图神经网络 (Graph..._图神经网络是用thesfolw吗

产生死锁的必要条件_不是产生线程死锁的必要条件是 一个进程因请求资源而阻塞时,对已获得的资源保特不-程序员宅基地

文章浏览阅读1k次,点赞2次,收藏2次。产生死锁的原因主要是:(1) 因为系统资源不足。(2) 进程运行推进的顺序不合适。(3) 资源分配不当等。如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。产生死锁的四个必要条件:(1)互斥条件:一个资源每次只能被一个进程使用。(2)请求与保持条件(占有等待):..._不是产生线程死锁的必要条件是 一个进程因请求资源而阻塞时,对已获得的资源保特不

android软件安装到平板,新人看过来 安卓平板装机必备软件推荐-程序员宅基地

文章浏览阅读1.6k次。1平板与PC的桥梁:豌豆荚、91【PConline 应用】前几天我们为大家简单对比介绍了一下不同第三方安卓电子市场之间的差异。如今,安卓平板已经到手,也知道哪些电子市场好了,开始疯狂装软件呗。停!急切体验各种应用、游戏的心情可以理解,但不假思索的看见叫QQ的就下载,它真的适合你手中的平板吗?辛苦下载了20款软件,结果试用一圈后发现18个都不给力,这岂不是既浪费时间又影响心情。说的可能有点夸张,但如..._平板电脑装什么小程序可以用安卓手机

Spring-aop的四种增强方式_spring aop增强方法-程序员宅基地

文章浏览阅读144次。java_spring aop增强方法

推荐文章

热门文章

相关标签