// // JXAnalytics.m // AICity // // Feature: 003-ios-api-https // Task: T062 - iOS数据上报客户端 // Created on 2025-10-14. // #import "JXAnalytics.h" #import "JXNetworkManager.h" #import #import // 通知名称 NSString * const JXAnalyticsStatusDidChangeNotification = @"JXAnalyticsStatusDidChangeNotification"; // 配置常量 static const NSInteger kMaxQueueSize = 1000; static const NSInteger kBatchSize = 50; static const NSTimeInterval kFlushInterval = 30.0; // 30秒 static const NSInteger kMaxRetries = 3; @implementation JXDeviceInfo @end @implementation JXAnalyticsEvent @end @implementation JXQueueStatus - (BOOL)isQueueFull { return self.queueSize >= self.maxQueueSize; } - (NSInteger)queueUsagePercent { if (self.maxQueueSize == 0) return 0; NSInteger percent = (self.queueSize * 100) / self.maxQueueSize; return MAX(0, MIN(100, percent)); } @end @interface JXAnalytics () @property (nonatomic, strong) NSMutableArray *eventQueue; @property (nonatomic, assign) JXReportingStatus reportingStatus; @property (nonatomic, strong) NSTimer *flushTimer; @property (nonatomic, strong) JXNetworkManager *networkManager; @property (nonatomic, strong) JXDeviceInfo *deviceInfo; @property (nonatomic, strong) dispatch_queue_t analyticsQueue; @property (nonatomic, strong) NSString *lastErrorMessage; @property (nonatomic, assign) NSInteger lastSuccessCount; @end @implementation JXAnalytics #pragma mark - Singleton + (instancetype)sharedAnalytics { static JXAnalytics *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; } - (instancetype)init { self = [super init]; if (self) { _eventQueue = [[NSMutableArray alloc] init]; _reportingStatus = JXReportingStatusIdle; _networkManager = [JXNetworkManager sharedManager]; _deviceInfo = [self createDeviceInfo]; _analyticsQueue = dispatch_queue_create("com.juxing.analytics", DISPATCH_QUEUE_SERIAL); [self startPeriodicFlush]; [self setupApplicationNotifications]; } return self; } - (void)dealloc { [self stopPeriodicFlush]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - Public Methods - (void)reportPlayStartWithDramaId:(NSString *)dramaId episodeId:(NSString *)episodeId { JXAnalyticsEvent *event = [[JXAnalyticsEvent alloc] init]; event.eventType = @"play_start"; event.dramaId = dramaId; event.episodeId = episodeId; event.playDuration = 0; event.isCompleted = NO; event.isMember = NO; event.deviceInfo = self.deviceInfo; event.timestamp = [[NSDate date] timeIntervalSince1970]; event.retryCount = 0; [self addEvent:event]; } - (void)reportPlayProgressWithDramaId:(NSString *)dramaId episodeId:(NSString *)episodeId playDuration:(NSInteger)playDuration { JXAnalyticsEvent *event = [[JXAnalyticsEvent alloc] init]; event.eventType = @"play_progress"; event.dramaId = dramaId; event.episodeId = episodeId; event.playDuration = playDuration; event.isCompleted = NO; event.isMember = NO; event.deviceInfo = self.deviceInfo; event.timestamp = [[NSDate date] timeIntervalSince1970]; event.retryCount = 0; [self addEvent:event]; } - (void)reportPlayCompleteWithDramaId:(NSString *)dramaId episodeId:(NSString *)episodeId playDuration:(NSInteger)playDuration isMember:(BOOL)isMember { JXAnalyticsEvent *event = [[JXAnalyticsEvent alloc] init]; event.eventType = @"play_complete"; event.dramaId = dramaId; event.episodeId = episodeId; event.playDuration = playDuration; event.isCompleted = YES; event.isMember = isMember; event.deviceInfo = self.deviceInfo; event.timestamp = [[NSDate date] timeIntervalSince1970]; event.retryCount = 0; [self addEvent:event]; } - (void)reportUserActionWithDramaId:(NSString *)dramaId episodeId:(NSString *)episodeId actionType:(NSString *)actionType { JXAnalyticsEvent *event = [[JXAnalyticsEvent alloc] init]; event.eventType = actionType; event.dramaId = dramaId; event.episodeId = episodeId; event.playDuration = 0; event.isCompleted = NO; event.isMember = NO; event.deviceInfo = self.deviceInfo; event.timestamp = [[NSDate date] timeIntervalSince1970]; event.retryCount = 0; [self addEvent:event]; } - (void)reportLikeWithDramaId:(NSString *)dramaId episodeId:(NSString *)episodeId { [self reportUserActionWithDramaId:dramaId episodeId:episodeId actionType:@"like"]; } - (void)reportFavoriteWithDramaId:(NSString *)dramaId episodeId:(NSString *)episodeId { [self reportUserActionWithDramaId:dramaId episodeId:episodeId actionType:@"favorite"]; } - (void)reportShareWithDramaId:(NSString *)dramaId episodeId:(NSString *)episodeId shareType:(NSString *)shareType { NSString *actionType = [NSString stringWithFormat:@"share_%@", shareType]; [self reportUserActionWithDramaId:dramaId episodeId:episodeId actionType:actionType]; } - (void)reportFollowWithDramaId:(NSString *)dramaId { [self reportUserActionWithDramaId:dramaId episodeId:@"" actionType:@"follow"]; } - (void)reportCommentWithDramaId:(NSString *)dramaId episodeId:(NSString *)episodeId { [self reportUserActionWithDramaId:dramaId episodeId:episodeId actionType:@"comment"]; } - (void)flushEvents { if (self.reportingStatus == JXReportingStatusReporting) { return; // 正在上报中,跳过 } dispatch_async(self.analyticsQueue, ^{ [self performFlush]; }); } - (JXQueueStatus *)getQueueStatus { __block JXQueueStatus *status; dispatch_sync(self.analyticsQueue, ^{ status = [[JXQueueStatus alloc] init]; status.queueSize = self.eventQueue.count; status.maxQueueSize = kMaxQueueSize; status.reportingStatus = self.reportingStatus; status.errorMessage = self.lastErrorMessage; status.lastSuccessCount = self.lastSuccessCount; }); return status; } - (void)clearQueue { dispatch_async(self.analyticsQueue, ^{ [self.eventQueue removeAllObjects]; self.reportingStatus = JXReportingStatusIdle; self.lastErrorMessage = nil; [self notifyStatusChange]; }); } - (void)addStatusObserver:(id)observer selector:(SEL)selector { [[NSNotificationCenter defaultCenter] addObserver:observer selector:selector name:JXAnalyticsStatusDidChangeNotification object:nil]; } - (void)removeStatusObserver:(id)observer { [[NSNotificationCenter defaultCenter] removeObserver:observer name:JXAnalyticsStatusDidChangeNotification object:nil]; } #pragma mark - Private Methods - (void)addEvent:(JXAnalyticsEvent *)event { dispatch_async(self.analyticsQueue, ^{ // 检查队列大小 if (self.eventQueue.count >= kMaxQueueSize) { // 队列满了,移除最旧的事件 [self.eventQueue removeObjectAtIndex:0]; } [self.eventQueue addObject:event]; // 如果队列达到批量大小,立即上报 if (self.eventQueue.count >= kBatchSize) { [self performFlush]; } }); } - (void)performFlush { if (self.eventQueue.count == 0) { return; } if (self.reportingStatus == JXReportingStatusReporting) { return; } self.reportingStatus = JXReportingStatusReporting; [self notifyStatusChange]; // 获取待上报的事件 NSInteger eventCount = MIN(kBatchSize, self.eventQueue.count); NSArray *eventsToReport = [self.eventQueue subarrayWithRange:NSMakeRange(0, eventCount)]; [self.eventQueue removeObjectsInRange:NSMakeRange(0, eventCount)]; // 构建上报数据 NSMutableArray *eventDataArray = [[NSMutableArray alloc] init]; for (JXAnalyticsEvent *event in eventsToReport) { NSDictionary *eventData = @{ @"event_type": event.eventType, @"jx_drama_id": event.dramaId, @"jx_episode_id": event.episodeId, @"play_duration": @(event.playDuration), @"is_completed": @(event.isCompleted), @"is_member": @(event.isMember), @"device_type": event.deviceInfo.deviceType, @"app_version": event.deviceInfo.appVersion }; [eventDataArray addObject:eventData]; } NSDictionary *requestData = @{ @"events": eventDataArray }; // 发送上报请求 [self.networkManager reportAnalyticsWithData:requestData completion:^(NSDictionary * _Nullable response, NSError * _Nullable error) { dispatch_async(self.analyticsQueue, ^{ if (error) { // 网络异常,将事件重新加入队列 [self handleReportFailure:eventsToReport error:error]; } else if (response && [response[@"code"] integerValue] == 0) { // 上报成功 [self handleReportSuccess:eventsToReport.count]; } else { // 服务器错误,将事件重新加入队列 NSString *message = response[@"message"] ?: @"服务器错误"; NSError *serverError = [NSError errorWithDomain:@"JXAnalytics" code:-1 userInfo:@{NSLocalizedDescriptionKey: message}]; [self handleReportFailure:eventsToReport error:serverError]; } }); }]; } - (void)handleReportSuccess:(NSInteger)eventCount { self.reportingStatus = JXReportingStatusSuccess; self.lastSuccessCount = eventCount; self.lastErrorMessage = nil; [self notifyStatusChange]; // 1秒后重置状态 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), self.analyticsQueue, ^{ self.reportingStatus = JXReportingStatusIdle; [self notifyStatusChange]; }); } - (void)handleReportFailure:(NSArray *)events error:(NSError *)error { self.reportingStatus = JXReportingStatusError; self.lastErrorMessage = error.localizedDescription; [self notifyStatusChange]; // 将失败的事件重新加入队列(如果重试次数未超限) for (JXAnalyticsEvent *event in events) { if (event.retryCount < kMaxRetries) { event.retryCount++; [self.eventQueue insertObject:event atIndex:0]; // 插入到队列前面,优先重试 } } // 1秒后重置状态 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), self.analyticsQueue, ^{ self.reportingStatus = JXReportingStatusIdle; [self notifyStatusChange]; }); } - (void)notifyStatusChange { dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:JXAnalyticsStatusDidChangeNotification object:self]; }); } - (void)startPeriodicFlush { [self stopPeriodicFlush]; self.flushTimer = [NSTimer scheduledTimerWithTimeInterval:kFlushInterval target:self selector:@selector(timerFlush) userInfo:nil repeats:YES]; } - (void)stopPeriodicFlush { if (self.flushTimer) { [self.flushTimer invalidate]; self.flushTimer = nil; } } - (void)timerFlush { [self flushEvents]; } - (void)setupApplicationNotifications { // 应用进入后台时立即上报 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil]; // 应用即将终止时立即上报 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate) name:UIApplicationWillTerminateNotification object:nil]; } - (void)applicationDidEnterBackground { [self flushEvents]; } - (void)applicationWillTerminate { // 同步上报,确保数据不丢失 dispatch_sync(self.analyticsQueue, ^{ [self performFlush]; }); } - (JXDeviceInfo *)createDeviceInfo { JXDeviceInfo *deviceInfo = [[JXDeviceInfo alloc] init]; deviceInfo.deviceType = @"ios"; // 设备型号 struct utsname systemInfo; uname(&systemInfo); deviceInfo.deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; // 系统版本 deviceInfo.osVersion = [NSString stringWithFormat:@"iOS %@", [[UIDevice currentDevice] systemVersion]]; // 应用版本 deviceInfo.appVersion = [self getAppVersion]; return deviceInfo; } - (NSString *)getAppVersion { NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary]; NSString *version = infoDictionary[@"CFBundleShortVersionString"]; return version ?: @"unknown"; } @end