美文网首页iOS小知识点好东西
内购支付踩过的坑以及自己的解决途径

内购支付踩过的坑以及自己的解决途径

作者: 皮乐皮儿 | 来源:发表于2017-07-14 13:29 被阅读2155次

更新:经过这几天的用户反馈及自己的查找,发现了一些问题。首先,在添加观察者之前是获取不到未完成订单的,只有在观察者的updateTransaction方法中才能获取到,所以,我和服务端同事联调做了如下调整:

上个版本做的内购支付,在内购封装方法中有过初步介绍和整理,结果在版本上线后收到用户的反馈说是支付成功,但是充值账户却不能到账,结果引发了退款等恶性问题,下面就我在实际项目中遇到的问题以及解决方案给出详细的介绍(上述给出的链接是swift版本的,由于笔者项目依旧是OC语言,所以下面依旧以OC语言来介绍)

1.封装的内购工具一定要设置为单例模式,且在程序启动的时候初始化并在初始化中设置观察者模式

笔者上个版本中虽说封装了内购支付工具,但是由于经验缺乏,内购工具只在支付页面中有效,结果有一个巨大的坑,用户可能在支付完成之前就退出了支付页面,导致了支付成功但是却没有充值成功的情形,在检查代码之后,我将内购支付工具做成了单例,而且,这个单例的初始化放在了程序入口处,这一点要说明的是,为什么放到入口处呢?是因为放到这里,如果之前有未移除的订单,可以在这里做一些逻辑处理,因为项目及实际情况,笔者是这样处理的:

这个方法不能奏效,移除不用,此思路就是错的

- (void)removeOldTransaction {

/*
    NSArray *tansactions = [SKPaymentQueue defaultQueue].transactions;
    //如果没有移除过订单信息
    BOOL result = NO;
    
    if ( ![kUserDefaults boolForKey:@"hasFinishOldTransaction"] && tansactions.count > 0) {
        for (SKPaymentTransaction *transaction in tansactions) {
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
        }
        result = YES;
    }
    [kUserDefaults setBool:YES forKey:@"hasFinishOldTransaction"];
    if (result) {
        return;
    }
*/
}

+ (instancetype)sharedInstance {

    static YGIAPTool *tool;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        tool = [[YGIAPTool alloc] init];
    });
    return  tool;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
       // [self removeOldTransaction];移除不用
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
    return self;
}

为什么要移除掉旧的订单呢?因为我之前的错误逻辑,导致一些订单就算支付成功而且成功充值,也没有移除订单,这个时候如果设置了观察者,苹果提供的系统API中会自动去查询有没有未移除的订单,这样就会继续执行充值逻辑,可能会造成重复充值的情形,为了避免这种情况带来的损失,笔者就只能硬性要求在版本升级后启动时移除旧的订单,这样就不会有这种隐忧了。

更新:此处描述有误,硬性移除订单是不可取的,会给用户造成一定的损失,这里只需要指定updateTranscation方法,按照正确逻辑走就可以了

didFinishLaunching中调用初始化方法 [YGIAPTool sharedInstance];

更新,关于何时移除订单的问题,之前想着本地存取凭证可以管理订单,后来偶然间发现,尽管是同一个订单,如果有未完成的,每次启动app,执行到updateTransaction方法后,走到Purchased状态后,取出的凭证都是不一样的,而交易的transactionIdentifier是一样的,所以在订单移除的问题上做了一些调整,首先,本地不用管理凭证,因为管理也没有用。因为业务需求,我们不再存储凭证,而是存储交易id,每次判断本地是否有交易id,如果某一条交易已经有交易id了,就记录到服务端,方便以后对账。这个时候结束交易我们选择放到了充值成功,也就是success之中,同时移除掉本地存储的交易id。

2.关于何时移除订单的问题

