本文已参与“新人创作献礼”活动,和我们一起开启掘金创作之旅。
本系列的其他文章:
【1】从入门到精通苹果应用内购买(IAP)(1)-应用内购买的种类和配置
【2】从入门到精通苹果应用内购买(IAP)(2)——银行卡与税务信息配置
【3】苹果应用内购买(IAP)从入门到精通(3)-产品充值流程(非订阅)
【4】苹果应用内购买(IAP)从入门到精通(4)——订阅、续订、取消、恢复
1. 交易结束 -
[ ] 此队列包含所有已付款和未付款的订单,已付款的订单需要手动移除。一般我们会在以下几种情况下移除订单:
a. 付款失败。例如,付款被手动取消、卡内没有钱等。
b.支付成功,账单上传到服务器验证后,验证返回失败结果。
c.支付成功,账单上传至服务器验证,验证成功。
但一般情况下,对苹果票务验证接口的请求都是由App服务器完成的,将加密的原票上传到服务器后,服务器再请求对应的苹果票务验证接口,获取解密后的票务数据,之后再分析票务的合法性,合法后才会通知前端,发放道具。这个验证合法性的逻辑,只有通过延迟轮询的方式,才能被前端获取。
理想情况下,我们需要确保商品已经成功送达用户手中后再调用,但由于服务器的验证结果,前端无法立刻知道,有时候由于网络问题,甚至验证失败。因此,我们可以请求服务器,回调结果后再调用,这样不会导致充值队列卡死,但容易导致掉单,有利有弊。
对于账单上传成功,但是验证失败的情况,我现在的操作是记录该笔交易的信息(用户信息,订单号,交易时间等),然后上报日志到服务器,之后服务器记录该笔异常订单,方便后续为用户补单或者查询坏账。
2.透传参数
在发起充值之前,我们创建对象并将其添加到支付队列中。
SKMutablePayment *payment = [[SKMutablePayment alloc] init]; payment.applicationUsername = orderId; //透传参数 payment.productIdentifier = _productId; payment.quantity = count; [[SKPaymentQueue defaultQueue] addPayment:payment];
其中有一个参数,是一个透传参数,当你在支付完成中添加之后,这个参数就会存在...在实际的应用开发中,我们经常需要把自己服务器的订单号或者用户ID跟苹果订单进行关联,我们可以通过缓存的形式读写,但是我们也可以通过这个透传字段传递参数,比如我们将(订单号)写入透传字段,这样就可以把这个订单从我们创建订单发起支付到支付完成的过程中都关联起来。
这个透明字段看上去是解决我们关联问题的一个很方便的方法,但是在实际操作中我们发现支付完成后这个参数有可能为 nil,以下是我们发现的一些场景:
因此,我们可以采用“透传字段+双透传”的多种策略。如下图所示:
a. 尝试从..读取,如果uid是,则进行下一步;
b. 尝试恢复,检查 和 中记录的购买开始时间戳是否在允许范围内,保存的位置是第一次返回状态时,如果此时没有取到值,则进行下一步;
c.再次尝试恢复,该值在订单创建时被缓存。
d.每次启动App发起充值前,检查交易队列中是否有交易(非订阅类),若有则处理该交易,并重新上传票证到服务器进行验证。
3. 防止丢单及补单 (1)连续购买多件商品
比如快速连续多次点击某个商品按钮,就会触发多次IAP购买,如果不涉及APP本身的订单逻辑,苹果充值然后拿票验证的整套逻辑是没有问题的,但几乎每个APP都会把IAP订单和自己服务器的订单系统关联起来。
如果本地只缓存一个订单号,然后在验证订单请求的时候作为请求参数,目前情况下会导致订单号和实际号码错误的关联起来。
如果通过队列将订单缓存在本地,或许可以避免上述情况,但是队列计算也会考验开发者的算力,对队列中订单的误判也会导致丢单;
如果通过透明参数传递订单号或者其他信息则为空。
当然办法总是有的,我还没想出完美的处理连续购买不丢单的方案,相对来说我更愿意避免“连续购买多件商品”的情况,不让用户触发这种情况。
方式一:在一笔支付完成之前,支付队列中只允许操作一笔订单,该笔订单支付完成后,才可以发起一笔新的支付。
最简单的办法就是放一个按钮。但前提是控件直接触发支付。如果是SDK,或者游戏,那就不行了。
- (IBAction)clickBtn:(UIbutton *)sender { sender.enabled = NO; [[PayKit shareKit] startPay:^(BOOL result) { sender.enabled = YES; }]; }
您还可以使用属性标签和 BOOL 值。

