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

更新:经过这几天的用户反馈及自己的查找,发现了一些问题。首先,在添加观察者之前是获取不到未完成订单的,只有在观察者的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上查看相关的逻辑(里面附带的一些牵扯到公司业务,笔者有做了详细的注释),喜欢的可以给个赞或者✨星哦

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 《非银行支付机构网络支付业务管理办法》条款释义 - 中国支付网 - 中国支付行业第一门户网站2016年7月1日...
    菜菜苔阅读 7,495评论 1 44
  • 最近开发一个项目涉及到内购, 也遇到过一些问题. 这里拿出来分享一下, 避免一些人走弯路.开头先聊一聊最近苹果关于...
    东方_未明阅读 6,177评论 16 56
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 九月份的时候到公司,开始制作一个游戏的sdk,包括了登录注册,初始化,信息收集等功能... 这些相对来说简单些,最...
    DovYoung阅读 2,697评论 5 7
  • 【读经】 箴言5 【金句】 恐怕将你的尊荣给别人,将你的岁月给残忍的人;(箴言 5:9 和合本) 【感动】 经文讲...
    chanor阅读 607评论 0 0