我之前搜索过相关的问题,网上给出的答案大都是在充值业务成功之后再移除订单,这个也有一定的问题,主要的就是网络问题或者是用户在充值完成之前就退出或者意外中断的时候引发的问题,这些情况下都会造成订单不能及时移除,给支付体验和充值风险上带来一定的问题。那么,怎么解决这种情况呢?当然,我所提供的方案也只是相对自己遇到的问题上有所改善,至于全面而深入的方案,有知道的大神麻烦指点一下,不胜感激。

我们都知道,如果在客户端去处理验证凭证的逻辑,很容易被有心人入侵做手脚,这个时候常用的保险做法就是客户端将本次交易产生的凭证发给服务端,让服务端去和苹果服务器验证,在一定程度上能够保证了安全性,那么这样也有一个隐忧,万一我传给服务端了,但是服务端验证失败了呢?或者万一由于网络问题传送失败呢?这个时候再加一层保险,就是客户端在传递给服务端之前先将本凭证存储下来(关于存储方法,笔者在后面会介绍,这里也有),然后服务器验证成功,返回到我们的success回调中去移除本地凭证,而相对应的服务端也已经存储了我们的凭证,当然考虑到服务器验证失败的问题,这个逻辑就要在服务端处理,笔者这里简单说下:就是服务器接到客户端传的凭证后,也是先存下来,直到验证成功并充值完成后才移除,否则就定时去发送验证,知道成功为止。
服务端不多做介绍,主要还是客户端逻辑,在移除本地凭证后,如果服务端正常处理,那么充值就应该到位了。

3.关于存储凭证的坑

笔者一开始存储用的是NSUserDefault方法,在每次支付成功后都会存储凭证到本地,然后在服务器验证成功后,将本地存储的凭证清空。这样看似乎没有毛病,但是如果用户频繁操作,会导致创建两次或者更多次订单,那么问题来了,NSUserDefault只能覆盖(因为存储的凭证对应的key是同一个),这样会造成只能保留最后一个存储的凭证,会产生一些意想不到的支付问题,所以在得知这个之后,笔者改成了用数据库存储到本地,这样我就可以在验证成功后根据当前凭证去删除数据库中的数据,而且还有一个好处是,如果凭证发送失败,在合适的地点我可以遍历数据库中的凭证,然后进行凭证验证,这样用户支付过的订单就很难出现充值不对等的问题(到账延迟问题是必然的,这个不知道有什么好方法没)

4.关于观察者方法updatedTransactions对应状态的处理问题。

SKPaymentTransactionStatePurchased:充值成功

SKPaymentTransactionStateFailed:充值失败

SKPaymentTransactionStateRestored:恢复内购

SKPaymentTransactionStatePurchasing:正在采购

对于这四种状态对应的处理情况,我这里简单介绍一下:
正在采购:只要添加订单,第一步就会走到这里,这里可以不作处理,要注意的是千万不能在这里移除订单,否则会崩溃,提示不能再采购状态移除订单。

至于恢复内购,笔者倒没有遇到,不过这里主要进行以下操作

- (void)removeTransaction {

    [[SKPaymentQueue defaultQueue] finishTransaction:self.currentTransaction];
}

只需要移除订单就好了

充值失败:毋庸置疑,这时候订单交易失败,就是废订单了,所以同样要移除

充值成功:能进入到这里,说明用户支付成功,钱已经扣掉了,那么它之后的相关处理就比较重要了,为了说明清晰,笔者用代码来展示:

更新

