应用内购买前言
我已经参与公司充值业务很久了,在处理苹果充值的时候也踩过不少坑,这里抽空总结一下。
应用内购买
IAP是In-App的缩写,是指苹果App内购买,是苹果提供的用于在App内购买虚拟商品或服务的交易系统。
这里为什么要重点讲IAP呢?因为IAP的实现逻辑和微信支付、支付宝不一样,IAP支付依赖IOS客户端调用异步支付回调接口绑定用户和订单,可能会造成订单丢失。这里我们假设大家对 Pay有一定的了解,重点主要讨论 Pay的技术实现方案。
说说IAP充值遇到的问题
1.订单丢失;
2.订单充值到错误的账户;
3.直接在苹果设置里点击充值,导致没有积分。
Pay 的难点
关于苹果充值的难点,这里说一下以下两点:
1、如何处理订单收据与用户账户的绑定关系;
2、APP在复杂的网络环境下,如何保证订单能够成功回调?
解决方案设计
以下是 Pay需要特别注意的一些点。
1. 产品设计
需要申请苹果对应的充值产品ID,命名规则没有强制要求,需要根据产品类型进行映射,通常服务端会定义自己系统内部的产品,服务端会提供一个提供产品列表的接口,将自己系统中的产品信息映射到苹果系统中的产品ID。
苹果iap的 ID该如何合理设计呢?一般有两种做法?
1、系统中的每个产品都对应一个 IAP产品ID;
这种方式需要申请更多的产品ID,如果产品信息发生变化,则需要重新申请。当然如果你的系统内的产品比较简单,基本可以保持不变,那么这种方式可以作为产品设计的选择之一。
2、每一类产品一个价格对应一个 IAP产品ID;
苹果的IAP产品ID是和价格绑定的,如果产品信息发生变化,只要价格不变,不需要重新申请对应的IAP产品ID。
具体使用哪种方式可以根据您系统中产品的特性来选择。
2. 用户与收据的绑定
在 Pay中,苹果回调中是没有第三方订单信息的,所以苹果订单收款信息和用户的绑定需要在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接收苹果服务回调,受限于客户端当前网络环境以及APP开通状态,相对于服务端通知稳定性较差。
如何处理?
原则上,客户端在收到苹果的回调通知时,会尽力将用户信息和收货信息拼凑起来,发送给自己的服务订单进行数据验证,服务在验证数据时会保证订单的唯一性,避免商品超发。
可以概括为以下几种情况:
1、收到异常回调:充值成功,但是客户端先收到充值失败的回调,然后草草结束订单,导致客户端收到充值成功的订单,但是由于已经完成,所以不再处理该订单,最终的结果是订单丢失;
2、网络不稳定:充值成功,客户端成功收到苹果的回调,但在回调自身服务器时,由于网络原因回调失败,导致用户丢单。
3、用户频繁切换账户:充值成功,但是用户在充值过程中切换账户。由于使用的苹果账户是同一个,但是登录App的账户可以是多个,在切换账户过程中,充值转到其中一个账户,但是用户体验到的是当前账户还没有到账,所以认为订单丢失;
4、服务器验票异常:充值成功,客户端收到回调,但是在向服务器发送回调收款信息时,服务器在验票时出现异常,导致订单验证失败,用户体验就是订单丢失;
我们来分析一下上面的情况:
对于场景1和2,客户端应该尽量重试,只要收到苹果IAP的回调,就会拼接信息回传给自己的服务器,如果当前回调接口没有返回成功标记,则继续重试。
针对场景三,可以优化交互,当用户充值后返回APP时,可以增加充值页面,避免用户中途切换账号。
对于场景三,由于服务端在设计充值等服务时,一般都会采用分布式事务,因此可以避免这种情况。
2.充值成功,但发送的商品不正确
由于充值商品、订单号、ipa充值收据信息的绑定都是在APP内完成的,如果用户在充值时频繁点击充值按钮,绑定充值收据数据时可能会出现混乱。
绑定过程中将原产品a的收货信息与产品b进行了绑定。
这时候服务端就需要对数据进行验证了,如果通过苹果订单接口接收到收款信息,就可以获取到该充值订单对应的IAP产品,通过这个产品可以判断返回的数据绑定的数据是否正确,如果不正确,就可以修改当前订单信息的数据。
3. 处理退款
根据苹果的政策,用户在购买IAP后的90天内,可以因各种原因(扣款后购买失败、错误购买、不喜欢等)申请退款。
如果用户成功申请退款,系统内对应的商品也必须清零,否则就是充值漏洞,系统内商品会被用户免费拿走。
在 WWDC 2020 上,苹果宣布针对所有类型的应用内购买,当用户成功退款时,App 会向开发者服务器发送实时通知,告知退款成功,开发者可以通过处理该消息来更新用户的账户信息。
退款流程:
1、用户购买应用内商品;
2.用户申请退款;
3.苹果发起退款;
4.发送退款通知;
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": "1111101_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 // }