写在前面
网络上关于内购支付的文章大都千篇一律,讲述的是如何创建内购项目,最后再把内购发起的相关代码直接写上就完事了。但是,在实际开发中,关于内购的坑却多的让人抓狂,比如常见的掉单,重复充值等情形。有的用户反馈我明明扣过钱了,为何没有充值?有的却是用户付了一次款,却给冲了两次+的额度,无论哪种情况,对开发者而言都是灾难,一方面是用户体验,一方面是公司亏损。
接下来我先讲两种常见的流程和思路,以及列举将会出现的问题,最后再来讲一下我自己开发的流程和应对策略
1. 验证放在客户端
1.1 流程
1.判断是否允许内购
2.根据商品id请求商品的具体信息
3.商品信息请求成功,创建支付交易,发起支付,之后就走的是系统提供的观察者
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
4.如果支付不成功,结束交易
5.支付成功,结束交易,获取凭证,本地进行验证(具体验证方法,我这里不再贴出代码,网上有一大堆)
6.等待校验结果,成功则调取充值接口,否则不充值
1.2 可能存在的问题
看了上述流程,我们会发现,前面的还好,验证的过程却完全不可控,比如,校验失败,再比如校验回来了,充值接口却没有来得及调用,或者充值接口还没回来等,用户退出了界面或者app。由于在支付成功的时候就结束了交易,这个时候若是校验结果没有返回或者校验结果回来了充值没有顺利进行,那么这个单子就丢了,对用户而言这就是损失。再有,放在客户端有个问题,凭证可以伪造,如果有人截取请求,伪造凭证,那就可以肆无忌惮地进行充值,对公司而言这是损失,所以这种方案不用想肯定错漏百出。
2.支付在客户端,校验和充值在服务端
基于第一种方案,第二种方案是要好一些的,而且我下面提到的第三种也是在第二种的基础上改进的。
2.1 流程
1.判断是否允许内购
2.根据商品id请求商品的具体信息
3.商品信息请求成功,创建支付交易,发起支付,之后就走的是系统提供的观察者
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
4.支付失败,结束交易
5.支付成功,取出凭证上传给服务端,服务端进行验证并充值,这些步骤完成后返回给客户端,客户端结束交易。
这么做看似没有什么毛病,如果按照正常流程的话的确是对的。但是都是有意外的,出现意外的点:
- 服务端接到的凭证还有可能是伪造的
- 服务端验证是和苹果服务器打交道,不可能那么快,如果这个时候用户退出了app,那么交易就没有结束,下次启动依然会执行上传凭证的后续操作,由于每次获取的凭证和时间戳有关,所以服务端在判断凭证重复性上来说也是不安全的,这就可能造成重复充值的情况
3.本文要介绍的方案,基于第二种方案的改进
由于要对交易进行判重,如果凭借苹果那边返回的数据进行判断,就等着被坑死吧,网上一开始以为没有问题的applicationUserName也可能抽风给你来个nil,而且在查账的时候根本查无可查,所以最好的做法是判断逻辑都用我们自己的数据,下面我列举一下我的流程,所有的操作都从点击购买按钮开始:
3.1 流程
向服务器请求创建订单接口,这个订单是服务器那边生成的,用来确定每次请求订单的唯一性,也是用来对账的标识。
进入内购支付流程
根据商品ID请求商品信息
商品信息请求成功,发起交易
支付没有成功,结束交易
支付成功,先判断交易的transactionIdentifier是否存在于本地,如果存在,则此项交易已经无效,直接结束交易,如果不存在,则走验证逻辑
验证逻辑:取出沙盒中的凭证,将凭证上传给服务器,要带上之前生成的订单id,服务器接收失败,什么也不做。接收成功,将交易的transactionIdentifier存到本地同时结束交易,后面的验证都在服务器了。
服务器验证过程:服务器会维护一个订单id列表,此订单id的初始状态为未验证,当接收到客户端传的凭证后,将此订单id状态置为验证中。如果验证成功,将订单id状态设置为无效,同时前去充值,充值结果返回给客户端。
有人会问,客户端如果这个时候退出了呢?这个也没关系,因为从客户端上传凭证给服务端成功后,就没有客户端什么事儿了,所有的校验和验证,充值逻辑都有服务端把控。
做的好一点的,为了让用户知晓充值成功,服务端在充值完成后,可以定向给用户发一条推送通知。同时客户端上传凭证成功后,给用户一个充值延迟到账的提示。
下面模拟几种意外情况,由于支付工具类做的是单例,这里只模拟退出app的情况,不用再模拟退出支付页面的情况:
1 用户在上传凭证中退出app,这个很好理解,什么也不会发生,交易依然存在于苹果服务器缓存中,用户下次启动界面,会从缓存中查询是否有未完成交易,如果有,会继续执行支付成功后的逻辑,不用担心掉单的情形
2 上传凭证成功了,如果服务端验证失败呢?这个可不在客户端控制范围内,而且客户端已经做了结束交易和移除交易id的操作,那么有没有可能丢单呢?下面我具体说一下:
前面讲过,服务端有一个订单id标识每一笔支付交易,接收了凭证后也会立即根据订单id存起来,如果验证失败,服务器会用定时器做轮询验证操作,只要是有效的订单id,就会去进行验证,前面已经知道,唯有校验成功,订单id才会无效,所以这保证了凭证一定会去验证,而且最终一定会验证通过,这样就极大地减少了丢单的概率,而由于服务端存在严谨的判断逻辑,所以重复充值的情况在正常逻辑下是不会存在的(排除有用户恶意退款的情况,这个也非重复充值,而是另外一种情况,由于苹果恶心的保密机制,我们无法获取到用户退款的详情,所以也就无法应对这种恶意行为)
总结
结束语:总体而言就这些,由于代码涉及到具体的业务和接口,我这里只用语言进行陈述,有描述的不到位的情况,还请在留言区指出,有更好的思路的也请在留言区交流。内购这块一直是一个巨坑,我很希望有朝一日能彻底填平,这样对大家都好。