- (void)requestValidReceipt:(SKPaymentTransaction *)transaction {
    
    self.currentTransaction = transaction;

    //交易验证
    NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData = [NSData dataWithContentsOfURL:recepitURL];
    
    if(!receiptData){
        [kWindow showLoadingView:@"获取支付凭证为空"];
        return;
    }
    //转化为base64字符串
    NSString *receiptString= [receiptData base64EncodedStringWithOptions:0];;
    NSString *source = @"";
    if ([YGDataBase isReceiptExists:self.currentTransaction.transactionIdentifier]) {
        self.buyId = [YGDataBase getBuyIdWithReceipt:self.currentTransaction.transactionIdentifier];
        source = @"self.buyId = [YGDataBase getBuyIdWithReceipt:receiptString];";
    }else {
        source = @"购买界面";
        [self buySuccess];
        //1.先将交易id存起来
        [YGDataBase saveReceiptAndGoodsID:self.currentTransaction.transactionIdentifier goodId:self.buyId];
    }
    [self startValidReceipt:receiptString source:source];

    //2.传给服务端凭证数据
    [kWindow showLoadingView];
    [[YGNetWorkTool sharedInstance] ApplePayReceiptVerifyBuyId:self.buyId buyType:1 receipt:receiptString success:^(id responseObj) {
        [kWindow hideLoadingView];
        if ([responseObj[@"code"] intValue] != 200 ) {
            [kWindow showLoadingView:responseObj[@"msg"]];
        }else {//充值成功之后将凭证移除
             [self removeTransaction];
            [YGDataBase removeReceipt:self.currentTransaction.transactionIdentifier];
        }
        if (self.transactionSuccess) {
            self.transactionSuccess(self.currentTransaction);
        }
        [self showAlert];
        self.buyId = nil;
       
        
    } failure:^(NSError *error) {
        [kWindow hideLoadingView];
        if (self.transactionSuccess) {
            self.transactionSuccess(self.currentTransaction);
        }
        self.buyId = nil;
    }];

}

- (void)requestValidReceipt:(SKPaymentTransaction *)transaction {
    
    self.currentTransaction = transaction;

    //获取交易的凭证
    NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData = [NSData dataWithContentsOfURL:recepitURL];
    
    if(!receiptData){
        [kWindow showLoadingView:@"获取支付凭证为空"];
        return;
    }
    //转化为base64字符串
    NSString *receiptString= [receiptData base64EncodedStringWithOptions:0];
    //判断本地是否已经有过这个凭证,如果有,为了避免重复交易,什么也不做(这个可能没什么用,不过为了财政安全和保险,加上也不错)
    if ([YGDataBase isReceiptExists:receiptString]) {
        return;
    }

    [self buySuccess];//这个不用管,是项目中的统计作用

    //1.先将凭证存起来
    [YGDataBase saveReceiptAndGoodsID:receiptString goodId:self.ID];
//移除当前支付的交易
    [self removeTransaction];
//统计日志
    [self startValidReceipt:receiptString];
    
    //2.传给服务端凭证数据
    [kWindow showLoadingView];
    [[YGNetWorkTool sharedInstance] ApplePayReceiptVerifyBuyId:self.ID buyType:1 receipt:receiptString success:^(id responseObj) {
        [kWindow hideLoadingView];
        if ([responseObj[@"code"] intValue] != 200 ) {
            [kWindow showLoadingView:responseObj[@"msg"]];
        }else {//充值成功之后将凭证移除 这一点要注意,一定是服务端返回200的时候才能将本地凭证移除,否则会造成支付后没到账的丢单问题
            
            [YGDataBase removeReceipt:receiptString];
        }
        if (self.transactionSuccess) {
            self.transactionSuccess(self.currentTransaction);
        }
        [self showAlert];
        self.ID = nil;
        
    } failure:^(NSError *error) {
        [kWindow hideLoadingView];
        if (self.transactionSuccess) {
            self.transactionSuccess(self.currentTransaction);
        }
        self.ID = nil;
    }];

}

按照这个逻辑走下来,一般的内购支付问题应该能够解决了,笔者也是花了两天的时间,反复验证测试,将各种可能出现的奇葩操作都测试了一遍,结果充值都能够正常进行,希望能够给有需要的童鞋一些帮助,有需要源码的同学,可以到我的github上查看相关的逻辑(里面附带的一些牵扯到公司业务,笔者有做了详细的注释),喜欢的可以给个赞或者✨星哦

写在最后:由于苹果官方给出的验证方法非常简单,网上相关的内购资料也大都基于官方文档,许多实际问题根本找不到方法,希望大家能多多分享些这方面的实际问题,为以后内购的开发提供便利。

