首先,在我上一次发的这篇文章里https://www.jianshu.com/p/d2b3f297ba9e 存在几个容易出现问题的点,在实际的运营中确实也出现过,所以针对出现的问题也做了一下优化,希望对大家有帮助。
问题一
在前一篇内购的代码中,在实际运营中,出现过获取不到平台订单号,也就是applicationUsername为nil的情况 ,这种情况是因为苹果返回参数为nil导致的漏单,所以说这个参数并不是完全的可靠。所以,有必要在这个参数返回为nil 的时候做一下处理。首先有两种方案:
一、可以在本地进行存储,先去判断苹果返回的这个applicationUsername是否为nil 如果为nil,去判断传入的这个平台订单号是否有值,如果有,直接使用这个值(一般走购买流程,这个值都是有的即可,如果是走的漏单流程,applicationUsername 为nil 且此时是不传入平台订单号的,这时候就应该在发起购买的之后存储这个订单号,具体存储可以将拉起购买传进来的平台order作为值,然后transaction.transactionIdentifier作为key(注:实测当凭证为key 存储的话,在有订阅类型商品的收会出现同一个商品不同时段凭证不一样的情况,导致取不到存储的order,因此通过transaction.transactionIdentifier(唯一事务id)来进行存储,这个实测确保是唯一的)存储到某个目录下,文件名可以用:平台订单号.plist的格式保存。所以在下次走漏单的流程时候苹果返回applicatonUsername为nil 的 ,可以去遍历存储的plist,然后将键为该凭证的值取出来,这就是与购买凭证匹配的平台订单号了。
二、第二种方法需要跟服务端交互 ,也就是在拉起内购的时候,在获取到凭证的时候,将凭证和传进来的平台订单号一起post到你们家后台,当在获取苹果返回的applicationUsername为nil 的时候 ,通过凭证去后台将这个平台订单号取回来就可以了。第二种方法需要进行两部交互有点繁琐。
问题二
在此前的文章中的这一段代码,在实际运营中出现过因为订单号为nil 而无法删除已经成功的凭证,不停向服务器发起访问的问题,部分代码如下:
-(void)checkIAPFiles:(SKPaymentTransaction *)transaction{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
NSArray *cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:[SandBoxHelper iapReceiptPath] error:&error];
if (error == nil) {
for (NSString *name in cacheFileNameArray) {
if ([name hasSuffix:@".plist"]){
//如果有plist后缀的文件,说明就是存储的购买凭证
NSString *filePath = [NSString stringWithFormat:@"%@/%@", [SandBoxHelper iapReceiptPath], name];
[self sendAppStoreRequestBuyPlist:filePath trans:transaction];
}
}
} else {
[RRHUD hide];
}
}
#pragma mark -- 根据订单号来移除本地凭证的方法
-(void)successConsumptionOfGoodsWithOrder:(NSString * )cpOrder{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError * error;
if ([fileManager fileExistsAtPath:[SandBoxHelper iapReceiptPath]]) {
NSArray * cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:[SandBoxHelper iapReceiptPath] error:&error];
if (error == nil) {
for (NSString * name in cacheFileNameArray) {
NSString * filePath = [NSString stringWithFormat:@"%@/%@", [SandBoxHelper iapReceiptPath], name];
[self removeReceiptWithPlistPath:filePath ByCpOrder:cpOrder];
}
}
}
}
#pragma mark -- 根据订单号来删除 存储的凭证
-(void)removeReceiptWithPlistPath:(NSString *)plistPath ByCpOrder:(NSString *)cpOrder{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError * error;
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:plistPath];
NSString * order = [dic objectForKey:@"order"];
if ([cpOrder isEqualToString:order]) {
//移除与游戏cp订单号一样的plist 文件
BOOL ifRemove = [fileManager removeItemAtPath:plistPath error:&error];
if (ifRemove) {
NSLog(@"成功订单移除成功");
}else{
NSLog(@"成功订单移除失败");
}
}else{
NSLog(@"本地无与之匹配的订单");
}
}
以上代码的话会在获取不到订单号的时候删除不了本地的已经成功的订单。会导致 -(void)checkIAPFiles:(SKPaymentTransaction *)transaction这个方法始终判断这个目录下有文件。在购买的时候不停地向服务端发起访问,加剧服务器压力。
针对以上的问题,我简化了一些流程,考虑到苹果内购有独有的漏单操作。其实不必要存储凭证也可以实现补单流程 。优化代码我贴出来,具体关于applicationUsername为nil 的处理,大家可以根据需求从中选择一个来进行处理 。至于文件我也会更新到github ,链接请看评论,有需要大家可以下载使用。至于该文件中的一些block 或者你们看不懂的回调其实跟我的业务有关,因为我是写SDK给别人用的所以要回调购买的结果给研发做判断进行发货操作。不关乎你们的内购逻辑。一下是优化之后的内购代码 :
IPAPurchase.h
IPAPurchase.h
#import
/**
block
@param isSuccess 是否支付成功
@param certificate 支付成功得到的凭证(用于在 自己服务器验证)
@param errorMsg 错误信息
*/
typedef void(^PayResult)(BOOL isSuccess,NSString *certificate,NSString *errorMsg);
@interface IPAPurchase : NSObject
@property (nonatomic, copy)PayResult payResultBlock;
内购支付
@param productID 内购商品ID
@param payResult 结果
*/
-(void)buyProductWithProductID:(NSString *)productID payResult:(PayResult)payResult;
@end
IPAPurchase.m
// IPAPurchase.m
// iOS_Purchase
// Created by zhanfeng on 2017/6/6.
// Copyright © 2017年 unlock_liujia. All rights reserved.
#import "IPAPurchase.h"
#import "ULSDKConfig.h"
#import
#import
static NSString * const receiptKey = @"receipt_key";
dispatch_queue_t iap_queue(){
static dispatch_queue_t as_iap_queue;
static dispatch_once_t onceToken_iap_queue;
dispatch_once(&onceToken_iap_queue, ^{
as_iap_queue = dispatch_queue_create("com.iap.queue", DISPATCH_QUEUE_CONCURRENT);
});
return as_iap_queue;
}
@interface IPAPurchase()
{
SKProductsRequest *request;
}
//购买凭证
@property (nonatomic,copy)NSString *receipt;//存储base64编码的交易凭证
//产品ID
@property (nonnull,copy)NSString * profductId;
@end
static IPAPurchase * manager = nil;
@implementation IPAPurchase
#pragma mark -- 单例方法
+ (instancetype)manager{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!manager) {
manager = [[IPAPurchase alloc] init];
}
});
return manager;
}
#pragma mark -- 添加内购监听者
-(void)startManager{
dispatch_sync(iap_queue(), ^{
[[SKPaymentQueue defaultQueue] addTransactionObserver:manager];
});
}
#pragma mark -- 移除内购监听者
-(void)stopManager{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
});
}
#pragma mark -- 发起购买的方法
-(void)buyProductWithProductID:(NSString *)productID payResult:(PayResult)payResult{
[self removeAllUncompleteTransactionsBeforeNewPurchase];
self.payResultBlock = payResult;
[RRHUD showWithContainerView:RR_keyWindow status:NSLocalizedString(@"Buying...", @"")];
self.profductId = productID;
if (!self.profductId.length) {
UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"Warm prompt" message:@"There is no corresponding product." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
[alertView show];
}
if ([SKPaymentQueue canMakePayments]) {
[self requestProductInfo:self.profductId];
}else{
UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"Warm prompt" message:@"Please turn on the in-app paid purchase function first." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
[alertView show];
}
}
#pragma mark -- 结束上次未完成的交易
-(void)removeAllUncompleteTransactionsBeforeNewPurchase{
NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
if (transactions.count >= 1) {
for (NSInteger count = transactions.count; count > 0; count--) {
SKPaymentTransaction* transaction = [transactions objectAtIndex:count-1];
if (transaction.transactionState == SKPaymentTransactionStatePurchased||transaction.transactionState == SKPaymentTransactionStateRestored) {
[[SKPaymentQueue defaultQueue]finishTransaction:transaction];
}
}
}else{
NSLog(@"没有历史未消耗订单");
}
}
#pragma mark -- 发起购买请求
-(void)requestProductInfo:(NSString *)productID{
NSArray * productArray = [[NSArray alloc]initWithObjects:productID,nil];
NSSet * IDSet = [NSSet setWithArray:productArray];
request = [[SKProductsRequest alloc]initWithProductIdentifiers:IDSet];
request.delegate = self;
[request start];
}
#pragma mark -- SKProductsRequestDelegate 查询成功后的回调
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSArray *myProduct = response.products;
if (myProduct.count == 0) {
[RRHUD hide];
[RRHUD showErrorWithContainerView:UL_rootVC.view status:NSLocalizedString(@"No Product Info", @"")];
if (self.payResultBlock) {
self.payResultBlock(NO, nil, @"无法获取产品信息,购买失败");
}
return;
}
SKProduct * product = nil;
for(SKProduct * pro in myProduct){
NSLog(@"SKProduct 描述信息%@", [pro description]);
NSLog(@"产品标题 %@" , pro.localizedTitle);
NSLog(@"产品描述信息: %@" , pro.localizedDescription);
NSLog(@"价格: %@" , pro.price);
NSLog(@"Product id: %@" , pro.productIdentifier);
if ([pro.productIdentifier isEqualToString:self.profductId]) {
product = pro;
break;
}
}
if (product) {
SKMutablePayment * payment = [SKMutablePayment paymentWithProduct:product];
//使用苹果提供的属性,将平台订单号复制给这个属性作为透传参数
payment.applicationUsername = self.order;
[[SKPaymentQueue defaultQueue] addPayment:payment];
}else{
NSLog(@"没有此商品信息");
}
}
//查询失败后的回调
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
if (self.payResultBlock) {
self.payResultBlock(NO, nil, [error localizedDescription]);
}
}
//如果没有设置监听购买结果将直接跳至反馈结束;
-(void)requestDidFinish:(SKRequest *)request{
}
#pragma mark -- 监听结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
//当用户购买的操作有结果时,就会触发下面的回调函数,
for (SKPaymentTransaction * transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased:{
[self completeTransaction:transaction];
}break;
case SKPaymentTransactionStateFailed:{
[self failedTransaction:transaction];
}break;
case SKPaymentTransactionStateRestored:{//已经购买过该商品
[self restoreTransaction:transaction];
}break;
case SKPaymentTransactionStatePurchasing:{
NSLog(@"正在购买中...");
}break;
case SKPaymentTransactionStateDeferred:{
NSLog(@"最终状态未确定");
}break;
default:
break;
}
}
}
//完成交易
#pragma mark -- 交易完成的回调
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
[self getAndSaveReceipt:transaction]; //获取交易成功后的购买凭证
}
#pragma mark -- 处理交易失败回调
- (void)failedTransaction:(SKPaymentTransaction *)transaction{
[RRHUD hide];
NSString * error = nil;
if(transaction.error.code != SKErrorPaymentCancelled) {
[RRHUD showInfoWithContainerView:UL_rootVC.view status:NSLocalizedString(@"Buy Failed", @"")];
error = [NSString stringWithFormat:@"%ld",transaction.error.code];
} else {
[RRHUD showInfoWithContainerView:UL_rootVC.view status:NSLocalizedString(@"Buy Canceled", @"")];
error = [NSString stringWithFormat:@"%ld",transaction.error.code];
}
if (self.payResultBlock) {
self.payResultBlock(NO, nil, error);
}
[[SKPaymentQueue defaultQueue]finishTransaction:transaction];
}
- (void)restoreTransaction:(SKPaymentTransaction *)transaction{
[RRHUD hide];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
#pragma mark -- 获取购买凭证
-(void)getAndSaveReceipt:(SKPaymentTransaction *)transaction{
//获取交易凭证
NSURL * receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
NSData * receiptData = [NSData dataWithContentsOfURL:receiptUrl];
NSString * base64String = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
//初始化字典
NSMutableDictionary * dic = [[NSMutableDictionary alloc]init];
NSString * order = transaction.payment.applicationUsername;
//如果这个返回为nil
NSLog(@"后台订单号为订单号为%@",order);
[dic setValue: base64String forKey:receiptKey];
[dic setValue: order forKey:@"order"];
[dic setValue:[self getCurrentZoneTime] forKey:@"time"];
NSString * userId;
if (self.userid) {
userId = self.userid;
[[NSUserDefaults standardUserDefaults]setObject:userId forKey:@"unlock_iap_userId"];
}else{
userId = [[NSUserDefaults standardUserDefaults]
objectForKey:@"unlock_iap_userId"];
}
if (userId == nil||[userId length] == 0) {
userId = @"走漏单流程未传入userId";
}
if (order == nil||[order length] == 0) {
order = @"苹果返回透传参数为nil";
}
[[ULSDKAPI shareAPI]sendLineWithPayOrder:order UserId:userId Receipt:base64String LineNumber:@"IPAPurchase.m 337"];
NSString *fileName = [NSString UUID];
NSString *savedPath = [NSString stringWithFormat:@"%@/%@.plist", [SandBoxHelper iapReceiptPath], fileName];
[dic setValue: userId forKey:@"user_id"];
//这个存储成功与否其实无关紧要
BOOL ifWriteSuccess = [dic writeToFile:savedPath atomically:YES];
if (ifWriteSuccess){
NSLog(@"购买凭据存储成功!");
}else{
NSLog(@"购买凭据存储失败");
}
[self sendAppStoreRequestBuyWithReceipt:base64String userId:userId paltFormOrder:order trans:transaction];
}
-(void)getPlatformAmountInfoWithOrder:(NSString *)transOrcer{
[[ULSDKAPI shareAPI]getPlatformAmountWithOrder:transOrcer success:^(id responseObject) {
if (RequestSuccess) {
_platformAmount = [[responseObject objectForKey:@"data"]objectForKey:@"amount"];
_amount_type = [[responseObject objectForKey:@"data"]objectForKey:@"amount_type"];
_third_goods_id = [[responseObject objectForKey:@"data"]objectForKey:@"third_goods_id"];
[FBSDKAppEvents logEvent:@"pay_in_sdk" valueToSum:[_platformAmount doubleValue] parameters:@{@"fb_currency":@"USD",@"amount":_platformAmount,@"amount_type":_amount_type,@"third_goods_id":_third_goods_id}];
}else{
NSLog(@"%@",[responseObject objectForKey:@"message"]);
}
} failure:^(NSString *failure) {
}];
}
#pragma mark -- 存储成功订单
-(void)SaveIapSuccessReceiptDataWithReceipt:(NSString *)receipt Order:(NSString *)order UserId:(NSString *)userId{
NSMutableDictionary * mdic = [[NSMutableDictionary alloc]init];
[mdic setValue:[self getCurrentZoneTime] forKey:@"time"];
[mdic setValue: order forKey:@"order"];
[mdic setValue: userId forKey:@"userid"];
[mdic setValue: receipt forKey:receiptKey];
NSString *fileName = [NSString UUID];
NSString * successReceiptPath = [NSString stringWithFormat:@"%@/%@.plist", [SandBoxHelper SuccessIapPath], fileName];
//存储购买成功的凭证
[self insertReceiptWithReceiptByReceipt:receipt withDic:mdic inReceiptPath:successReceiptPath];
}
-(void)insertReceiptWithReceiptByReceipt:(NSString *)receipt withDic:(NSDictionary *)dic inReceiptPath:(NSString *)receiptfilePath{
BOOL isContain = NO;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError * error;
NSArray * cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:[SandBoxHelper SuccessIapPath] error:&error];
if (cacheFileNameArray.count == 0) {
[dic writeToFile:receiptfilePath atomically:YES];
if ([dic writeToFile:receiptfilePath atomically:YES]) {
NSLog(@"写入购买凭据成功");
}
}else{
if (error == nil) {
for (NSString * name in cacheFileNameArray) {
NSString * filePath = [NSString stringWithFormat:@"%@/%@", [SandBoxHelper SuccessIapPath], name];
NSMutableDictionary *localdic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];
if ([localdic.allValues containsObject:receipt]) {
isContain = YES;
}else{
continue;
}
}
}else{
NSLog(@"读取本文存储凭据失败");
}
}
if (isContain == NO) {
BOOL results = [dic writeToFile:receiptfilePath atomically:YES];
if (results) {
NSLog(@"写入凭证成功");
}else{
NSLog(@"写入凭证失败");
}
}else{
NSLog(@"已经存在凭证请勿重复写入");
}
}
#pragma mark -- 获取系统时间的方法
-(NSString *)getCurrentZoneTime{
NSDate * date = [NSDate date];
NSDateFormatter*formatter = [[NSDateFormatter alloc]init];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
NSString*dateTime = [formatter stringFromDate:date];
return dateTime;
}
#pragma mark -- 去服务器验证购买
-(void)sendAppStoreRequestBuyWithReceipt:(NSString *)receipt userId:(NSString *)userId paltFormOrder:(NSString * )order trans:(SKPaymentTransaction *)transaction{
[[ULSDKAPI shareAPI]sendLineWithPayOrder:order UserId:userId Receipt:receipt LineNumber:@"IPAPurchase.m 474"];
#pragma mark -- 发送信息去验证是否成功
[[ULSDKAPI shareAPI] sendVertifyWithReceipt:receipt order:order userId:userId success:^(ULSDKAPI *api, id responseObject) {
if (RequestSuccess) {
[RRHUD hide];
[RRHUD showSuccessWithContainerView:UL_rootVC.view status:NSLocalizedString(@"Buy Success", @"")];
//结束交易方法
[[SKPaymentQueue defaultQueue]finishTransaction:transaction];
[self getPlatformAmountInfoWithOrder:order];
NSData * data = [NSData dataWithContentsOfFile:[[[NSBundle mainBundle] appStoreReceiptURL] path]];
NSString *result = [data base64EncodedStringWithOptions:0];
if (self.payResultBlock) {
self.payResultBlock(YES, result, nil);
}
//这里将成功但存储起来
[self SaveIapSuccessReceiptDataWithReceipt:receipt Order:order UserId:userId];
[self successConsumptionOfGoodsWithReceipt:receipt];
}else{
#pragma mark -- callBack 回调
[api sendVertifyWithReceipt:receipt order:order userId:userId success:^(ULSDKAPI *api, id responseObject) {
if (RequestSuccess) {
[RRHUD hide];
[RRHUD showSuccessWithContainerView:UL_rootVC.view status:NSLocalizedString(@"Buy Success", @"")];
[[SKPaymentQueue defaultQueue]finishTransaction:transaction];
[self getPlatformAmountInfoWithOrder:order];
NSData *data = [NSData dataWithContentsOfFile:[[[NSBundle mainBundle] appStoreReceiptURL] path]];
NSString *result = [data base64EncodedStringWithOptions:0];
if (self.payResultBlock) {
self.payResultBlock(YES, result, nil);
}
//存储成功订单
[self SaveIapSuccessReceiptDataWithReceipt:receipt Order:order UserId:userId];
//删除已成功订单
[self successConsumptionOfGoodsWithReceipt:receipt];
}
#pragma 校验发货失败 1
} failure:^(ULSDKAPI *api, NSString *failure) {
[RRHUD hide];
[RRHUD showErrorWithContainerView:UL_rootVC.view status:NSLocalizedString(@"Request Error", @"")];
}];
}else{
[RRHUD hide];
#pragma mark --发送错误报告
[api sendFailureReoprtWithReceipt:receipt order:order success:^(ULSDKAPI *api, id responseObject) {
} failure:^(ULSDKAPI *api, NSString *failure) {
[RRHUD hide];
}];
}
} failure:^(ULSDKAPI *api, NSString *failure) {
[RRHUD hide];
}];
}
} failure:^(ULSDKAPI *api, NSString *failure) {
[RRHUD hide];
[api VertfyFailedRePostWithUserId:userId Order:order jsonStr:failure];
}];
}
#pragma mark -- 根据订单号来移除本地凭证的方法
-(void)successConsumptionOfGoodsWithReceipt:(NSString * )receipt{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError * error;
if ([fileManager fileExistsAtPath:[SandBoxHelper iapReceiptPath]]) {
NSArray * cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:[SandBoxHelper iapReceiptPath] error:&error];
if (error == nil) {
for (NSString * name in cacheFileNameArray) {
NSString * filePath = [NSString stringWithFormat:@"%@/%@", [SandBoxHelper iapReceiptPath], name];
[self removeReceiptWithPlistPath:filePath ByReceipt:receipt];
}
}
}
}
#pragma mark -- 根据订单号来删除 存储的凭证
-(void)removeReceiptWithPlistPath:(NSString *)plistPath ByReceipt:(NSString *)receipt{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError * error;
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:plistPath];
NSString * localReceipt = [dic objectForKey:@"receipt_key"];
//通过凭证进行对比
if ([receipt isEqualToString:localReceipt]) {
BOOL ifRemove = [fileManager removeItemAtPath:plistPath error:&error];
if (ifRemove) {
NSLog(@"成功订单移除成功");
}else{
NSLog(@"成功订单移除失败");
}
}else{
NSLog(@"本地无与之匹配的订单");
}
}
@end
代码可能粘贴不齐,细节调整之后和最新的单例类内购文件已经同步到GitHub 了,如有需要可以去gitHud 直接下载源文件,建议自己揣摩清楚之后再参考或者引入自己的项目,之所谓知其所以也要知其所以然。另外在-(void)getAndSaveReceipt:(SKPaymentTransaction *)transaction;方法里面没有对applicationUsername为nil 做处理,大家可以跟我说的哪两种思路进行解决,我在这里就不代码演示了。大家多动脑~ github链接如下:https://github.com/jiajiaailaras/ULIPAPurchase