JXAnalytics.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. //
  2. // JXAnalytics.m
  3. // AICity
  4. //
  5. // Feature: 003-ios-api-https
  6. // Task: T062 - iOS数据上报客户端
  7. // Created on 2025-10-14.
  8. //
  9. #import "JXAnalytics.h"
  10. #import "JXNetworkManager.h"
  11. #import <UIKit/UIKit.h>
  12. #import <sys/utsname.h>
  13. // 通知名称
  14. NSString * const JXAnalyticsStatusDidChangeNotification = @"JXAnalyticsStatusDidChangeNotification";
  15. // 配置常量
  16. static const NSInteger kMaxQueueSize = 1000;
  17. static const NSInteger kBatchSize = 50;
  18. static const NSTimeInterval kFlushInterval = 30.0; // 30秒
  19. static const NSInteger kMaxRetries = 3;
  20. @implementation JXDeviceInfo
  21. @end
  22. @implementation JXAnalyticsEvent
  23. @end
  24. @implementation JXQueueStatus
  25. - (BOOL)isQueueFull {
  26. return self.queueSize >= self.maxQueueSize;
  27. }
  28. - (NSInteger)queueUsagePercent {
  29. if (self.maxQueueSize == 0) return 0;
  30. NSInteger percent = (self.queueSize * 100) / self.maxQueueSize;
  31. return MAX(0, MIN(100, percent));
  32. }
  33. @end
  34. @interface JXAnalytics ()
  35. @property (nonatomic, strong) NSMutableArray<JXAnalyticsEvent *> *eventQueue;
  36. @property (nonatomic, assign) JXReportingStatus reportingStatus;
  37. @property (nonatomic, strong) NSTimer *flushTimer;
  38. @property (nonatomic, strong) JXNetworkManager *networkManager;
  39. @property (nonatomic, strong) JXDeviceInfo *deviceInfo;
  40. @property (nonatomic, strong) dispatch_queue_t analyticsQueue;
  41. @property (nonatomic, strong) NSString *lastErrorMessage;
  42. @property (nonatomic, assign) NSInteger lastSuccessCount;
  43. @end
  44. @implementation JXAnalytics
  45. #pragma mark - Singleton
  46. + (instancetype)sharedAnalytics {
  47. static JXAnalytics *sharedInstance = nil;
  48. static dispatch_once_t onceToken;
  49. dispatch_once(&onceToken, ^{
  50. sharedInstance = [[self alloc] init];
  51. });
  52. return sharedInstance;
  53. }
  54. - (instancetype)init {
  55. self = [super init];
  56. if (self) {
  57. _eventQueue = [[NSMutableArray alloc] init];
  58. _reportingStatus = JXReportingStatusIdle;
  59. _networkManager = [JXNetworkManager sharedManager];
  60. _deviceInfo = [self createDeviceInfo];
  61. _analyticsQueue = dispatch_queue_create("com.juxing.analytics", DISPATCH_QUEUE_SERIAL);
  62. [self startPeriodicFlush];
  63. [self setupApplicationNotifications];
  64. }
  65. return self;
  66. }
  67. - (void)dealloc {
  68. [self stopPeriodicFlush];
  69. [[NSNotificationCenter defaultCenter] removeObserver:self];
  70. }
  71. #pragma mark - Public Methods
  72. - (void)reportPlayStartWithDramaId:(NSString *)dramaId episodeId:(NSString *)episodeId {
  73. JXAnalyticsEvent *event = [[JXAnalyticsEvent alloc] init];
  74. event.eventType = @"play_start";
  75. event.dramaId = dramaId;
  76. event.episodeId = episodeId;
  77. event.playDuration = 0;
  78. event.isCompleted = NO;
  79. event.isMember = NO;
  80. event.deviceInfo = self.deviceInfo;
  81. event.timestamp = [[NSDate date] timeIntervalSince1970];
  82. event.retryCount = 0;
  83. [self addEvent:event];
  84. }
  85. - (void)reportPlayProgressWithDramaId:(NSString *)dramaId
  86. episodeId:(NSString *)episodeId
  87. playDuration:(NSInteger)playDuration {
  88. JXAnalyticsEvent *event = [[JXAnalyticsEvent alloc] init];
  89. event.eventType = @"play_progress";
  90. event.dramaId = dramaId;
  91. event.episodeId = episodeId;
  92. event.playDuration = playDuration;
  93. event.isCompleted = NO;
  94. event.isMember = NO;
  95. event.deviceInfo = self.deviceInfo;
  96. event.timestamp = [[NSDate date] timeIntervalSince1970];
  97. event.retryCount = 0;
  98. [self addEvent:event];
  99. }
  100. - (void)reportPlayCompleteWithDramaId:(NSString *)dramaId
  101. episodeId:(NSString *)episodeId
  102. playDuration:(NSInteger)playDuration
  103. isMember:(BOOL)isMember {
  104. JXAnalyticsEvent *event = [[JXAnalyticsEvent alloc] init];
  105. event.eventType = @"play_complete";
  106. event.dramaId = dramaId;
  107. event.episodeId = episodeId;
  108. event.playDuration = playDuration;
  109. event.isCompleted = YES;
  110. event.isMember = isMember;
  111. event.deviceInfo = self.deviceInfo;
  112. event.timestamp = [[NSDate date] timeIntervalSince1970];
  113. event.retryCount = 0;
  114. [self addEvent:event];
  115. }
  116. - (void)reportUserActionWithDramaId:(NSString *)dramaId
  117. episodeId:(NSString *)episodeId
  118. actionType:(NSString *)actionType {
  119. JXAnalyticsEvent *event = [[JXAnalyticsEvent alloc] init];
  120. event.eventType = actionType;
  121. event.dramaId = dramaId;
  122. event.episodeId = episodeId;
  123. event.playDuration = 0;
  124. event.isCompleted = NO;
  125. event.isMember = NO;
  126. event.deviceInfo = self.deviceInfo;
  127. event.timestamp = [[NSDate date] timeIntervalSince1970];
  128. event.retryCount = 0;
  129. [self addEvent:event];
  130. }
  131. - (void)reportLikeWithDramaId:(NSString *)dramaId episodeId:(NSString *)episodeId {
  132. [self reportUserActionWithDramaId:dramaId episodeId:episodeId actionType:@"like"];
  133. }
  134. - (void)reportFavoriteWithDramaId:(NSString *)dramaId episodeId:(NSString *)episodeId {
  135. [self reportUserActionWithDramaId:dramaId episodeId:episodeId actionType:@"favorite"];
  136. }
  137. - (void)reportShareWithDramaId:(NSString *)dramaId
  138. episodeId:(NSString *)episodeId
  139. shareType:(NSString *)shareType {
  140. NSString *actionType = [NSString stringWithFormat:@"share_%@", shareType];
  141. [self reportUserActionWithDramaId:dramaId episodeId:episodeId actionType:actionType];
  142. }
  143. - (void)reportFollowWithDramaId:(NSString *)dramaId {
  144. [self reportUserActionWithDramaId:dramaId episodeId:@"" actionType:@"follow"];
  145. }
  146. - (void)reportCommentWithDramaId:(NSString *)dramaId episodeId:(NSString *)episodeId {
  147. [self reportUserActionWithDramaId:dramaId episodeId:episodeId actionType:@"comment"];
  148. }
  149. - (void)flushEvents {
  150. if (self.reportingStatus == JXReportingStatusReporting) {
  151. return; // 正在上报中,跳过
  152. }
  153. dispatch_async(self.analyticsQueue, ^{
  154. [self performFlush];
  155. });
  156. }
  157. - (JXQueueStatus *)getQueueStatus {
  158. __block JXQueueStatus *status;
  159. dispatch_sync(self.analyticsQueue, ^{
  160. status = [[JXQueueStatus alloc] init];
  161. status.queueSize = self.eventQueue.count;
  162. status.maxQueueSize = kMaxQueueSize;
  163. status.reportingStatus = self.reportingStatus;
  164. status.errorMessage = self.lastErrorMessage;
  165. status.lastSuccessCount = self.lastSuccessCount;
  166. });
  167. return status;
  168. }
  169. - (void)clearQueue {
  170. dispatch_async(self.analyticsQueue, ^{
  171. [self.eventQueue removeAllObjects];
  172. self.reportingStatus = JXReportingStatusIdle;
  173. self.lastErrorMessage = nil;
  174. [self notifyStatusChange];
  175. });
  176. }
  177. - (void)addStatusObserver:(id)observer selector:(SEL)selector {
  178. [[NSNotificationCenter defaultCenter] addObserver:observer
  179. selector:selector
  180. name:JXAnalyticsStatusDidChangeNotification
  181. object:nil];
  182. }
  183. - (void)removeStatusObserver:(id)observer {
  184. [[NSNotificationCenter defaultCenter] removeObserver:observer
  185. name:JXAnalyticsStatusDidChangeNotification
  186. object:nil];
  187. }
  188. #pragma mark - Private Methods
  189. - (void)addEvent:(JXAnalyticsEvent *)event {
  190. dispatch_async(self.analyticsQueue, ^{
  191. // 检查队列大小
  192. if (self.eventQueue.count >= kMaxQueueSize) {
  193. // 队列满了,移除最旧的事件
  194. [self.eventQueue removeObjectAtIndex:0];
  195. }
  196. [self.eventQueue addObject:event];
  197. // 如果队列达到批量大小,立即上报
  198. if (self.eventQueue.count >= kBatchSize) {
  199. [self performFlush];
  200. }
  201. });
  202. }
  203. - (void)performFlush {
  204. if (self.eventQueue.count == 0) {
  205. return;
  206. }
  207. if (self.reportingStatus == JXReportingStatusReporting) {
  208. return;
  209. }
  210. self.reportingStatus = JXReportingStatusReporting;
  211. [self notifyStatusChange];
  212. // 获取待上报的事件
  213. NSInteger eventCount = MIN(kBatchSize, self.eventQueue.count);
  214. NSArray<JXAnalyticsEvent *> *eventsToReport = [self.eventQueue subarrayWithRange:NSMakeRange(0, eventCount)];
  215. [self.eventQueue removeObjectsInRange:NSMakeRange(0, eventCount)];
  216. // 构建上报数据
  217. NSMutableArray *eventDataArray = [[NSMutableArray alloc] init];
  218. for (JXAnalyticsEvent *event in eventsToReport) {
  219. NSDictionary *eventData = @{
  220. @"event_type": event.eventType,
  221. @"jx_drama_id": event.dramaId,
  222. @"jx_episode_id": event.episodeId,
  223. @"play_duration": @(event.playDuration),
  224. @"is_completed": @(event.isCompleted),
  225. @"is_member": @(event.isMember),
  226. @"device_type": event.deviceInfo.deviceType,
  227. @"app_version": event.deviceInfo.appVersion
  228. };
  229. [eventDataArray addObject:eventData];
  230. }
  231. NSDictionary *requestData = @{
  232. @"events": eventDataArray
  233. };
  234. // 发送上报请求
  235. [self.networkManager reportAnalyticsWithData:requestData completion:^(NSDictionary * _Nullable response, NSError * _Nullable error) {
  236. dispatch_async(self.analyticsQueue, ^{
  237. if (error) {
  238. // 网络异常,将事件重新加入队列
  239. [self handleReportFailure:eventsToReport error:error];
  240. } else if (response && [response[@"code"] integerValue] == 0) {
  241. // 上报成功
  242. [self handleReportSuccess:eventsToReport.count];
  243. } else {
  244. // 服务器错误,将事件重新加入队列
  245. NSString *message = response[@"message"] ?: @"服务器错误";
  246. NSError *serverError = [NSError errorWithDomain:@"JXAnalytics"
  247. code:-1
  248. userInfo:@{NSLocalizedDescriptionKey: message}];
  249. [self handleReportFailure:eventsToReport error:serverError];
  250. }
  251. });
  252. }];
  253. }
  254. - (void)handleReportSuccess:(NSInteger)eventCount {
  255. self.reportingStatus = JXReportingStatusSuccess;
  256. self.lastSuccessCount = eventCount;
  257. self.lastErrorMessage = nil;
  258. [self notifyStatusChange];
  259. // 1秒后重置状态
  260. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), self.analyticsQueue, ^{
  261. self.reportingStatus = JXReportingStatusIdle;
  262. [self notifyStatusChange];
  263. });
  264. }
  265. - (void)handleReportFailure:(NSArray<JXAnalyticsEvent *> *)events error:(NSError *)error {
  266. self.reportingStatus = JXReportingStatusError;
  267. self.lastErrorMessage = error.localizedDescription;
  268. [self notifyStatusChange];
  269. // 将失败的事件重新加入队列(如果重试次数未超限)
  270. for (JXAnalyticsEvent *event in events) {
  271. if (event.retryCount < kMaxRetries) {
  272. event.retryCount++;
  273. [self.eventQueue insertObject:event atIndex:0]; // 插入到队列前面,优先重试
  274. }
  275. }
  276. // 1秒后重置状态
  277. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), self.analyticsQueue, ^{
  278. self.reportingStatus = JXReportingStatusIdle;
  279. [self notifyStatusChange];
  280. });
  281. }
  282. - (void)notifyStatusChange {
  283. dispatch_async(dispatch_get_main_queue(), ^{
  284. [[NSNotificationCenter defaultCenter] postNotificationName:JXAnalyticsStatusDidChangeNotification
  285. object:self];
  286. });
  287. }
  288. - (void)startPeriodicFlush {
  289. [self stopPeriodicFlush];
  290. self.flushTimer = [NSTimer scheduledTimerWithTimeInterval:kFlushInterval
  291. target:self
  292. selector:@selector(timerFlush)
  293. userInfo:nil
  294. repeats:YES];
  295. }
  296. - (void)stopPeriodicFlush {
  297. if (self.flushTimer) {
  298. [self.flushTimer invalidate];
  299. self.flushTimer = nil;
  300. }
  301. }
  302. - (void)timerFlush {
  303. [self flushEvents];
  304. }
  305. - (void)setupApplicationNotifications {
  306. // 应用进入后台时立即上报
  307. [[NSNotificationCenter defaultCenter] addObserver:self
  308. selector:@selector(applicationDidEnterBackground)
  309. name:UIApplicationDidEnterBackgroundNotification
  310. object:nil];
  311. // 应用即将终止时立即上报
  312. [[NSNotificationCenter defaultCenter] addObserver:self
  313. selector:@selector(applicationWillTerminate)
  314. name:UIApplicationWillTerminateNotification
  315. object:nil];
  316. }
  317. - (void)applicationDidEnterBackground {
  318. [self flushEvents];
  319. }
  320. - (void)applicationWillTerminate {
  321. // 同步上报,确保数据不丢失
  322. dispatch_sync(self.analyticsQueue, ^{
  323. [self performFlush];
  324. });
  325. }
  326. - (JXDeviceInfo *)createDeviceInfo {
  327. JXDeviceInfo *deviceInfo = [[JXDeviceInfo alloc] init];
  328. deviceInfo.deviceType = @"ios";
  329. // 设备型号
  330. struct utsname systemInfo;
  331. uname(&systemInfo);
  332. deviceInfo.deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
  333. // 系统版本
  334. deviceInfo.osVersion = [NSString stringWithFormat:@"iOS %@", [[UIDevice currentDevice] systemVersion]];
  335. // 应用版本
  336. deviceInfo.appVersion = [self getAppVersion];
  337. return deviceInfo;
  338. }
  339. - (NSString *)getAppVersion {
  340. NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
  341. NSString *version = infoDictionary[@"CFBundleShortVersionString"];
  342. return version ?: @"unknown";
  343. }
  344. @end