iOS内购-收据验证以及漏单情况的处理

Apple官方收据验证编程指南

=================1.先说下验证方式==============

IOS 内购支付两种模式:

  1. 内置模式

  2. 服务器模式

内置模式的流程:
1.app从app store 获取产品信息
2.用户选择需要购买的产品
3.app发送支付请求到app store
4.app store 处理支付请求,并返回transaction信息
5.app将购买的内容展示给用户

内置模式可以这样进行本地验单
//本地验证
- (void)localPaymentTransactionVerify:(NSString *)environment body:(NSData *)postData transaction:(SKPaymentTransaction *)transaction{
    NSURL *StoreURL = nil;
    if ([environment isEqualToString:@"environment=Sandbox"]) {
        StoreURL = [[NSURL alloc] initWithString: ITMS_SANDBOX_VERIFY_RECEIPT_URL];
    }else {
        StoreURL = [[NSURL alloc] initWithString: ITMS_PRODUCT_VERIFY_RECEIPT_URL];
    }
    NSLog(@"运行环境是--- %@", StoreURL);
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:StoreURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:30.0];
    [request setHTTPMethod:@"POST"];
    [request setHTTPBody:postData];
    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    NSString *product = transaction.payment.productIdentifier;
    
    NSLog(@"transaction.payment.productIdentifier++++-----%@",product);
    
    if ([product length] > 0)
    {
        NSArray *tt = [product componentsSeparatedByString:@"."];
        
        NSString *bookid = [tt lastObject];
        
        if([bookid length] > 0)
        {
            
            NSLog(@"打印bookid------%@",bookid);
        }
    }
    //在此做交易记录
    // Remove the transaction from the payment queue.
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
#pragma mark connection delegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    
    //进行二次验证;
    // NSLog(@" 以下是HTTP协议的监听,若由服务器验证,可不用这段代码%@",  [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]);
    NSLog(@" 以下是HTTP协议的监听,若由服务器验证,可不用这段代码%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
    NSDictionary * dic=[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
    
    
    NSNumber *status=dic[@"status"];
    NSDictionary * receiptDic=dic[@"receipt"];
    //    NSString *purchased=receiptDic[@"product_id"];
    NSArray * tempArr=receiptDic[@"in_app"];
    
    NSString * purchased=nil;
    for (int i=0 ; i<tempArr.count; i++) {
        NSDictionary * tempPurchase=tempArr[i];
        purchased=tempPurchase[@"product_id"];
    }
    if (status.intValue==0) {
        // 发送通知更改账户V豆的数量;
//        [[NSNotificationCenter defaultCenter] postNotificationName:kProductPurchasedNotification object:purchased];
    }
    else
    {
        NSLog(@"收据校验失败");
        
        switch (status.intValue) {
            case 21000:
                NSLog(@"App Store不能读取你提供的JSON对象");
                break;
            case 21002:
                NSLog(@"receipt-data域的数据有问题");
                break;
            case 21003:
                NSLog(@"receipt无法通过验证");
                break;
            case 21004:
                NSLog(@"提供的shared secret不匹配你账号中的shared secret");
                break;
            case 21005:
                NSLog(@"receipt服务器当前不可用");
                break;
            case 21006:
                NSLog(@"receipt合法,但是订阅已过期。服务器接收到这个状态码时,receipt数据仍然会解码并一起发送");
                break;
            case 21007:
                NSLog(@"receipt是Sandbox receipt,但却发送至生产系统的验证服务");
                break;
            case 21008:
                NSLog(@"receipt是生产receipt,但却发送至Sandbox环境的验证服务");
                break;
            default:
                break;
        }
    }
    NSLog(@"%@",dic[@"status"]);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
    
    //    NSLog(@" 以下是HTTP协议的监听,若由服务器验证,可不用这段代码");
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
    NSLog(@" 以下是HTTP协议的监听,若由服务器验证,可不用这段代码");
    NSLog(@"%@+++",response);
    switch([(NSHTTPURLResponse *)response statusCode]) {
            
        case 200:
            NSLog(@"200------");
            break;
        case 206:
            NSLog(@"206------");
            break;
        case 304:
            NSLog(@"304------");
            break;
        case 400:
            NSLog(@"400------");
            break;
        case 404:
            NSLog(@"404------");
            break;
        case 416:
            NSLog(@"416------");
            break;
        case 403:
            NSLog(@"403------");
            break;
        case 401:
            NSLog(@"401------");
        case 500:
            NSLog(@"500------");
            break;
        default:
            break;
    }
}

服务器模式的流程:
*******最重要的一点:在确认服务端收到receipt之前不要结束订单(不要调用[[SKPaymentQueue defaultQueue] finishTransaction:transaction];)******
1.app从服务器获取产品标识列表
2.app从app store 获取产品信息
3.用户选择需要购买的产品
4.app 发送 支付请求到app store
5.app store 处理支付请求,返回transaction信息
6.app 将transaction receipt 发送到服务器
7.服务器收到收据后发送到app stroe验证收据的有效性
8.app store 返回收据的验证结果
9.根据app store 返回的结果决定用户是否购买成功


服务器验证这样处理---在下面这个交易结束方法里进行服务器验证;就不需要上面本地内置模式的代码块了
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
    NSLog(@"交易结束");
    NSString *productID =  transaction.payment.productIdentifier;
    //验证购买结果
    if (productID.length > 0) {
        //向自己的服务器验证购买凭证
        //最好将返回的数据转换成 base64再传给后台,后台再转换回来;以防返回字符串中有特字符传给后台显示空
        NSString *result=[[NSString alloc]initWithData:transaction.transactionReceipt encoding:NSUTF8StringEncoding];
        NSLog(@"---transactionReceipt:%@", result);
        NSString *environment = [self environmentForReceipt:result];
        // appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址
        NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
        // 从沙盒中获取到购买凭据
        NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
        NSString *encodeStr = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
        
        NSString *sendString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", encodeStr];
        NSLog(@"_____%@",sendString);
        
        //******这个二进制数据由服务器进行验证*****************************
        NSData *postData = [NSData dataWithBytes:[sendString UTF8String] length:[sendString length]];
        //在这里进行服务器验证
        
        
        //******本地验证***********************************************
        [self localPaymentTransactionVerify:environment body:postData transaction:transaction];
        
        //结束交易(收到服务器的验证之后再调用此方法---避免造成漏单)
//        [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
        
    }
}

