| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- //
- // JXAnalytics.m
- // AICity
- //
- // Feature: 003-ios-api-https
- // Task: T062 - iOS数据上报客户端
- // Created on 2025-10-14.
- //
- #import "JXAnalytics.h"
- #import "JXNetworkManager.h"
- #import <UIKit/UIKit.h>
- #import <sys/utsname.h>
- // 通知名称
- 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<JXAnalyticsEvent *> *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<JXAnalyticsEvent *> *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<JXAnalyticsEvent *> *)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
|