相关文章

  • 内购支付踩过的坑以及自己的解决途径

    更新:经过这几天的用户反馈及自己的查找,发现了一些问题。首先,在添加观察者之前是获取不到未完成订单的,只有在观察者...

  • Flutter 接入iOS苹果内购支付踩坑过程

    如何配置内购商品 坑1:项目与价格配置 苹果内购支付和我们平时接入支付宝或者微信支付有很大的差别。 苹果内购支付的...

  • applePay

    # iOS应用内支付(内购)的个人开发过程及坑!

  • 从业多年,谈谈 iOS 内购以及踩过的坑

    什么是内购? 一般来说,开发者刚接触到内购,都会遇到流程不清楚、踩坑、千头万绪。如何一次性搞定内购问题?首先,我们...

  • 【笔记】一些已解决问题的方法汇总

    记录一下平时解决过的问题以及参考文章,避免踩过的坑重复踩。- -↓ ↓ ↓ windows下vue.js开发环...

  • 面向省时间工作

    理论上,组里其他同学踩过的坑,自己不该跌进去。至于为啥知道组里其他人踩过,以及别人如何解决的,这个要看自己了。

  • 苹果内购踩的坑

    内购官方文档 https://developer.apple.com/library/archive/docume...

  • PHP中的数据类型

    一说到数据类型,这个坑就太多了,多到有哪些坑,有多少坑,不知道自己还会踩哪些坑,以及踩过的坑还会不会再踩,我对...

  • 网购踩过的坑

    1.因为贪图便宜,只听人家秒杀,把衣服说的天花乱坠,然后就抢抢抢,结果回来倒是花的冤枉钱,因为都是一些便宜没用的东...

  • 支付宝发起支付sdk2.0(已填坑)

    本人负责公司的支付系统,因此少不了和第三方支付的对接。踩过的坑也不计其数,为了让更少的人踩坑,故记下。 支付的第一...

网友评论

  • 68ffb1c88190:请问这个问题你遇到过吗:如果用户是第一次支付,需要去绑定银行卡或者支付宝什么的,绑定成功后再继续支付,扣款成功后客户端不知道什么问题并没有收到SKPaymentTransactionStatePurchased回调,造成掉单,这个问题怎么避免呢?
    皮乐皮儿:@守护欧拉贡的呀咪 不会啊,只要你在退出之前创建了交易,就算退到后台去也是会支付成功的。还有就是交易失败直接移除交易就行了,这个对用户也没啥损失,你可以多测测交易过程中退出或者其他各种奇葩操作,看看都会有啥情况,我当初测试的时候试了很多情况,都是成功的
    68ffb1c88190:@顾语流年 是单例,每次应用启动时(didFinishLaunchingWithOptions)就会注册观察者;我用demo测试了一下,支付时跳转到应用外后,过几分钟在支付,再次打开APP收到的是支付失败,不清楚是不是苹果的bug:sob:
    皮乐皮儿:@守护欧拉贡的呀咪 首先你要看下你的内购功能的工具是否是单例模式,而且在程序入扣函数中要对单例进行初始化,如果你的内购不是全局可用的,那么在支付过程中退出购买页面就可能造成支付成功,但是回调不执行的情况,导致无法继续后面的验证凭证而掉单。如果你的内购工具是单例模式在整个app生命周期都可用,这个时候如果用户支付成功了,是一定会执行那个观察者方法的,至于你说的没有收到回调,可能支付失败,如果支付确定成功了,那么你的交易记录只要没有验证完凭证,下次启动的时候,你的程序会自动去检测你是否有未完成订单,从来对掉单的问题进行再次处理(未完成订单指的是没有经过验证的订单)。只要你的代码写的正确,内购工具是单例,这个回调在支付结果之后一般都会执行的

本文标题:内购支付踩过的坑以及自己的解决途径

本文链接:https://www.haomeiwen.com/subject/lrinhxtx.html