上述两种模式的不同之处主要在于:交易的收据验证,内建模式没有专门去验证交易收据,而服务器模式会使用独立的服务器去验证交易收据。内建模式简单快捷,但容易被破解。服务器模式流程相对复杂,但相对安全。

=================2.再说下关于漏单的处理==============

官方产考文档:https://developer.apple.com/library/content/technotes/tn2413/_index.html#//apple_ref/doc/uid/DTS40016228-CH1-TNTAG1

*****最重要的一点:在确认服务端收到receipt之前不要结束订单(不要调用[[SKPaymentQueue defaultQueue] finishTransaction:transaction];)

漏单:正常玩家购买了却没有收到物品、且自己的服务端没有任何记录iOS的订单。iOS的补单是非常麻烦的,用户提供支付的截图中的订单号我们又不能在itunes 或者其他地方找到相应的订单号。

服务端需要处理一个receipt中携带了多个未处理的订单,即在in-app中有多个支付记录。 
因为虽然按正常逻辑,一次只会处理一笔支付,在漏掉以前充值订单的情况下,一个receipt,可能含有多个购买记录,这些记录可能就是没有下发给用户的,需要对receipt 的 in-app记录逐条检查,根据订单记录查看某一单是否已经下发过了。


如果 in_app 里面值为空.看下这个:https://forums.developer.apple.com/thread/8954

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

推荐阅读更多精彩内容