if (isOrdering) { NSLog(@"订单正在支付中..."); return; } isOrdering = YES; [[PayKit shareKit] startPay:^(BOOL result) { if(支付完成){isOrdering = NO} }];
方法二:用户点击按钮后立即显示遮罩,防止用户再次点击按钮,订单支付完成后关闭遮罩。
[[MaskView shareView] showView]; [[PayKit shareKit] startPay:^(BOOL result) { if(支付完成){ [[MaskView shareView] hideView]; } }];
陷阱:点击支付按钮后,如果没有立即触发支付功能,则可能会触发多次支付。
方法三:设置1秒内只能支付一次(需配合方法二使用)
[self performSelector:(payPlatformWithModel:) withObject:payModel afterDelay:1.0]; - (void)payPlatformWithModel:(id)payModel { [[MaskView shareView] showView]; [[PayKit shareKit] startPay:^(BOOL result) { if(支付完成){ [[MaskView shareView] hideView]; } }]; }
个人建议:为了防止多次触发支付,建议全部使用,越多越好。
(2)多件商品一起付款,且苹果退回多件商品收据
如果开发者没有采用“拦截多次触发支付”的方式,而是支持多个订单连续调用,并通过支付队列将订单号和苹果收据形式正确匹配(这确实是技术上正统的做法),你很可能又踩到了苹果给你埋的另一个坑。在线上环境中,可能会触发将多个订单合并支付。比如你点击购买一个6元的商品和一个30元的商品,苹果会要求你输入密码支付36元。支付成功后,苹果只会回调你一次,然后解析后就会有两条支付数据。但对于服务器来说,6元和30元是两个订单,因为它们是分开创建的。那么我应该用哪个订单号来匹配收据呢?可能其中一个订单可以匹配,但很难两个订单都匹配(苹果把app服务器的两个订单当成一个订单),这样必然会导致其中一个订单丢失。 您也许可以在下次启动[ ]时重新验证订单来补单,或者等用户联系客服后再在服务器上手动补单。
这个问题目前没有什么很好的解决办法,所以我个人建议使用“阻止多次触发付款”的方法。
(3)苹果接口网络问题
的 API,在线上环境下,即便网络良好,也会出现回调慢,甚至请求失败的情况。以下几种情况我们需要向 发起请求:
场景一:获取商品时请求失败。
建议不要获取对象,而是手动创建以绕过此步骤。
SKMutablePayment *payment = [[SKMutablePayment alloc] init]; payment.applicationUsername = orderId; //透传参数 payment.productIdentifier = _productId; //商品ID直接传字符串 payment.quantity = count; [[SKPaymentQueue defaultQueue] addPayment:payment];
场景二:到达支付队列后,没有进行充值,直接回调“充值失败”消息;或者输入密码后,由于网络问题回调“充值失败”消息(其实并没有扣钱)。
因为此时这个订单实际上还没有发生扣款,所以不需要继续这个订单,直接走就可以了。
场景三:请求苹果接口验证失败。
因为账单验证逻辑是服务端异步完成的。如上所说,充值成功后的最佳时机应该是账单验证成功,道具(权利)已经发放的时候。但有时候为了避免等待,我们也会请求服务端验证,等服务端返回上传成功后再进行验证。但后一种情况,如果服务端请求苹果验证接口失败,而客户端已经进行了,就会导致订单掉单。这种情况下,服务端应该给订单增加“已付款,未收到”的标记,前端也可以上报自定义日志,告诉服务端“此订单异常”。当客户端因提前无法再次发起验证时,用户找到客服后,服务端需要根据订单状态手动补单。
但如果等到道具发放的时候再做,这个问题就可以避免,只是需要等待的时间更长,有利有弊。
(4)启用付款队列监控的最佳位置是哪里?
当我们调用[[ ]er:self]开启支付队列的时候,支付队列会自动检查是否有,并回调给我们。那么这个队列监控应该在哪里开启比较合适呢?
1.启动App时调用
[[SKPaymentQueue defaultQueue] addTransactionObserver:**self**];
注意这里调用的是er而不是,因为支付队列还没初始化,直接调用它不会有任何效果。
若有未完成交易,会回调该交易的最新状态,若是状态,则输入密码,成功后继续走验证逻辑;若是状态,则上传票证到服务器进行验证。注意:在未完成交易的情况下,上述透传参数可能为空,请确保订单数据匹配。
2. 发起新付款前先致电
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
与上面描述相同。
以上方法只能减少丢单的发生,但不能完全解决问题。每个APP的逻辑不一样,坑点也不一样。丢单怎么补?补哪个账号?补哪个订单?很多问题需要结合具体问题具体分析。
【参考】
【6】IAP苹果支付防丢单攻略
【7】谈谈苹果应用内支付(IAP)的陷阱
【8】iOS内购-防越狱破解与盗购