Files
peko-ios/YuMi/Modules/YMMine/View/Recharge/IAPManager.m

744 lines
26 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// IAPManager.m
// YuMi
//
// Created by P on 2024/9/24.
//
#import "IAPManager.h"
#import <Bugly/Bugly.h>
#import "Api+Mine.h"
#import "YuMi-swift.h"
#import "RechargeStorage.h"
#define MAX_RETRY_COUNT 50
@interface IAPManager()
@property (nonatomic, assign) BOOL isLogin;
@property (nonatomic, assign) NSInteger recheckInterval;
@property (nonatomic, assign) NSInteger recheckIndex;
@property (nonatomic, strong) NSTimer *recheckTimer;
@property (nonatomic, assign) BOOL isProcessing;
@property (nonatomic, copy) NSString *orderID;
@property (nonatomic, copy) NSString *transactionID;
@property (nonatomic, copy) void(^successPurchase)(NSString *transactionID, NSString *orderID);
@property (nonatomic, copy) void(^successRecheck)(void);
@property (nonatomic, copy) void(^failurePurchase)(NSError *error);
@property (nonatomic, copy) void(^contactCustomerService)(NSString *uid);
// 记录交易重试次数
@property (nonatomic, strong) NSMutableDictionary *retryCountDict;
// 线程安全队列
@property (nonatomic, strong) dispatch_queue_t retryCountQueue;
@end
@implementation IAPManager
+ (instancetype)sharedManager {
static IAPManager *proxy;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
proxy = [[self alloc] init];
proxy.recheckIndex = 0;
proxy.recheckInterval = 1.0;
proxy.retryCountDict = [NSMutableDictionary dictionary];
proxy.retryCountQueue = dispatch_queue_create("com.iapmanager.retrycount.queue", DISPATCH_QUEUE_CONCURRENT);
proxy.isProcessing = NO;
proxy.isLogin = NO;
});
return proxy;
}
- (void)handleLogin {
self.isLogin = YES;
}
- (void)handleLogout {
self.isLogin = NO;
if (self.recheckTimer) {
[self.recheckTimer invalidate];
}
}
- (NSString *)fetchEncodedReceipt {
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
if (!receiptData) return nil;
return [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
}
- (BOOL)isValidTransactionID:(NSString *)tID orderID:(NSString *)orderID {
return (tID.length > 0 && orderID.length > 0);
}
// 开始正常内购流程
- (void)purchase:(NSString *)productId
success:(void(^)(NSString *transactionID, NSString *orderID))success
failure:(void(^)(NSError *error))failure
contactCS:(void(^)(NSString *uid))contactCS {
self.successPurchase = success;
self.failurePurchase = failure;
self.contactCustomerService = contactCS;
[self handleIAPState];
[self requestAPPOrderData:productId isFroRecheck:NO];
}
// 定时轮训未完成的订单并进行验单
- (void)retryCheckAllReceipt {
// 先清理旧定时器
if (self.recheckTimer) {
[self.recheckTimer invalidate];
self.recheckTimer = nil;
}
// 清理过期交易
[self cleanupStaleTransactions];
// 设置最大重试间隔
NSTimeInterval interval = MIN(self.recheckInterval, 300.0);
// 使用传统定时器创建方式避免在旧版iOS上的兼容性问题
self.recheckTimer = [NSTimer scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(handleRetryCheckReceipt)
userInfo:nil
repeats:NO];
// 确保定时器在主运行循环中运行
[[NSRunLoop mainRunLoop] addTimer:self.recheckTimer forMode:NSRunLoopCommonModes];
}
// 验单逻辑
- (void)handleRetryCheckReceipt {
NSLog(@"[YuMi IAP] 用户触发补单检查 - Retry checking receipts");
// 如果已经在处理中或未登录,则不执行,直接等待下次定时器触发
if (self.isProcessing || !self.isLogin) {
// 只销毁当前定时器,不递归创建新定时器,避免无限循环
if (self.recheckTimer) {
[self.recheckTimer invalidate];
self.recheckTimer = nil;
}
// 重新安排下一次检查(延迟更长时间,防止频繁空转)
self.recheckInterval = MIN(self.recheckInterval * 1.5, 300.0);
[self retryCheckAllReceipt];
return;
}
NSArray *array = [RechargeStorage getAllReceiptsWithUid:[AccountInfoStorage instance].getUid];
NSLog(@" ------------.------------ 尝试:%@", array);
// 如果没有需要处理的收据,定时器继续以较长间隔轮询
if (array.count == 0) {
NSLog(@" ------------.------------ 没有需要验单的数据");
if (self.recheckTimer) {
[self.recheckTimer invalidate];
self.recheckTimer = nil;
}
// 设为较长间隔如10分钟
self.recheckInterval = 600.0;
[self retryCheckAllReceipt];
return;
}
// 确保索引在有效范围内
if (self.recheckIndex >= array.count) {
self.recheckIndex = 0;
}
// 标记为处理中
self.isProcessing = YES;
// 安全地获取当前需要处理的收据
NSDictionary *dic = [array xpSafeObjectAtIndex:self.recheckIndex];
NSString *transactionId = dic[@"transactionId"];
NSString *orderId = dic[@"orderId"];
// 记录尝试验单
[self _logToBugly:transactionId oID:orderId status:0];
// 检查重试次数
NSInteger retryCount = [self getRetryCountForTransaction:transactionId];
if (retryCount > MAX_RETRY_COUNT) {
// 超过最大重试次数,记录并清理
[self _logToBugly:transactionId oID:orderId status:4]; // 新状态:重试次数过多
[RechargeStorage delegateTransactionId:transactionId uid:[AccountInfoStorage instance].getUid];
[self removeRetryCountForTransaction:transactionId];
// 重置处理状态
self.isProcessing = NO;
// 增加索引并安排下一次检查
self.recheckIndex = (self.recheckIndex + 1) % array.count;
if (self.recheckTimer) {
[self.recheckTimer invalidate];
self.recheckTimer = nil;
}
[self retryCheckAllReceipt];
return;
}
// 增加重试计数
[self incrementRetryCountForTransaction:transactionId];
@kWeakify(self);
[self backgroundCheckReceiptWithTransactionID:transactionId
orderID:orderId
next:^(BOOL isSuccess){
@kStrongify(self);
if (isSuccess) {
// 成功处理
[RechargeStorage delegateTransactionId:transactionId
uid:[AccountInfoStorage instance].getUid];
// 成功后减少重试间隔,加快处理速度
self.recheckInterval = MAX(self.recheckInterval / 2, 1.0);
[self _logToBugly:transactionId oID:orderId status:1];
// 成功后移除重试记录
[self removeRetryCountForTransaction:transactionId];
} else {
// 失败后增加重试间隔,避免频繁请求
self.recheckInterval = MIN(self.recheckInterval * 1.5, 300.0);
[self _logToBugly:transactionId oID:orderId status:2];
}
// 增加索引,准备处理下一个收据
self.recheckIndex = (self.recheckIndex + 1) % array.count;
// 重置处理状态
self.isProcessing = NO;
// 安排下一次检查
if (self.recheckTimer) {
[self.recheckTimer invalidate];
self.recheckTimer = nil;
}
[self retryCheckAllReceipt];
}];
}
- (void)_logToBugly:(NSString *)tid oID:(NSString *)oid status:(NSInteger)status {
NSMutableDictionary *logDic = [NSMutableDictionary dictionary];
// 安全处理交易ID
if ([NSString isEmpty:tid]) {
[logDic setObject:@"" forKey:@"内购 transactionId"];
tid = @"";
} else {
[logDic setObject:tid forKey:@"内购 transactionId"];
}
// 安全处理订单ID
if ([NSString isEmpty:oid]) {
[logDic setObject:@"" forKey:@"内购 orderId"];
} else {
[logDic setObject:oid forKey:@"内购 orderId"];
}
// 安全获取用户ID
NSString *uid = [AccountInfoStorage instance].getUid ?: @"未知用户";
[logDic setObject:uid forKey:@"内购 用户id"];
// 添加重试次数信息
[logDic setObject:@([self getRetryCountForTransaction:tid]) forKey:@"重试次数"];
[logDic setObject:@(self.recheckInterval) forKey:@"重试间隔"];
[logDic setObject:@(self.recheckIndex) forKey:@"当前索引"];
[logDic setObject:@(self.isProcessing) forKey:@"处理中状态"];
[logDic setObject:@(self.isLogin) forKey:@"登录状态"];
NSString *statusMsg = @"";
NSInteger code = -20000;
switch (status) {
case 0:
statusMsg = [NSString stringWithFormat:@"UID: %@, 尝试验单", uid];
break;
case 1:
statusMsg = [NSString stringWithFormat:@"UID: %@, 验单-补单成功", uid];
code = -20001;
break;
case 2:
statusMsg = [NSString stringWithFormat:@"UID: %@, 验单-补单失败", uid];
code = -20002;
break;
case 3:
statusMsg = [NSString stringWithFormat:@"UID: %@, 验单-补单 id 异常", uid];
code = -20002;
break;
case 4:
statusMsg = [NSString stringWithFormat:@"UID: %@, 重试次数过多", uid];
code = -20003;
break;
case 5:
statusMsg = [NSString stringWithFormat:@"UID: %@, 过期交易清理", uid];
code = -20004;
break;
default:
break;
}
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[Bugly reportError:[NSError errorWithDomain:statusMsg
code:code
userInfo:logDic]];
});
}
// 内购成功并通知外部
- (void)handleSuccessPurchase:(NSString *)tID order:(NSString *)orderID {
if (self.successPurchase) {
@kWeakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@kStrongify(self);
self.successPurchase(tID, orderID);
});
}
if (self.successRecheck) {
@kWeakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@kStrongify(self);
self.successRecheck();
});
}
}
// 内购失败并通知外部
- (void)handleFailurePurchase:(NSString *)errorMsg {
if (self.failurePurchase) {
@kWeakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@kStrongify(self);
if (errorMsg.length == 0) {
self.failurePurchase(nil);
} else {
self.failurePurchase([NSError errorWithDomain:errorMsg code:-1 userInfo:nil]);
}
});
}
}
// 获取客服内容
- (void)handleContactCS {
@kWeakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
@kStrongify(self);
TTAlertConfig *config = [[TTAlertConfig alloc]init];
config.title = YMLocalizedString(@"XPIAPRechargeViewController7");
config.message = YMLocalizedString(@"XPIAPRechargeViewController8");
TTAlertButtonConfig *confirmButtonConfig = [[TTAlertButtonConfig alloc]init];
confirmButtonConfig.title = YMLocalizedString(@"XPIAPRechargeViewController9");
UIImage *image = [UIImage gradientColorImageFromColors:@[UIColorFromRGB(0x13E2F5),UIColorFromRGB(0x9DB4FF),UIColorFromRGB(0xCC67FF)] gradientType:GradientTypeLeftToRight imgSize:CGSizeMake(200, 200)];
confirmButtonConfig.backgroundColor = [UIColor colorWithPatternImage:image];
confirmButtonConfig.cornerRadius = 38/2;
config.confirmButtonConfig = confirmButtonConfig;
@kWeakify(self);
[TTPopup alertWithConfig:config confirmHandler:^{
@kStrongify(self);
[self loadCSUid];
} cancelHandler:^{}];
});
}
// 获取客服 UID
- (void)loadCSUid {
if (self.contactCustomerService) {
[Api requestContactCustomerServiceCompletion:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
if (code == 200) {
NSString *uid = [NSString stringWithFormat:@"%@",data.data];
self.contactCustomerService(uid);
}
}];
}
}
// 监听内购流程状态
- (void)handleIAPState {
if (@available(iOS 15.0, *)) {
@kWeakify(self);
[[PIIAPRegulate shared] setConditionBlock:^(enum StoreConditionResult state, NSDictionary<NSString *,NSString *> * _Nullable param) {
@kStrongify(self);
switch (state) {
case StoreConditionResultStart:
NSLog(@"~~~```~~~ Purchase started");
break;
case StoreConditionResultPay:
NSLog(@"~~~```~~~ Processing payment");
break;
case StoreConditionResultVerifiedServer:
NSLog(@"~~~```~~~ Verified by server");
[self handleIAPSuccess:param];
break;
case StoreConditionResultUserCancelled:
NSLog(@"~~~```~~~ User cancelled purchase");
[self handleFailurePurchase:@""];
break;
case StoreConditionResultNoProduct:
NSLog(@"~~~```~~~ No product found");
[self handleFailurePurchase:[NSString stringWithFormat:@"%@ No product found", YMLocalizedString(@"XPIAPRechargeViewController1")]];
break;
case StoreConditionResultFailedVerification:
NSLog(@"~~~```~~~ Verification failed");
[self handleFailurePurchase:YMLocalizedString(@"XPIAPRechargeViewController1")];
break;
case StoreConditionResultUnowned:
NSLog(@"~~~```~~~ Result Unowned");
[self handleFailurePurchase:YMLocalizedString(@"XPIAPRechargeViewController1")];
break;
default:
[self handleFailurePurchase:YMLocalizedString(@"XPIAPRechargeViewController0")];
break;
}
}];
}
}
// 生成后端订单
- (void)requestAPPOrderData:(NSString *)productId isFroRecheck:(BOOL)isFroRecheck {
if (@available(iOS 15.0, *)) {
@kWeakify(self);
[Api requestIAPRecharge:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
if (isFroRecheck) {
return;
}
@kStrongify(self);
if (code == 200) {
NSString *orderId = (NSString *)data.data[@"recordId"];
NSString *uuid = (NSString *)data.data[@"appAccountToken"];
[self requestIAPOrder:orderId
productID:productId
uuid:uuid];
} else if (code == 50000) {
[self handleContactCS];
[self handleFailurePurchase:@""];
} else {
[self handleFailurePurchase:msg];
}
}
chargeProdId:productId
uid:[AccountInfoStorage instance].getUid
ticket:[AccountInfoStorage instance].getTicket
deviceInfo:[YYUtility deviceID]
clientIp:[YYUtility ipAddress]];
} else {
[self handleFailurePurchase:YMLocalizedString(@"XPIAPRechargeViewController10")];
}
}
// 获取后端订单后,查询线上是否有对应 productID 的物品如果有会自动拉起付费弹窗PIIAPRegulate - purchase
- (void)requestIAPOrder:(NSString *)orderID productID:(NSString *)productID uuid:(NSString *)uuid {
self.orderID = orderID;
if (@available(iOS 15.0, *)) {
// @kWeakify(self);
[[PIIAPRegulate shared] demandCommodityThingWithProductId:productID
uuid:uuid
completionHandler:^(NSError * _Nullable error) {
// @kStrongify(self);
if (error) {
// 已在 ConditionBlock 中回调
}
}];
}
}
// 处理内购付款成功
- (void)handleIAPSuccess:(NSDictionary *)param {
id tid = param[@"transactionId"];
self.transactionID = tid;
if (self.transactionID.length == 0) {
[self handleFailurePurchase:YMLocalizedString(@"XPIAPRechargeViewController1")];
return;
}
[self saveTransactionID];
[self checkReceiptWithTransactionID:self.transactionID
orderID:self.orderID];
}
// 内购付款成功后保存收据 id 到钥匙串
- (void)saveTransactionID {
NSString *encodedReceipt = [self fetchEncodedReceipt];
NSMutableDictionary *receiptInfo = [NSMutableDictionary dictionary];
[self addValueIfNotNil:self.transactionID forKey:@"transactionId" toDictionary:receiptInfo];
[self addValueIfNotNil:encodedReceipt forKey:@"receipt" toDictionary:receiptInfo];
[self addValueIfNotNil:self.orderID forKey:@"orderId" toDictionary:receiptInfo];
// 添加时间戳便于后续判断过期交易
[receiptInfo setObject:[[NSDate date] description] forKey:@"timestamp"];
@synchronized (self.transactionID) {
[RechargeStorage saveTransactionId:self.transactionID
receipt:[receiptInfo toJSONString]
uid:[AccountInfoStorage instance].getUid];
}
}
// 通过苹果收据与后端订单进行验单与发货
// MARK: 必须等待结果
- (void)checkReceiptWithTransactionID:(NSString *)tID
orderID:(NSString *)orderID {
NSLog(@" ------------.------------ 后端验单:%@ | %@", tID, orderID);
@kWeakify(self);
[Api checkReceipt:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
@kStrongify(self);
if (code == 200) {
[self handleSuccessPurchase:tID order:orderID];
[self handleCheckReceiptSuccess:tID isFromRecheck:NO];
} else {
[self handleFailurePurchase:msg];
if (code == 1444) {
// 参数异常,暂不处理
}
}
NSLog(@" ------------.------------ 后端验单结果:%@ ",msg);
}
chooseEnv:@"true"
chargeRecordId:orderID
transcationId:tID
uid:[AccountInfoStorage instance].getUid
ticket:[AccountInfoStorage instance].getTicket];
}
// 通过苹果收据与后端订单进行验单与发货,此方法是对本地未删除的订单进行检测
- (void)backgroundCheckReceiptWithTransactionID:(NSString *)tID
orderID:(NSString *)orderID
next:(void(^)(BOOL isSuccess))next {
// 验证参数有效性
if (![self isValidTransactionID:tID orderID:orderID]) {
if (next) {
next(NO);
}
return;
}
// 创建后台任务标识
UIBackgroundTaskIdentifier backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"VerifyReceipt"
expirationHandler:^{
// 超时处理
if (next) {
next(NO);
}
[[UIApplication sharedApplication] endBackgroundTask:backgroundTask];
}];
// 完成后务必结束后台任务
@kWeakify(self);
[Api checkReceipt:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) {
@kStrongify(self);
BOOL isSuccess = (code == 200);
if (isSuccess) {
[self handleCheckReceiptSuccess:tID isFromRecheck:YES];
NSLog(@" ------------.------------ 后台验单成功:%@ | %@", tID, orderID);
} else {
NSLog(@" ------------.------------ 后台验单失败:%@ | %@ | %@", tID, orderID, msg);
}
// 确保回调被调用
if (next) {
next(isSuccess);
}
// 结束后台任务
[[UIApplication sharedApplication] endBackgroundTask:backgroundTask];
}
chooseEnv:@"true"
chargeRecordId:orderID
transcationId:tID
uid:[AccountInfoStorage instance].getUid
ticket:[AccountInfoStorage instance].getTicket];
}
// 查找在缓存中的 transactionID 并告知 apple 订单已完成
- (void)handleCheckReceiptSuccess:(NSString *)tID
isFromRecheck:(BOOL)isFromRecheck {
// 验证参数
if (tID.length == 0) {
NSLog(@" ------------.------------ apple 验单失败交易ID为空");
return;
}
if (@available(iOS 15.0, *)) {
@kWeakify(self);
[[PIIAPRegulate shared] verifyBusinessAccomplishWithTransactionID:tID
completionHandler:^(BOOL success, NSError * _Nullable error) {
@kStrongify(self);
if (success) {
NSLog(@" ------------.------------ apple 验单成功");
// 流程完成,移除本地缓存账单
[RechargeStorage delegateTransactionId:tID
uid:[AccountInfoStorage instance].getUid];
// 成功后移除重试记录
[self removeRetryCountForTransaction:tID];
} else {
// 出现异常
NSLog(@" ------------.------------ apple 验单失败:%@ ",error);
if (error == nil) {
// 该订单在 appstore 已无法找到,不必再重试
[RechargeStorage delegateTransactionId:tID
uid:[AccountInfoStorage instance].getUid];
// 清理不存在的交易记录
[self removeRetryCountForTransaction:tID];
} else {
// 只有在重试检查时才安排下一次重试
if (isFromRecheck) {
// 不立即重试,让定时器自然触发下一次
}
}
}
if (!isFromRecheck) {
// 正常购买情况,重置内存订单数据
self.orderID = @"";
self.transactionID = @"";
}
}];
}
}
- (void)addValueIfNotNil:(id)value
forKey:(NSString *)key
toDictionary:(NSMutableDictionary *)dictionary {
if (value != nil && key != nil) {
dictionary[key] = value;
}
}
// 添加 dealloc 方法确保定时器释放
- (void)dealloc {
if (self.recheckTimer) {
[self.recheckTimer invalidate];
self.recheckTimer = nil;
}
}
// 在应用进入后台时暂停定时器
- (void)applicationDidEnterBackground {
if (self.recheckTimer) {
[self.recheckTimer invalidate];
self.recheckTimer = nil;
}
}
// 在应用恢复活动时重新启动定时器
- (void)applicationWillEnterForeground {
if (self.isLogin) {
[self retryCheckAllReceipt];
}
}
// 清理长时间未处理的交易记录
- (void)cleanupStaleTransactions {
NSArray *receipts = [RechargeStorage getAllReceiptsWithUid:[AccountInfoStorage instance].getUid];
if (!receipts || receipts.count == 0) {
return;
}
NSDate *now = [NSDate date];
NSInteger cleanupDays = 70; // 70天后清理
for (NSDictionary *receipt in receipts) {
// 检查交易时间,如果超过指定天数还未成功,则清理
NSString *timestamp = receipt[@"timestamp"];
if (!timestamp || timestamp.length == 0) {
continue;
}
NSDate *transactionDate = nil;
// 尝试多种日期格式解析
NSArray *dateFormats = @[
@"yyyy-MM-dd HH:mm:ss Z",
@"yyyy-MM-dd'T'HH:mm:ssZ",
@"EEE MMM dd HH:mm:ss Z yyyy"
];
for (NSString *format in dateFormats) {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:format];
[dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
transactionDate = [dateFormatter dateFromString:timestamp];
if (transactionDate) {
break;
}
}
// 如果无法解析日期使用当前时间减去60天作为估计值
if (!transactionDate) {
transactionDate = [now dateByAddingTimeInterval:-60 * 24 * 60 * 60];
}
// 检查是否超过清理期限
if ([now timeIntervalSinceDate:transactionDate] > cleanupDays * 24 * 60 * 60) {
NSString *tID = receipt[@"transactionId"];
NSString *oID = receipt[@"orderId"];
// 记录清理日志
[self _logToBugly:tID oID:oID status:5]; // 状态5过期清理
// 从存储中删除
[RechargeStorage delegateTransactionId:tID uid:[AccountInfoStorage instance].getUid];
// 清理过期交易的重试记录
[self removeRetryCountForTransaction:tID];
}
}
}
// 获取交易的重试次数
- (NSInteger)getRetryCountForTransaction:(NSString *)transactionId {
__block NSInteger count = 0;
dispatch_sync(self.retryCountQueue, ^{
NSNumber *countNumber = self.retryCountDict[transactionId];
count = countNumber ? [countNumber integerValue] : 0;
});
return count;
}
// 增加交易的重试次数
- (void)incrementRetryCountForTransaction:(NSString *)transactionId {
dispatch_barrier_async(self.retryCountQueue, ^{
NSInteger currentCount = [self.retryCountDict[transactionId] integerValue];
self.retryCountDict[transactionId] = @(currentCount + 1);
});
}
// 移除交易的重试记录
- (void)removeRetryCountForTransaction:(NSString *)transactionId {
dispatch_barrier_async(self.retryCountQueue, ^{
[self.retryCountDict removeObjectForKey:transactionId];
});
}
// 清理所有重试记录
- (void)cleanAllRetryCount {
dispatch_barrier_async(self.retryCountQueue, ^{
[self.retryCountDict removeAllObjects];
});
}
@end