苹果内购
前言
接触公司的充值业务很久了,在处理苹果充值的时候也踩了很多的坑,这里就花时间来总结下。
苹果内购
IAP 全称:
In-App Purchase
,是指苹果
App Store
的应用内购买,是苹果为 App 内购买虚拟商品或服务提供的一套交易系统。
为什么这里着重来介绍 IAP 呢,因为 IAP 和微信支付和支付宝支付的实现逻辑不太一样,因为 IAP 支付依赖 IOS 客户端调用异步支付回调接口,进行用户和订单的绑定,会有丢单的情况产生,这里
假定大家对苹果支付有一定了解了,重点主要是来聊下苹果支付的技术实现方案。
聊下,目前 IAP 中充值我们遇到的问题点
1、丢单;
2、订单充值到错误的账号;
3、在苹果设置中直接点击充值,导致不到账。
苹果支付的难点
关于苹果充值的难点,这边总结下来就是下面两点:
1、如何处理订单回执和用户账号的绑定关系;
2、APP 中复杂的网络环境,如何保证订单能够成功回调;
方案设计
下面来介绍下苹果支付中需要特别关注的点。
1、商品设计
苹果对应的充值商品 id 需要进行申请,命名规则没有强制性的要求,需要根据自己的商品类型来进行映射。通常的做法,服务端会定义自己系统的内部的商品,服务端会提供一个提供一个商品列表的接口,会把自己系统内部的商品信息,映射到苹果系统中的商品id。
如何合理的设计苹果 iap 的商品ID呢?一般有两种做法?
1、每个系统内部的商品都对应一个苹果 iap 的商品id;
这种就是需要申请的商品id比较多,同时如果商品信息发生更改,就需要重新申请,当然,如果自己系统内部的商品,比较单一,并且基本上能够保持不变,那这种方式可以商品设计的选择之一。
2、每种类型的商品,一个价格对应一个苹果 iap 的商品id;
苹果的 iap 的商品id和价格做绑定了,如果商品信息有变化,只要价格不变,就不用重新申请对应的 iap 商品id 了。
具体使用哪种可以根据自己系统中的商品特点来进行选择。
2、用户和回执的绑定
苹果支付中,苹果回调中的是没有第三方订单信息的,所以,苹果订单回执信息和用户的绑定需要在 APP 中完成。
苹果充值成功,给到 IOS 客户端充值的回调信息,然后客户端需要把当前的回执信息和当前的用户做好绑定,上报数据到自己的服务端,进行订单校验和充值道具的下发。
处理充值的时候,每一笔订单都会生成唯一的订单号。用户和回执信息的绑定,也就是,订单号和用户回执信息的绑定,因为订单对应数据,里面会关联用户信息。
这样用户和回执的绑定也就回归到用户订单和苹果回执的绑定,那么订单号在何时生成,如何和苹果回执做绑定呢?有两种方案,下面来一一探讨下。
1、生成订单 -> 发起 iap 充值 -> 客户端 绑定订单id和 iap 充值的回执信息,向服务端发起回调 -> 服务端接收到回调信息,校验订单回执,给用户增加道具;
2、发起 iap 充值 -> 接收到回调信息 -> 客户端将商品信息,用户信息,iap 的回执信息,一起回调给服务端 -> 服务端接收到回调信息,校验回执信息,生成对应的订单,同时给用户增加道具;
这两种最主要的差别就是订单的生成时机不同,第一中订单号对客户端是可见的,第二种订单号对客户端是不可见的。
1、订单提前生成
订单提前生成,比较符合我们处理的订单的逻辑,如果自己的充值系统中也同时集成了微信和支付宝的支付,那么选用这种方式,订单的处理就相对统一。
缺点:
1、每次点击充值就会有一个订单的生成,即使用户后面没有实际的充值,会有无效订单数据的产生;
2、因为订单号和苹果回执信息的绑定是需要在客户端中进行绑定操作的,那么这个就会存在绑定错乱的情况,用户可能点击充值了多个商品,这时候处理绑定的时候, A 商品的订单号,绑定到了 B 商品的回执信息上了。结果就是用户订单异常。
2、回调的时候生成订单号
订单是后面生成的,所以可以控制只有回执信息验证通过的时候才生成订单号,避免了无用订单的生成。
不过这两种处理方式个人感觉差别不大,设计的时候考虑下如果有其它的支付方式同时存在,如何做好兼容就行了。
我们用的第一种,提前生成订单,因为我们系统中同时存在了,微信,支付宝和苹果充值,所以各个充值方式的兼容也是我们考虑的一个重要的点。
不过也根据第一种方式做了细微的调整,因为苹果充值存在充值成功但是回调是失败的情况,接收到失败的订单回调,本地记录的订单号就会被清理了,这时候有成功的回调过来,本地就没有对应的订单号了,同时对于订阅商品,下一个扣款日重新发我续订,app 接收到回调通知,本地也是没有存储对应的订单号。没有订单号,也就意味着苹果的充值回执信息,和用户关联不上了。这种情况,我们的处理方式就是,app 接收到成功的回调,本地没有订单号,就重新调用服务端接口重新生成一个。保证用户和苹果的回执信息一定能关联上。
3、回调的重试
因为苹果订单的绑定依赖于客户端,当网络环境不好,接收到回调信息,之后向自己的服务端进行回执信息的验证,出现接口请求不通的情况,这时候就需要执行回调回执的重试。
当然重试分成两部分
1、客户端回调回执信息的重试;
2、服务端验证回执信息的重试。
一般服务端在设计充值的时候,都会使用到分布式事务,消息队列等来保证事务的最终一致性。同时对于一些第三方的请求,也会有对应的重试机制:
1、数据标记法:接收到请求,先落数据库,如果验证成功将数据标记成功,如果一直没有验证处理成功,就定时从数据库将此数据取出来,继续重试验证的逻辑;
2、消息队列的重试机制:一般消息队列都有对应的重试机制,消息验证成功,就将消息从队列中移除,否则重新丢到队列中,借助队列的重试机制,知道本消息处理成功。
客户端部分的重试步骤:
1、接收到苹果的回调,拼接订单和用户信息,向自己的服务端发起回调;
2、如果自己服务端的回调接口,返回成功,在一定时间内定时查询该订单的状态,如果订单返回成功,更新用户的账户信息;
3、如果回调自己的服务端失败,这时候就需要进行重试操作,1分钟内重试5次,直到该回调接口返回成功的状态;
4、如果1分钟内重试了5次还是失败,记录该回执信息,用户打开app,或者切换到充值页面的时候继续重试。
因为对于回调的处理,服务端只要接收到请求,就会记录该回调信息,然后接口返回成功,所以只要客户端的网络正常,这种失败的情况是不会出现的,多次的重试操作一定能规避这种情况。
充值冲遇到的问题点
1、丢单
丢单是苹果充值经常遇到的问题,因为是 app 接收到的苹果的服务回调,相比于服务端的
sever to server
的通知,受限于客户端的当时的网络环境,app 的打开状况,稳定性是偏差的。
如何处理呢?
原则上就是客户端接收到苹果的回调通知,尽可能的拼接用户信息和回执信息发送给自己的服务单进行数据的验证,服务在验证数据的时候做好订单的唯一性处理,避免商品超发的情况。
总结了可能有下面几种情况:
1、接收到异常的回调:充值成功了,但是客户端先接收到的是一个充值失败的回调,然后草草结束掉本次订单,导致后面收到了充值成功的订单,但是订单已经结束就不处理了,最终结果就是丢单了;
2、网络不稳定:充值成功了,客户端成功接收到了苹果的回调,但是给自己服务端回调的时候,因为网络原因导致回调失败了,结果就是用户丢单了;
3、用户频繁切换账户:充值成功了,用户在充值过程中发生了账号的切换,因为使用的苹果账号是同一个,但是登陆 app 的账号可以是多个,切换账号的过程中,充值到其中一个账号中了,但是给到用户的体验就是当前账号没到账,就认为是丢单了;
4、自己服务端校验票据异常:充值成功了,客户端接收到了回调,但是回调回执信息给自己服务端的时候,服务端在验证票据的时候出现了异常,导致该订单验证失败,用户的体验就是该订单丢单了;
下面来分析下上面的几种情况:
对于情况场景 1 和场景 2,客户端尽可能的做好重试,只要接收到苹果 iap 中的回调,就拼接信息回掉给自己的服务端,如果当前的回调接口没有返回成功的标识,就要继续重试。
对于场景 3 ,可以在交互上优化,用户充值之后返回 app ,可以加一个充值中的 loading 页面,避免用户在这个过程中出现切换账号的操作。
对于场景 3 ,一般服务端在设计充值这种业务的时候都会用到分布式事务,所以这种情况是能够避免的。
2、充值成功,下发的物品不对
因为充值商品,订单号和 ipa 充值回执信息的绑定是在 app 中操作的。如果用户在充值的时候有频繁点击充值的行为,那么在绑定充值回执的数据的时候,就有可能出现绑定错乱的情况。
原来商品的 a 的回执信息,绑定时候,被绑定到了 商品 b 上面。
这时候服务端就需要做好数据的检验,如果通过回执信息请求苹果的订单接口是能拿到,充值订单对应的 iap 商品,通过这个商品就能判断回到数据绑定的数据是否正确,如果不正确修改当前订单信息的数据即可。
3、处理退款
根据苹果的策略,用户在购买IAP后90天内,能以各种原因申请退款(扣款后购买失败、买错了、不喜欢等等)。
用户成功申请退款了,系统中对应的道具也要清除掉,不然就是充值漏洞了,里面的商品就会被用户白嫖了。
苹果在
WWDC 2020
苹果全球开发者大会,苹果宣布所有的内购项类型,当用户在应用内退款成功时,
App Store Server
会发送实时的通知给开发者服务器告知有退款,开发者可通过处理该消息来更新用户的账户信息。
退款流程:
1、用户购买内购商品;
2、用户申请退款;
3、苹果发起退款;
4、Apple Store Server 发送退款通知;
5、用户收到退款成功的通知;
6、开发者收到退款订单通知。
最后来看下普通充值的订单的具体信息
{
"environment": "Production", // 当前的环境,Production表示生产环境,Sandbox表示的是沙盒环境
"receipt": {
"receipt_type": "Production",
"adam_id": 6666666,
"app_item_id": 8888888,
"bundle_id": "test.888888",
"application_version": "4.79.0.1",
"download_id": 999999999,
"version_external_identifier": 862386348,
"receipt_creation_date": "2024-01-07 04:33:30 Etc/GMT",
"receipt_creation_date_ms": "1704602010000",
"receipt_creation_date_pst": "2024-01-06 20:33:30 America/Los_Angeles",
"request_date": "2024-01-10 01:39:43 Etc/GMT",
"request_date_ms": "1704850783803",
"request_date_pst": "2024-01-09 17:39:43 America/Los_Angeles",
"original_purchase_date": "2023-12-30 23:42:26 Etc/GMT",
"original_purchase_date_ms": "1703979746000",
"original_purchase_date_pst": "2023-12-30 15:42:26 America/Los_Angeles",
"original_application_version": "4.79.0.1",
"in_app": [{
"quantity": "1", // 商品的数量
"product_id": "6666661101_2_2_12.00", // iap 的商品id
"transaction_id": "381201227775036", // 交易号
"original_transaction_id": "381201227775036", // 原始交易号
"purchase_date": "2024-01-07 04:33:29 Etc/GMT", // 最新的购买时间
"purchase_date_ms": "1704602009000", // 最新的购买时间毫秒
"purchase_date_pst": "2024-01-06 20:33:29 America/Los_Angeles", // 最新的购买时间,太平洋时间
"original_purchase_date": "2024-01-07 04:33:29 Etc/GMT", // 最初的购买时间
"original_purchase_date_ms": "1704602009000", // 最初的购买时间,毫秒
"original_purchase_date_pst": "2024-01-06 20:33:29 America/Los_Angeles", // 最初的购买时间太平洋时间
"is_trial_period": "false", // 是否是试用期
"in_app_ownership_type": "PURCHASED"
}
]
},
"latest_receipt": "xxxxx", // 凭证信息
"status": 0 //
}
苹果订阅
上面简单聊了下苹果中普通商品的充值流程,下面来聊一下订阅商品的充值。
自动订阅根据名字能看出来相对于普通的商品,自动订阅的商品到了扣款周期,苹果会自动发起重新扣款。
先来看下订阅的充值流程,订阅的首次充值流程和普通的商品的首次充值流程一样,充值成功之后,后面会涉及订阅的下次扣费,所以后面多了原始交易号和回执信息的记录,方便后面自动扣款的续订操作。
苹果订阅商品在下个扣款周期扣费的时候,扣款的动作由苹果自动发起,这和支付宝和微信的订阅扣款逻辑不同。
因为是苹果自动进行的扣款处理,所以会如果苹果扣款的时候的通知不及时或者消息丢失,就很容易造成用户的丢单情况。
处理思路:
1、配置服务端回调通知
配置服务端回调通知,配置服务端通知的动作思可选的,不过建议开启。开启之后就能及时收到苹果的订单的状态,处理用户的订单状态。
App Store Server Notifications V1
服务端的通知类型,有下面几种:
CANCEL:表示苹果支持已经取消了自动续期订阅并且用户在cancellation_date_ms时间收到了退款信息;
触发时机:
CANCEL事件通过AppleCare支持取消订阅并退还购买款项时触发。
DID_CHANGE_RENEWAL_PREF:表示客户对其订阅计划进行了更改,该更改会在下一次续订时生效。当前活动的计划不受影响;
触发时机:
当用户在同一订阅分组中,从一个订阅商品切换到另一个订阅商品时,会触发 DID_CHANGE_RENEWAL_PREF 事件;
DID_CHANGE_RENEWAL_STATUS:表示订阅续订状态发生变化;
触发时机:
用户关闭了订阅,或者非订阅状态重新续订。
通过判断 auto_renew_status 判断当前的订阅状态,auto_renew_status == 0 表示订阅状态已经关闭, auto_renew_status == 1 表示订阅处于开启状态。
DID_FAIL_TO_RENEW:表示由于计费问题而无法续订的订阅,栗如,用户当前卡上没钱了;
DID_RECOVER:表示成功自动续订一个过去续订失败的过期订阅。检查expires_date以确定下一次续费的日期和时间;
DID_RENEW:表示用户当前的订阅周期已经重新续订了;
INITIAL_BUY:用户在首次发生订阅时触发;
INTERACTIVE_RENEWAL:表示客户通过使用应用程序界面或在 App Store 帐户的订阅设置中以交互方式续订订阅;
触发时机:
用户取消了订阅,一段时间后用户通过 AppStore 交互页面重新订阅产品,会触发 INTERACTIVE_RENEWAL 事件。
REFUND:表示 App Store 已成功对消耗性应用内购买、非消耗性应用内购买或非续订订阅的交易进行退款,不同于取消(CANCEL)通知类型,取消通知类型针对的是自动续期订阅类型商品,用户通过 AppleCare 支持取消订阅并退还购买款项时触发;
苹果服务端的返回状态 DID_RENEW 就表示苹果当前的订阅的扣款已经成功了,接收到这个状态的通知,处理用户的订单即可。
2、客户端通知;
苹果每次订阅的扣款也会下发通知到客户端,客户端接收到的扣款成功的通知,回调该信息到自己的服务端,服务端接收到该回调通知,判断当前订阅的道具有没有下发,没有下发,修改本次订阅的状态,下发对应的充值道具给到当前的用户,并记录本次订阅已经完成。
3、服务端定时轮询;
服务端定时轮询快到期的订阅,向苹果发起请求查询当前订阅的状态,判断订阅当前的扣款状态,如果扣款了,就修改订阅扣款到下个周期,下发充值道具给到用户,否则就继续轮询,直到用户取消订阅,或者用户订阅扣款成功。
StoreKit 1 对比 2
StoreKit 1
存在的问题:
1、苹果后台不能查看到退款的订单详情。只能苹果处理退款后发通知给我们的服务器,告知发生了一笔退款;
2、消耗性、非消耗性、非续期订阅、自动续订能不能在沙盒环境测试退款,系统没提供这种测试方式;
3、不能够将用户反馈的苹果付费收据里的 orderID 与具体的业务订单进行关联;
4、研发过程中,无法直接关联苹果交易号 transactionId 与 业务订单号 orderID 之间联系,、在开发过程中,无法直接关联 transaction 与 orderID 之间联系,虽然有一个 applicationUserName 字段,可以存储一个信息。但是这个字段是不是 100%靠谱,在某些情况下会丢失存储的数据;
StoreKit 2
2021 年 WWDC,在 iOS 15 系统上推出了一个新的
StoreKit 2
库,该库采用了完全新的 API 来解决应用内购买问题。
StoreKit 2
主要的更新有这几个:
StoreKit 2 库采用 Swift 5.5 版本最新特性重写,只支持 Swift、iOS 15+,提供了一些新的 API 接口,导致新的支付流程会发生一些变化。
1、提供了获取交易历史记录、可购买的商品列表(自动续期订阅以及非消耗品)信息;
2、提供了获取订阅状态、管理订阅状态接口;
3、支持在 App 内发起退款。
新的 api
1、新的商品接口
新增了一些商品类型,订阅信息,这些字段信息在 StoreKit 1 里是没有的。
方便利用的字段:
1、通过新增的 product type 我们可以轻易的知道当前的商品是消耗品还是订阅商品;
2、针对于自动连续订阅的第一次购买优惠,我们可以直接感知到当前的商品是不是用户的 Apple ID 下的第一次购买;
2、新的购买接口
提供了新的购买商品接口。其中购买商品时增加了一些可选参数 PurchaseOption 结构体,该结构体里有新增的特别重要的字段 appAccountToken, 类似 SKPayment.applicationUsername 字段,但是 appAccountToken 信息会永久保存在 Transaction 信息内。
appAccountToken 字段是由开发者创建的;关联到 App 里的用户账号;使用 UUID 格式;永久存储在 Transaction 信息里。这里的存储的信息,不会像 v1 版本,存在数据丢失的情况。
这里的 appAccountToken 字段苹果的意思是用来存储用户账号信息的,但是应该也可以用来存储 orderID 相关的信息,需要将 orderID 转成 UUID 格式塞到 Transaction 信息内,方便处理补单、退款等操作。
处理验证 Transaction。系统会验证是否是一个合法的 Transaction,此时系统不再提供 base64 的 receip string 信息,只需要上传 transaction.id 和 transaction.originalID,服务器端根据需要选择合适的 ID 进行验证。
3、交易历史查询接口
提供了三个新的交易(Transcation)相关的 API:
1、All transactions:全部的购买交易订单,在 transaction 里面获取;
2、Latest transactions:最新的购买交易订单;
3、Current entitlements:所有当前订阅的交易,以及所有购买(且未退还)的非消耗品。
4、订阅类型项目的状态
订阅类型项目的状态,比如主动获取最新的交易、获取更新订阅的状态,获取更新订阅的信息等。其中获取更新订阅的信息,可以获取更新的状态、品项 id、如果过期的话,可以知道过期的原因。(比如用户取消、扣费失败、订阅正常过期等。)获取的所有数据都是 JWS 格式验证。
5、管理订阅页面
可以直接唤起 App Store 里的管理订阅页面。
6、退款api
提供了新的发起退款 API,允许用户在开发者的 App 中直接进行退款申请。用户进行申请退款后,App 可以收到通知、另外苹果服务器也会通知开发者服务器。
StoreKit 2
支持 iOS 15 以上的系统,所有如果用户有很多这个版本之下的用户,就需要考虑如何合理的接入新的版本了。
对于后端来说,
Apple Server API V1
和
Apple Server API V2
都能够运用,与客户端是否升级到
StoreKit 2
无关。
总结
上面主要总结了苹果支付的主要逻辑。
1、苹果支付对比微信和支付宝的最大的不同就是,IAP 支付依赖 IOS 客户端调用异步支付回调接口,进行用户和订单的绑定;
2、苹果支付最大的难点就是用户和回执的绑定;
3、重试,因为依赖于 app 的回调,所有当网络环境不好,接收到回调信息,之后向自己的服务端进行回执信息的验证,出现接口请求不通的情况,这时候就需要执行回调回执的重试;
客户端部分的重试步骤:
1、接收到苹果的回调,拼接订单和用户信息,向自己的服务端发起回调;
2、如果自己服务端的回调接口,返回成功,在一定时间内定时查询该订单的状态,如果订单返回成功,更新用户的账户信息;
3、如果回调自己的服务端失败,这时候就需要进行重试操作,1分钟内重试5次,直到该回调接口返回成功的状态;
4、如果1分钟内重试了5次还是失败,记录该回执信息,用户打开app,或者切换到充值页面的时候继续重试。
因为对于回调的处理,服务端只要接收到请求,就会记录该回调信息,然后接口返回成功,所以只要客户端的网络正常,这种失败的情况是不会出现的,多次的重试操作一定能规避这种情况。
4、退款的处理,根据苹果的策略,用户在购买IAP后90天内,能以各种原因申请退款(扣款后购买失败、买错了、不喜欢等等);
5、苹果订阅:因为订阅是苹果直接发起的,所以我们要合理的处理订阅扣款之后的回调;
1、配置服务端回调通知:配置服务端通知的动作思可选的,不过建议开启。开启之后就能及时收到苹果的订单的状态,处理用户的订单状态;
回调中的 CANCEL:表示苹果支持已经取消了自动续期订阅并且用户在cancellation_date_ms时间收到了退款信息;
2、客户端通知;
苹果每次订阅的扣款也会下发通知到客户端,客户端接收到的扣款成功的通知,回调该信息到自己的服务端,服务端接收到该回调通知,判断当前订阅的道具有没有下发,没有下发,修改本次订阅的状态,下发对应的充值道具给到当前的用户,并记录本次订阅已经完成。
3、服务端定时轮询;
服务端定时轮询快到期的订阅,向苹果发起请求查询当前订阅的状态,判断订阅当前的扣款状态,如果扣款了,就修改订阅扣款到下个周期,下发充值道具给到用户,否则就继续轮询,直到用户取消订阅,或者用户订阅扣款成功。
参考
【AppStore内购】
https://liushoukai.github.io/2020/04/04/appstore-in-app-purchase/
【官方文档】
https://developer.apple.com/cn/in-app-purchase/
【iOS StoreKit 2 新特性解析】
https://juejin.cn/post/7096063372159877150
【苹果支付】
https://boilingfrog.github.io/2024/01/28/苹果iap支付/