api_controller.dart 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'package:device_info_plus/device_info_plus.dart';
  5. import 'package:dio/dio.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:get/get.dart';
  8. import 'package:nomo/app/api/router/api_router.dart';
  9. import 'package:nomo/app/data/sp/ix_sp.dart';
  10. import 'package:nomo/app/constants/sp_keys.dart';
  11. import 'package:package_info_plus/package_info_plus.dart';
  12. import 'package:path_provider/path_provider.dart';
  13. import 'package:play_install_referrer/play_install_referrer.dart';
  14. import 'package:system_clock/system_clock.dart';
  15. import 'package:uuid/uuid.dart';
  16. import '../../config/translations/localization_service.dart';
  17. import '../../config/translations/strings_enum.dart';
  18. import '../../utils/boost_report_manager.dart';
  19. import '../../utils/gzip_manager.dart';
  20. import '../api/file/api_file.dart';
  21. import '../data/models/banner/banner_list.dart';
  22. import '../data/models/launch/upgrade.dart';
  23. import 'base_core_api.dart';
  24. import '../../utils/api_statistics.dart';
  25. import '../../utils/device_manager.dart';
  26. import '../../utils/geo_downloader.dart';
  27. import '../../utils/log/logger.dart';
  28. import '../../utils/network_helper.dart';
  29. import '../../utils/ntp_time_service.dart';
  30. import '../api/core/api_core.dart';
  31. import '../api/log/api_log.dart';
  32. import '../components/country_restricted_overlay.dart';
  33. import '../components/ix_snackbar.dart';
  34. import '../constants/api_domains.dart';
  35. import '../constants/configs.dart';
  36. import '../constants/enums.dart';
  37. import '../constants/errors.dart';
  38. import '../constants/platforms.dart';
  39. import '../data/models/api_exception.dart';
  40. import '../data/models/channelplan/channel_plan_list.dart';
  41. import '../data/models/failure.dart';
  42. import '../data/models/fingerprint.dart';
  43. import '../data/models/launch/groups.dart';
  44. import '../data/models/launch/launch.dart';
  45. import '../dialog/all_dialog.dart';
  46. class ApiController extends GetxService with WidgetsBindingObserver {
  47. final TAG = 'ApiController';
  48. // 记录是否已经显示禁用弹窗
  49. bool isShowDisabled = false;
  50. // 上次调用 uploadBoostLog 的时间(用于节流)
  51. DateTime? _lastUploadBoostLogTime;
  52. //是否是游客
  53. final _isGuest = false.obs;
  54. bool get isGuest => _isGuest.value;
  55. set isGuest(bool value) => _isGuest.value = value;
  56. //是否是会员
  57. final _isPremium = false.obs;
  58. bool get isPremium => _isPremium.value;
  59. set isPremium(bool value) => _isPremium.value = value;
  60. //用户等级
  61. final _userLevel = 1.obs;
  62. int get userLevel => _userLevel.value;
  63. set userLevel(int value) => _userLevel.value = value;
  64. // 过期时间文本
  65. final _expireTimeText = ''.obs;
  66. String get expireTimeText => _expireTimeText.value;
  67. set expireTimeText(String value) => _expireTimeText.value = value;
  68. // 有效期文本
  69. final _validTermText = ''.obs;
  70. String get validTermText => _validTermText.value;
  71. set validTermText(String value) => _validTermText.value = value;
  72. //全部节点列表
  73. final _nodesList = <LocationList>[].obs;
  74. List<LocationList> get nodesList => _nodesList.value;
  75. set nodesList(List<LocationList> value) => _nodesList.value = value;
  76. //初始化fingerprint
  77. Fingerprint fp = Fingerprint.empty();
  78. // 全局剩余时间倒计时(秒)
  79. int _remainTimeSeconds = 0;
  80. int get remainTimeSeconds => _remainTimeSeconds;
  81. // 格式化后的剩余时间字符串(响应式,只有文案变化时才更新 UI)
  82. final _remainTimeFormatted = ''.obs;
  83. String get remainTimeFormatted => _remainTimeFormatted.value;
  84. // 是否应该显示倒计时(响应式,只有状态变化时才更新 UI)
  85. final _shouldShowCountdown = false.obs;
  86. bool get shouldShowCountdown => _shouldShowCountdown.value;
  87. // 倒计时定时器
  88. Timer? _remainTimeTimer;
  89. // 是否在后台
  90. bool isBackground = false;
  91. // 切换到后台时的系统时钟时间戳(毫秒,不受系统时间修改影响)
  92. int _backgroundElapsedRealtime = 0;
  93. @override
  94. void onInit() {
  95. super.onInit();
  96. WidgetsBinding.instance.addObserver(this);
  97. }
  98. @override
  99. void onClose() {
  100. WidgetsBinding.instance.removeObserver(this);
  101. super.onClose();
  102. }
  103. @override
  104. void didChangeAppLifecycleState(AppLifecycleState state) {
  105. log(TAG, "App state: $state");
  106. if (state == AppLifecycleState.paused) {
  107. isBackground = true;
  108. // 记录切换到后台的系统时钟时间戳(不受系统时间修改影响)
  109. _backgroundElapsedRealtime = SystemClock.elapsedRealtime().inMilliseconds;
  110. ApiStatistics.instance.onAppPaused();
  111. stopRemainTimeCountdown();
  112. } else if (state == AppLifecycleState.resumed) {
  113. if (isBackground) {
  114. isBackground = false;
  115. asyncHandleLaunch(isRefreshLaunch: true);
  116. ApiStatistics.instance.onAppResumed();
  117. // 计算后台经过的时间,继续倒计时
  118. _resumeRemainTimeCountdown();
  119. }
  120. }
  121. }
  122. /// 恢复倒计时(从后台切换到前台时调用)
  123. void _resumeRemainTimeCountdown() {
  124. if (_backgroundElapsedRealtime > 0 && _remainTimeSeconds > 0) {
  125. // 计算后台经过的秒数(使用系统时钟,不受系统时间修改影响)
  126. final currentElapsedRealtime =
  127. SystemClock.elapsedRealtime().inMilliseconds;
  128. final elapsedSeconds =
  129. (currentElapsedRealtime - _backgroundElapsedRealtime) ~/ 1000;
  130. // 更新剩余时间
  131. final newRemainTime = _remainTimeSeconds - elapsedSeconds;
  132. log(
  133. TAG,
  134. 'Resume countdown: elapsed=${elapsedSeconds}s, '
  135. 'old=$_remainTimeSeconds, new=$newRemainTime',
  136. );
  137. // 重新启动倒计时
  138. if (newRemainTime > 0) {
  139. startRemainTimeCountdown(newRemainTime);
  140. } else {
  141. // 时间已耗尽
  142. _remainTimeSeconds = 0;
  143. _updateRemainTimeFormatted();
  144. _onRemainTimeExpired();
  145. }
  146. }
  147. _backgroundElapsedRealtime = 0;
  148. }
  149. Future<Fingerprint> initFingerprint() async {
  150. // 读取app发布渠道
  151. if (Platform.isIOS) {
  152. fp.channel = 'apple';
  153. } else if (Platform.isAndroid) {
  154. try {
  155. final channel = await BaseCoreApi().getChannel();
  156. fp.channel = channel ?? 'unknown';
  157. } catch (e) {
  158. log(TAG, 'read app channel error: $e');
  159. fp.channel = '';
  160. }
  161. try {
  162. final advertisingId = await BaseCoreApi().getAdvertisingId();
  163. fp.googleId = advertisingId ?? '';
  164. } catch (e) {
  165. log(TAG, 'read app googleId error: $e');
  166. fp.googleId = '';
  167. }
  168. try {
  169. ReferrerDetails referrerDetails =
  170. await PlayInstallReferrer.installReferrer;
  171. fp.refer = referrerDetails.installReferrer ?? '';
  172. } catch (e) {
  173. log(TAG, 'get install referrer error: $e');
  174. fp.refer = '';
  175. }
  176. } else if (Platform.isWindows) {
  177. fp.channel = 'universal';
  178. }
  179. // 读取应用信息
  180. final info = await PackageInfo.fromPlatform();
  181. fp.appVersionCode = int.tryParse(info.buildNumber) ?? 0;
  182. fp.appVersionName = info.version;
  183. // 读取设备信息
  184. final deviceInfo = DeviceInfoPlugin();
  185. if (Platform.isIOS) {
  186. fp.platform = Platforms.iOS;
  187. final iosOsInfo = await deviceInfo.iosInfo;
  188. fp.deviceModel = iosOsInfo.model;
  189. fp.deviceOs = iosOsInfo.systemVersion;
  190. fp.deviceBrand = iosOsInfo.utsname.machine;
  191. } else if (Platform.isAndroid) {
  192. fp.platform = Platforms.android;
  193. final androidOsInfo = await deviceInfo.androidInfo;
  194. fp.deviceModel = androidOsInfo.model;
  195. fp.deviceOs = androidOsInfo.version.release;
  196. fp.deviceBrand = androidOsInfo.brand;
  197. fp.androidId = androidOsInfo.id;
  198. } else if (Platform.isWindows) {
  199. fp.platform = Platforms.windows;
  200. final windowsInfo = await deviceInfo.windowsInfo;
  201. fp.deviceModel = windowsInfo.productName;
  202. fp.deviceOs = windowsInfo.csdVersion;
  203. fp.deviceBrand = windowsInfo.computerName;
  204. }
  205. //获取设备尺寸
  206. fp.deviceHeight = Get.height.toInt();
  207. fp.deviceWidth = Get.width.toInt();
  208. // 读取设备ID
  209. fp.deviceId = DeviceManager.getCacheDeviceId();
  210. fp.isNewInstall = IXSP.getIsNewInstall();
  211. await updateFingerprintData();
  212. return fp;
  213. }
  214. // 更新部分数据
  215. Future<void> updateFingerprintData() async {
  216. fp.lang = IXSP.getCurrentLocal().languageCode;
  217. fp.phoneCountryIso = LocalizationService.getSystemCountry();
  218. fp.isVpn = await BaseCoreApi().isConnected() ?? false;
  219. if (!fp.isVpn) {
  220. fp.isConnectedVpn = false;
  221. }
  222. try {
  223. final simInfo = await BaseCoreApi().getSimInfo();
  224. // 解析sim
  225. final sim = jsonDecode(simInfo ?? '{}');
  226. fp.simReady = sim['simReady'];
  227. fp.carrierName = sim['carrierName'];
  228. fp.mcc = sim['mcc'];
  229. fp.mnc = sim['mnc'];
  230. fp.countryIso = sim['countryIso'];
  231. fp.networkCarrierName = sim['networkCarrierName'];
  232. fp.networkMcc = sim['networkMcc'];
  233. fp.networkMnc = sim['networkMnc'];
  234. fp.networkCountryIso = sim['networkCountryIso'];
  235. } catch (e) {
  236. log(TAG, 'read app sim error: $e');
  237. }
  238. }
  239. Future<void> initData(Launch? launch) async {
  240. // 初始化是否第一次安装
  241. IXSP.setIsNewInstall(false);
  242. fp.userUuid = '';
  243. fp.isNewInstall = false;
  244. await initLaunch(launch);
  245. }
  246. Future<void> initLaunch(Launch? launch) async {
  247. try {
  248. if (launch != null) {
  249. // 初始化用户状态
  250. isGuest = launch.userConfig?.memberLevel == MemberLevel.guest.level;
  251. userLevel = launch.userConfig?.userLevel ?? 1;
  252. isPremium = userLevel == 3 || userLevel == 9999;
  253. expireTimeText = _getExpireTimeText();
  254. validTermText = _getValidTermText();
  255. updateRemainTime(launch.userConfig?.remainTime ?? 0);
  256. NtpTimeService().initLaunchInitialTime();
  257. // 设置路由和节点
  258. nodesList = launch.groups?.normal?.list ?? [];
  259. // 设置资源url
  260. if (launch.appConfig?.assetUrls != null &&
  261. launch.appConfig!.assetUrls!.isNotEmpty) {
  262. Configs.assetUrl = launch.appConfig!.assetUrls![0];
  263. }
  264. // 设置官网url
  265. if (launch.appConfig?.websiteUrl != null &&
  266. launch.appConfig!.websiteUrl!.isNotEmpty) {
  267. Configs.websiteUrl = launch.appConfig!.websiteUrl!;
  268. }
  269. }
  270. } catch (e) {
  271. log(TAG, 'initLaunch error: $e');
  272. }
  273. }
  274. // 发送分析事件, 后续可以发送到firebase
  275. Future<void> sendAnalytics(FirebaseEvent event) async {
  276. try {} catch (e) {
  277. log('sendAnalytics error: $e');
  278. }
  279. }
  280. Future<Launch> launch({bool isCache = false}) async {
  281. sendAnalytics(isCache ? FirebaseEvent.launchCache : FirebaseEvent.launch);
  282. while (true) {
  283. try {
  284. ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
  285. final request = fp.toJson();
  286. final result = await ApiCore().launch(request);
  287. if (!result.success) {
  288. throw Failure(
  289. code: result.errorCode ?? '',
  290. message: result.errorMessage ?? '',
  291. );
  292. }
  293. sendAnalytics(
  294. isCache
  295. ? FirebaseEvent.launchCacheSuccess
  296. : FirebaseEvent.launchSuccess,
  297. );
  298. // 重置禁用状态
  299. IXSP.setLastIsRegionDisabled(false);
  300. IXSP.setLastIsUserDisabled(false);
  301. final launchData = Launch.fromJson(result.data);
  302. // 设置扩展数据
  303. fp.exData = launchData.exData;
  304. // 更新URL列表
  305. await ApiDomains.instance.updateFromLaunch(launchData);
  306. // 保存Launch数据
  307. await IXSP.saveLaunch(launchData);
  308. // 初始化Launch
  309. await initData(launchData);
  310. // 缓存日志上传
  311. processCachedLogs();
  312. // 缓存文件日志上传
  313. processCachedFileLogs();
  314. return launchData;
  315. } on ApiException catch (e) {
  316. final url = await ApiDomains.instance.getNextApiUrl();
  317. log(TAG, 'Launch request failed for URL $url: $e');
  318. if (url.isEmpty) {
  319. rethrow;
  320. }
  321. ApiCore().setbaseUrl(url);
  322. } on Failure catch (_) {
  323. rethrow;
  324. } on DioException catch (e) {
  325. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  326. e.response?.statusCode == Errors.eUserDisabled ||
  327. e.response?.statusCode == Errors.eTokenExpired) {
  328. rethrow;
  329. } else {
  330. if (await NetworkHelper.instance.isNetworkAvailable()) {
  331. final url = await ApiDomains.instance.getNextApiUrl();
  332. log(TAG, 'Launch request failed for URL $url: $e');
  333. if (url.isEmpty) {
  334. rethrow;
  335. }
  336. ApiCore().setbaseUrl(url);
  337. } else {
  338. rethrow;
  339. }
  340. }
  341. } catch (e) {
  342. final url = await ApiDomains.instance.getNextApiUrl();
  343. log(TAG, 'Launch request failed for URL $url: $e');
  344. if (url.isEmpty) {
  345. rethrow;
  346. }
  347. ApiCore().setbaseUrl(url);
  348. }
  349. }
  350. }
  351. Future<Launch> refreshLaunch() async {
  352. while (true) {
  353. try {
  354. ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
  355. final request = fp.toJson();
  356. final result = await ApiCore().refreshLaunch(request);
  357. if (!result.success) {
  358. throw Failure(
  359. code: result.errorCode ?? '',
  360. message: result.errorMessage ?? '',
  361. );
  362. }
  363. // 重置禁用状态
  364. IXSP.setLastIsRegionDisabled(false);
  365. IXSP.setLastIsUserDisabled(false);
  366. final launchData = Launch.fromJson(result.data);
  367. // 设置扩展数据
  368. fp.exData = launchData.exData;
  369. // 更新URL列表
  370. await ApiDomains.instance.updateFromLaunch(launchData);
  371. // 保存Launch数据
  372. await IXSP.saveLaunch(launchData);
  373. // 初始化Launch
  374. await initData(launchData);
  375. // 缓存日志上传
  376. processCachedLogs();
  377. // 缓存文件日志上传
  378. processCachedFileLogs();
  379. return launchData;
  380. } on ApiException catch (e) {
  381. final url = await ApiDomains.instance.getNextApiUrl();
  382. log(TAG, 'refresh launch request failed for URL $url: $e');
  383. if (url.isEmpty) {
  384. rethrow;
  385. }
  386. ApiCore().setbaseUrl(url);
  387. } on Failure catch (_) {
  388. rethrow;
  389. } on DioException catch (e) {
  390. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  391. e.response?.statusCode == Errors.eUserDisabled ||
  392. e.response?.statusCode == Errors.eTokenExpired) {
  393. rethrow;
  394. } else {
  395. if (await NetworkHelper.instance.isNetworkAvailable()) {
  396. final url = await ApiDomains.instance.getNextApiUrl();
  397. log(TAG, 'refresh launch request failed for URL $url: $e');
  398. if (url.isEmpty) {
  399. rethrow;
  400. }
  401. ApiCore().setbaseUrl(url);
  402. } else {
  403. rethrow;
  404. }
  405. }
  406. } catch (e) {
  407. final url = await ApiDomains.instance.getNextApiUrl();
  408. log(TAG, 'refresh launch request failed for URL $url: $e');
  409. if (url.isEmpty) {
  410. rethrow;
  411. }
  412. ApiCore().setbaseUrl(url);
  413. }
  414. }
  415. }
  416. Future<void> asyncHandleLaunch({bool isRefreshLaunch = false}) async {
  417. try {
  418. final data = isRefreshLaunch
  419. ? await refreshLaunch()
  420. : await launch(isCache: true);
  421. final isVpnRunning = await BaseCoreApi().isConnected() ?? false;
  422. if (!isVpnRunning) {
  423. await checkUpdate();
  424. // 下载smartgeo文件
  425. GeoDownloader().downloadSmartGeo(smartGeo: data.appConfig!.smartGeo!);
  426. }
  427. } catch (e, s) {
  428. if (IXSP.getLastIsUserDisabled()) {
  429. if (!isShowDisabled) {
  430. Get.offAll(
  431. () => CountryRestrictedOverlay(
  432. type: RestrictedType.user,
  433. onPressed: () async {
  434. // 清除LaunchData
  435. await IXSP.clearLaunchData();
  436. // 清除禁用状态
  437. IXSP.setLastIsUserDisabled(false);
  438. // 发送事件
  439. },
  440. ),
  441. transition: Transition.fadeIn,
  442. );
  443. }
  444. return;
  445. } else if (IXSP.getLastIsRegionDisabled()) {
  446. if (!isShowDisabled) {
  447. Get.offAll(
  448. () => const CountryRestrictedOverlay(type: RestrictedType.region),
  449. transition: Transition.fadeIn,
  450. );
  451. }
  452. return;
  453. } else if (IXSP.getLastIsDeviceDisabled()) {
  454. if (!isShowDisabled) {
  455. Get.offAll(
  456. () => const CountryRestrictedOverlay(type: RestrictedType.device),
  457. transition: Transition.fadeIn,
  458. );
  459. }
  460. return;
  461. }
  462. final isVpnRunning = await BaseCoreApi().isConnected() ?? false;
  463. if (!isVpnRunning) {
  464. await checkUpdate();
  465. }
  466. handleSnackBarError(e, s);
  467. }
  468. }
  469. void handleSnackBarError(dynamic error, StackTrace stackTrace) {
  470. if (error is ApiException) {
  471. IXSnackBar.showIXErrorSnackBar(
  472. title: Strings.error.tr,
  473. message: error.message,
  474. );
  475. } else if (error is Failure) {
  476. IXSnackBar.showIXErrorSnackBar(
  477. title: Strings.error.tr,
  478. message: error.message ?? Strings.unknownError.tr,
  479. );
  480. } else if (error is DioException) {
  481. switch (error.type) {
  482. case DioExceptionType.connectionError:
  483. case DioExceptionType.connectionTimeout:
  484. case DioExceptionType.receiveTimeout:
  485. case DioExceptionType.sendTimeout:
  486. IXSnackBar.showIXErrorSnackBar(
  487. title: Strings.error.tr,
  488. message: Strings.unableToConnectNetwork.tr,
  489. );
  490. break;
  491. default:
  492. IXSnackBar.showIXErrorSnackBar(
  493. title: Strings.error.tr,
  494. message: Strings.unableToConnectServer.tr,
  495. );
  496. }
  497. } else {
  498. IXSnackBar.showIXErrorSnackBar(
  499. title: Strings.error.tr,
  500. message: error.toString(),
  501. );
  502. }
  503. }
  504. // 更新检查 - 智能时间控制版本
  505. Future<bool> checkUpdate({bool isClickCheck = false}) async {
  506. try {
  507. final upgrade = IXSP.getUpgrade();
  508. // 如果当前versionCode等于升级的versionCode,则没有更新
  509. if (upgrade?.versionCode == fp.appVersionCode) {
  510. return false;
  511. }
  512. var hasUpdate = false;
  513. var hasForceUpdate = false;
  514. if (upgrade != null) {
  515. if (upgrade.upgradeType == 1) {
  516. hasUpdate = true;
  517. }
  518. if (upgrade.forced == true) {
  519. hasForceUpdate = true;
  520. }
  521. }
  522. if (hasUpdate) {
  523. // 强制更新或用户主动点击检查时,直接显示更新弹窗
  524. if (hasForceUpdate || isClickCheck) {
  525. _showUpgradeDialog(upgrade!, hasForceUpdate);
  526. return hasUpdate;
  527. }
  528. // 检查版本是否变化(新版本则重置时间逻辑)
  529. final currentVersion = upgrade?.versionCode?.toString() ?? '';
  530. final lastNoticeVersion =
  531. IXSP.getString(SPKeys.lastUpgradeNoticeVersion) ?? '';
  532. final isNewVersion = currentVersion != lastNoticeVersion;
  533. if (isNewVersion) {
  534. // 新版本,直接显示弹窗
  535. log(
  536. TAG,
  537. 'New version detected: $currentVersion (last: $lastNoticeVersion)',
  538. );
  539. _showUpgradeDialog(upgrade!, hasForceUpdate);
  540. return hasUpdate;
  541. }
  542. // 检查是否超过 upgradeNoticeTime 分钟
  543. final appConfig = IXSP.getAppConfig();
  544. final upgradeNoticeMinutes =
  545. appConfig?.upgradeNoticeTime ?? 1440; // 默认1天
  546. final lastNoticeTimeStr =
  547. IXSP.getString(SPKeys.lastUpgradeNoticeTime) ?? '0';
  548. final lastNoticeTime = int.tryParse(lastNoticeTimeStr) ?? 0;
  549. final now = NtpTimeService().getCurrentTimestamp();
  550. final elapsedMinutes = (now - lastNoticeTime) / (1000 * 60);
  551. if (elapsedMinutes >= upgradeNoticeMinutes) {
  552. _showUpgradeDialog(upgrade!, hasForceUpdate);
  553. } else {
  554. log(
  555. TAG,
  556. 'Upgrade notice skipped: ${elapsedMinutes.toStringAsFixed(1)}min '
  557. 'elapsed, need ${upgradeNoticeMinutes}min',
  558. );
  559. }
  560. }
  561. return hasUpdate;
  562. } catch (e) {
  563. log(TAG, 'checkUpdate error: $e');
  564. }
  565. return false;
  566. }
  567. /// 显示更新弹窗并记录时间和版本
  568. void _showUpgradeDialog(Upgrade upgrade, bool hasForceUpdate) {
  569. AllDialog.showUpdate(upgrade, hasForceUpdate: hasForceUpdate);
  570. // 记录本次提醒时间
  571. IXSP.setString(
  572. SPKeys.lastUpgradeNoticeTime,
  573. NtpTimeService().getCurrentTimestamp().toString(),
  574. );
  575. // 记录本次提醒的版本号
  576. IXSP.setString(
  577. SPKeys.lastUpgradeNoticeVersion,
  578. upgrade.versionCode?.toString() ?? '',
  579. );
  580. }
  581. Future<Launch> getDispatchInfo(
  582. int locationId,
  583. String locationCode, {
  584. CancelToken? cancelToken,
  585. }) async {
  586. while (true) {
  587. try {
  588. ApiRouter().setbaseUrl(ApiDomains.instance.getRouterUrl());
  589. final request = fp.toJson();
  590. request['locationId'] = locationId;
  591. request['locationCode'] = locationCode;
  592. // 获取选中的路由模式
  593. final routingMode =
  594. IXSP.getString(SPKeys.routingModeSelected) ?? "smart";
  595. request['routingMode'] = routingMode;
  596. final result = await ApiRouter().getDispatchInfo(
  597. request,
  598. cancelToken: cancelToken,
  599. );
  600. if (!result.success) {
  601. throw Failure(
  602. code: result.errorCode ?? '',
  603. message: result.errorMessage ?? '',
  604. );
  605. }
  606. // 重置禁用状态
  607. IXSP.setLastIsRegionDisabled(false);
  608. IXSP.setLastIsUserDisabled(false);
  609. final launchData = Launch.fromJson(result.data);
  610. // 更新URL列表
  611. await ApiDomains.instance.updateFromLaunch(launchData);
  612. // 保存app配置
  613. await IXSP.saveAppConfig(launchData.appConfig!);
  614. return launchData;
  615. } on ApiException catch (_) {
  616. rethrow;
  617. } on Failure catch (_) {
  618. rethrow;
  619. } on DioException catch (e) {
  620. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  621. e.response?.statusCode == Errors.eUserDisabled ||
  622. e.response?.statusCode == Errors.eTokenExpired) {
  623. rethrow;
  624. } else {
  625. if (await NetworkHelper.instance.isNetworkAvailable()) {
  626. final url = await ApiDomains.instance.getNextRouterUrl();
  627. log(TAG, 'getDispatchInfo request failed for URL $url: $e');
  628. if (url.isEmpty) {
  629. rethrow;
  630. }
  631. ApiRouter().setbaseUrl(url);
  632. } else {
  633. rethrow;
  634. }
  635. }
  636. } catch (e) {
  637. final url = await ApiDomains.instance.getNextRouterUrl();
  638. log(TAG, 'getDispatchInfo request failed for URL $url: $e');
  639. if (url.isEmpty) {
  640. rethrow;
  641. }
  642. ApiRouter().setbaseUrl(url);
  643. }
  644. }
  645. }
  646. Future<Launch> register(Map<String, dynamic> params) async {
  647. while (true) {
  648. try {
  649. ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
  650. final request = fp.toJson();
  651. request.addAll(params);
  652. final result = await ApiCore().register(request);
  653. if (!result.success) {
  654. throw Failure(
  655. code: result.errorCode ?? '',
  656. message: result.errorMessage ?? '',
  657. );
  658. }
  659. final launchData = Launch.fromJson(result.data);
  660. // 注册成功后上报firebase注册事件
  661. sendAnalytics(FirebaseEvent.register);
  662. // 保存 Launch 数据
  663. await IXSP.saveLaunch(launchData);
  664. // 初始化Launch
  665. await initData(launchData);
  666. return launchData;
  667. } on ApiException catch (_) {
  668. rethrow;
  669. } on Failure catch (_) {
  670. rethrow;
  671. } on DioException catch (e) {
  672. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  673. e.response?.statusCode == Errors.eUserDisabled ||
  674. e.response?.statusCode == Errors.eTokenExpired) {
  675. rethrow;
  676. } else {
  677. if (await NetworkHelper.instance.isNetworkAvailable()) {
  678. final url = await ApiDomains.instance.getNextApiUrl();
  679. log(TAG, 'Register request failed for URL $url: $e');
  680. if (url.isEmpty) {
  681. rethrow;
  682. }
  683. ApiCore().setbaseUrl(url);
  684. } else {
  685. rethrow;
  686. }
  687. }
  688. } catch (e) {
  689. final url = await ApiDomains.instance.getNextApiUrl();
  690. log(TAG, 'Register request failed for URL $url: $e');
  691. if (url.isEmpty) {
  692. rethrow;
  693. }
  694. ApiCore().setbaseUrl(url);
  695. }
  696. }
  697. }
  698. Future<Launch> login(Map<String, dynamic> params) async {
  699. while (true) {
  700. try {
  701. ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
  702. final request = fp.toJson();
  703. request.addAll(params);
  704. final result = await ApiCore().login(request);
  705. if (!result.success) {
  706. throw Failure(
  707. code: result.errorCode ?? '',
  708. message: result.errorMessage ?? '',
  709. );
  710. }
  711. final launchData = Launch.fromJson(result.data);
  712. // 注册成功后上报firebase注册事件
  713. sendAnalytics(FirebaseEvent.login);
  714. // 保存 Launch 数据
  715. await IXSP.saveLaunch(launchData);
  716. // 初始化Launch
  717. await initData(launchData);
  718. return launchData;
  719. } on ApiException catch (_) {
  720. rethrow;
  721. } on Failure catch (_) {
  722. rethrow;
  723. } on DioException catch (e) {
  724. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  725. e.response?.statusCode == Errors.eUserDisabled ||
  726. e.response?.statusCode == Errors.eTokenExpired) {
  727. rethrow;
  728. } else {
  729. if (await NetworkHelper.instance.isNetworkAvailable()) {
  730. final url = await ApiDomains.instance.getNextApiUrl();
  731. log(TAG, 'Login request failed for URL $url: $e');
  732. if (url.isEmpty) {
  733. rethrow;
  734. }
  735. ApiCore().setbaseUrl(url);
  736. } else {
  737. rethrow;
  738. }
  739. }
  740. } catch (e) {
  741. final url = await ApiDomains.instance.getNextApiUrl();
  742. log(TAG, 'Login request failed for URL $url: $e');
  743. if (url.isEmpty) {
  744. rethrow;
  745. }
  746. ApiCore().setbaseUrl(url);
  747. }
  748. }
  749. }
  750. Future<Launch> logout() async {
  751. try {
  752. final request = fp.toJson();
  753. final result = await ApiCore().logout(request);
  754. if (!result.success) {
  755. throw Failure(
  756. code: result.errorCode ?? '',
  757. message: result.errorMessage ?? '',
  758. );
  759. }
  760. final launchData = Launch.fromJson(result.data);
  761. // 登出成功后上报firebase登出事件
  762. sendAnalytics(FirebaseEvent.logout);
  763. // 保存 Launch 数据
  764. await IXSP.saveLaunch(launchData);
  765. await initData(launchData);
  766. return launchData;
  767. } catch (e) {
  768. rethrow;
  769. }
  770. }
  771. Future<Launch> deleteAccount() async {
  772. try {
  773. final request = fp.toJson();
  774. final result = await ApiCore().deleteAccount(request);
  775. if (!result.success) {
  776. throw Failure(
  777. code: result.errorCode ?? '',
  778. message: result.errorMessage ?? '',
  779. );
  780. }
  781. final launchData = Launch.fromJson(result.data);
  782. // 登出成功后上报firebase登出事件
  783. sendAnalytics(FirebaseEvent.deleteAccount);
  784. // 保存 Launch 数据
  785. await IXSP.saveLaunch(launchData);
  786. await initData(launchData);
  787. return launchData;
  788. } catch (e) {
  789. rethrow;
  790. }
  791. }
  792. Future<String> changePassword(Map<String, dynamic> params) async {
  793. try {
  794. final request = fp.toJson();
  795. request.addAll(params);
  796. final result = await ApiCore().changePassword(request);
  797. if (!result.success) {
  798. throw Failure(
  799. code: result.errorCode ?? '',
  800. message: result.errorMessage ?? '',
  801. );
  802. }
  803. return result.errorMessage ?? '';
  804. } catch (e) {
  805. rethrow;
  806. }
  807. }
  808. Future<Groups> getLocations() async {
  809. while (true) {
  810. try {
  811. ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
  812. final request = fp.toJson();
  813. final result = await ApiCore().getLocations(request);
  814. if (!result.success) {
  815. throw Failure(
  816. code: result.errorCode ?? '',
  817. message: result.errorMessage ?? '',
  818. );
  819. }
  820. final groups = Groups.fromJson(result.data);
  821. await IXSP.saveGroups(groups);
  822. return groups;
  823. } on ApiException catch (_) {
  824. rethrow;
  825. } on Failure catch (_) {
  826. rethrow;
  827. } on DioException catch (e) {
  828. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  829. e.response?.statusCode == Errors.eUserDisabled ||
  830. e.response?.statusCode == Errors.eTokenExpired) {
  831. rethrow;
  832. } else {
  833. if (await NetworkHelper.instance.isNetworkAvailable()) {
  834. final url = await ApiDomains.instance.getNextApiUrl();
  835. log(TAG, 'getLocations request failed for URL $url: $e');
  836. if (url.isEmpty) {
  837. rethrow;
  838. }
  839. ApiCore().setbaseUrl(url);
  840. } else {
  841. rethrow;
  842. }
  843. }
  844. } catch (e) {
  845. final url = await ApiDomains.instance.getNextApiUrl();
  846. log(TAG, 'getLocations request failed for URL $url: $e');
  847. if (url.isEmpty) {
  848. rethrow;
  849. }
  850. ApiCore().setbaseUrl(url);
  851. }
  852. }
  853. }
  854. Future<List<ChannelPlan>> getChannelPlanList() async {
  855. try {
  856. final request = fp.toJson();
  857. final result = await ApiCore().getChannelPlanList(request);
  858. if (!result.success) {
  859. throw Failure(
  860. code: result.errorCode ?? '',
  861. message: result.errorMessage ?? '',
  862. );
  863. }
  864. final channelPlanList = ChannelPlanList.fromJson(result.data);
  865. return channelPlanList.list ?? [];
  866. } catch (e) {
  867. rethrow;
  868. }
  869. }
  870. Future<Launch> subscribe(Map<String, dynamic> params) async {
  871. try {
  872. final request = fp.toJson();
  873. request.addAll(params);
  874. final result = await ApiCore().subscribe(request);
  875. if (!result.success) {
  876. throw Failure(
  877. code: result.errorCode ?? '',
  878. message: result.errorMessage ?? '',
  879. );
  880. }
  881. final launchData = Launch.fromJson(result.data);
  882. // 登出成功后上报firebase登出事件
  883. sendAnalytics(FirebaseEvent.subscribe);
  884. // 保存 Launch 数据
  885. await IXSP.saveLaunch(launchData);
  886. // 初始化Launch
  887. await initData(launchData);
  888. return launchData;
  889. } catch (e) {
  890. rethrow;
  891. }
  892. }
  893. Future<Launch> restore() async {
  894. try {
  895. final request = fp.toJson();
  896. final result = await ApiCore().restore(request);
  897. if (!result.success) {
  898. throw Failure(
  899. code: result.errorCode ?? '',
  900. message: result.errorMessage ?? '',
  901. );
  902. }
  903. final launchData = Launch.fromJson(result.data);
  904. // 登出成功后上报firebase登出事件
  905. sendAnalytics(FirebaseEvent.restore);
  906. // 保存 Launch 数据
  907. await IXSP.saveLaunch(launchData);
  908. // 初始化Launch
  909. await initData(launchData);
  910. return launchData;
  911. } catch (e) {
  912. rethrow;
  913. }
  914. }
  915. Future<void> connected(Map<String, dynamic> params) async {
  916. try {
  917. final request = fp.toJson();
  918. request.addAll(params);
  919. final result = await ApiRouter().connected(request);
  920. if (!result.success) {
  921. throw Failure(
  922. code: result.errorCode ?? '',
  923. message: result.errorMessage ?? '',
  924. );
  925. }
  926. } catch (e) {
  927. rethrow;
  928. }
  929. }
  930. Future<BannerList> getBanner({String position = "banner"}) async {
  931. try {
  932. final request = fp.toJson();
  933. request["position"] = position;
  934. final result = await ApiCore().getBanner(request);
  935. if (!result.success) {
  936. throw Failure(
  937. code: result.errorCode ?? '',
  938. message: result.errorMessage ?? '',
  939. );
  940. }
  941. final bannerList = BannerList.fromJson(result.data);
  942. return bannerList;
  943. } catch (e) {
  944. rethrow;
  945. }
  946. }
  947. /// 上传 Boost 日志(读取 app.json 和 core.json 并组合上传)
  948. /// 1秒内只允许调用一次
  949. Future<void> uploadBoostLog() async {
  950. // 节流检查:1秒内只允许调用一次
  951. final now = DateTime.now();
  952. if (_lastUploadBoostLogTime != null &&
  953. now.difference(_lastUploadBoostLogTime!).inMilliseconds < 1000) {
  954. log(TAG, 'uploadBoostLog throttled, skip');
  955. return;
  956. }
  957. _lastUploadBoostLogTime = now;
  958. try {
  959. // 读取 app.json 内容
  960. Map<String, dynamic> appLog = {};
  961. final appLogPath = await BoostReportManager().getAppLogFilePath();
  962. if (appLogPath != null) {
  963. final appFile = File(appLogPath);
  964. if (await appFile.exists()) {
  965. final content = await appFile.readAsString();
  966. if (content.isNotEmpty) {
  967. appLog = jsonDecode(content) as Map<String, dynamic>;
  968. }
  969. }
  970. }
  971. // 读取 core.json 内容
  972. Map<String, dynamic> coreLog = {};
  973. final coreLogPath = await BoostReportManager().getCoreLogFilePath();
  974. if (coreLogPath != null) {
  975. final coreFile = File(coreLogPath);
  976. if (await coreFile.exists()) {
  977. final content = await coreFile.readAsString();
  978. if (content.isNotEmpty) {
  979. coreLog = jsonDecode(content) as Map<String, dynamic>;
  980. }
  981. }
  982. }
  983. // 如果两个日志都为空,则不上传
  984. if (appLog.isEmpty && coreLog.isEmpty) {
  985. log(TAG, 'Both app log and core log are empty, skip upload');
  986. return;
  987. }
  988. // 组装日志数据
  989. final logItem = {
  990. 'id': const Uuid().v4(),
  991. 'time': DateTime.now().millisecondsSinceEpoch,
  992. 'level': LogLevel.info.name,
  993. 'module': LogModule.NM_Metrics.name,
  994. 'category': Configs.productCode,
  995. 'fields': {'appLog': appLog, 'coreLog': coreLog},
  996. };
  997. // 上传日志
  998. await uploadMetricsLog([logItem]);
  999. await uploadLocalLog(appLog['sessionInfo']?['boostSessionId'] ?? '');
  1000. log(TAG, 'Boost log uploaded successfully');
  1001. } catch (e) {
  1002. log(TAG, 'uploadBoostLog error: $e');
  1003. }
  1004. }
  1005. Future<void> uploadMetricsLog(List<Map<String, dynamic>> logs) async {
  1006. try {
  1007. await uploadLogs(logs, isCache: true);
  1008. } catch (e) {
  1009. log(TAG, 'uploadMetricsLog error: $e');
  1010. }
  1011. }
  1012. Future<void> uploadLocalLog(String boostSessionId) async {
  1013. try {
  1014. final appDir = await getApplicationSupportDirectory();
  1015. final logDir = Directory('${appDir.path}/logs');
  1016. if (await logDir.exists()) {
  1017. final filePaths = <String>[];
  1018. // 遍历 logDir 目录,查找 client_ 和 service_ 开头的文件
  1019. await for (final entity in logDir.list()) {
  1020. if (entity is File) {
  1021. final fileName = entity.path.split('/').last;
  1022. if (fileName.startsWith('client_') ||
  1023. fileName.startsWith('service_')) {
  1024. filePaths.add(entity.path);
  1025. }
  1026. }
  1027. }
  1028. if (filePaths.isEmpty) {
  1029. log(TAG, 'No client_ or service_ log files found');
  1030. return;
  1031. }
  1032. final gzipFilePath = await GzipManager().generateAndShareGzip(
  1033. filePaths: filePaths,
  1034. gzipFileName: '$boostSessionId.tar.gz',
  1035. );
  1036. if (gzipFilePath == null) {
  1037. return;
  1038. }
  1039. await uploadLogFile(
  1040. gzipFilePath,
  1041. boostSessionId,
  1042. LogModule.NM_Log.name,
  1043. );
  1044. }
  1045. } catch (e) {
  1046. rethrow;
  1047. }
  1048. }
  1049. Future<String> uploadLogs(List<dynamic> items, {bool isCache = false}) async {
  1050. await updateFingerprintData();
  1051. Map<String, dynamic> request = fp.toJson();
  1052. request['items'] = items;
  1053. while (true) {
  1054. try {
  1055. ApiLog().setbaseUrl(ApiDomains.instance.getLogUrl());
  1056. final result = await ApiLog().uploadLogs(request);
  1057. if (!result.success) {
  1058. throw Failure(
  1059. code: result.errorCode ?? '',
  1060. message: result.errorMessage ?? '',
  1061. );
  1062. }
  1063. return result.errorMessage ?? '';
  1064. } on ApiException catch (_) {
  1065. rethrow;
  1066. } on Failure catch (_) {
  1067. rethrow;
  1068. } on DioException catch (e) {
  1069. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  1070. e.response?.statusCode == Errors.eUserDisabled ||
  1071. e.response?.statusCode == Errors.eTokenExpired) {
  1072. if (isCache) {
  1073. await _cacheLogRequest(items);
  1074. }
  1075. rethrow;
  1076. } else {
  1077. if (await NetworkHelper.instance.isNetworkAvailable()) {
  1078. final url = await ApiDomains.instance.getNextLogUrl();
  1079. if (url.isEmpty) {
  1080. if (isCache) {
  1081. await _cacheLogRequest(items);
  1082. }
  1083. rethrow;
  1084. }
  1085. log(
  1086. TAG,
  1087. 'uploadLogs request failed for URL ${ApiLog().baseUrl}: $e',
  1088. );
  1089. ApiLog().setbaseUrl(url);
  1090. } else {
  1091. if (isCache) {
  1092. await _cacheLogRequest(items);
  1093. }
  1094. rethrow;
  1095. }
  1096. }
  1097. } catch (e) {
  1098. final url = await ApiDomains.instance.getNextLogUrl();
  1099. if (url.isEmpty) {
  1100. if (isCache) {
  1101. await _cacheLogRequest(items);
  1102. }
  1103. rethrow;
  1104. }
  1105. log(TAG, 'uploadLogs request failed for URL ${ApiLog().baseUrl}: $e');
  1106. ApiLog().setbaseUrl(url);
  1107. }
  1108. }
  1109. }
  1110. // 上传文件
  1111. Future<String> uploadLogFile(
  1112. String filePath,
  1113. String fileName,
  1114. String logType, {
  1115. bool isCache = false,
  1116. }) async {
  1117. try {
  1118. Map<String, dynamic> request = fp.toJson();
  1119. final data = jsonEncode(request);
  1120. while (true) {
  1121. try {
  1122. ApiFile().setbaseUrl(ApiDomains.instance.getFileUrl());
  1123. final result = await ApiFile().uploadLogFile(
  1124. filePath,
  1125. fileName,
  1126. logType,
  1127. data,
  1128. );
  1129. if (!result.success) {
  1130. throw Failure(
  1131. code: result.errorCode ?? '',
  1132. message: result.errorMessage ?? '',
  1133. );
  1134. }
  1135. return result.errorMessage ?? '';
  1136. } on ApiException catch (_) {
  1137. rethrow;
  1138. } on Failure catch (_) {
  1139. rethrow;
  1140. } on DioException catch (e) {
  1141. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  1142. e.response?.statusCode == Errors.eUserDisabled ||
  1143. e.response?.statusCode == Errors.eTokenExpired) {
  1144. if (isCache) {
  1145. await _cacheFileLogRequest(filePath, fileName);
  1146. }
  1147. rethrow;
  1148. } else {
  1149. if (await NetworkHelper.instance.isNetworkAvailable()) {
  1150. final url = await ApiDomains.instance.getNextFileUrl();
  1151. if (url.isEmpty) {
  1152. if (isCache) {
  1153. await _cacheFileLogRequest(filePath, fileName);
  1154. }
  1155. rethrow;
  1156. }
  1157. log('uploadLogs request failed for URL ${ApiLog().baseUrl}: $e');
  1158. ApiLog().setbaseUrl(url);
  1159. } else {
  1160. if (isCache) {
  1161. await _cacheFileLogRequest(filePath, fileName);
  1162. }
  1163. rethrow;
  1164. }
  1165. }
  1166. } catch (e) {
  1167. final url = await ApiDomains.instance.getNextFileUrl();
  1168. if (url.isEmpty) {
  1169. if (isCache) {
  1170. await _cacheFileLogRequest(filePath, fileName);
  1171. }
  1172. rethrow;
  1173. }
  1174. log('uploadLogs request failed for URL ${ApiLog().baseUrl}: $e');
  1175. ApiLog().setbaseUrl(url);
  1176. }
  1177. }
  1178. } catch (e) {
  1179. rethrow;
  1180. }
  1181. }
  1182. // 缓存文件日志请求数据
  1183. Future<void> _cacheFileLogRequest(String filePath, String fileName) async {
  1184. try {
  1185. final dir = await getApplicationSupportDirectory();
  1186. final cacheDir = Directory('${dir.path}/file_log_cache');
  1187. if (!await cacheDir.exists()) {
  1188. await cacheDir.create(recursive: true);
  1189. }
  1190. // 复制文件到缓存目录
  1191. final file = File(filePath);
  1192. await file.copy('${cacheDir.path}/${file.path.split('/').last}');
  1193. // 清理缓存目录
  1194. await _cleanupFileCacheDirectory(cacheDir);
  1195. } catch (e) {
  1196. log('Failed to cache file log request: $e');
  1197. }
  1198. }
  1199. // 清理文件缓存目录
  1200. Future<void> _cleanupFileCacheDirectory(Directory cacheDir) async {
  1201. try {
  1202. const maxFiles = 10; // 最多保留10个文件
  1203. final files = await cacheDir.list().toList();
  1204. // 按修改时间排序
  1205. files.sort(
  1206. (a, b) => a.statSync().modified.compareTo(b.statSync().modified),
  1207. );
  1208. // 如果超过文件数量或大小限制,删除最旧的文件
  1209. while (files.length > maxFiles && files.isNotEmpty) {
  1210. final oldestFile = files.removeAt(0);
  1211. await oldestFile.delete();
  1212. }
  1213. } catch (e) {
  1214. log('Failed to cleanup cache directory: $e');
  1215. }
  1216. }
  1217. // 处理缓存的文件日志数据
  1218. Future<void> processCachedFileLogs() async {
  1219. try {
  1220. final dir = await getApplicationSupportDirectory();
  1221. final cacheDir = Directory('${dir.path}/file_log_cache');
  1222. if (!await cacheDir.exists()) {
  1223. return;
  1224. }
  1225. final files = await cacheDir.list().toList();
  1226. if (files.isEmpty) {
  1227. return;
  1228. }
  1229. // 按修改时间排序,先处理最旧的文件
  1230. files.sort(
  1231. (a, b) => a.statSync().modified.compareTo(b.statSync().modified),
  1232. );
  1233. for (var fileEntity in files) {
  1234. try {
  1235. if (fileEntity is! File) continue;
  1236. final filePath = fileEntity.path;
  1237. final fileName = fileEntity.path.split('/').last.split('.').first;
  1238. // 尝试上传缓存的日志
  1239. await uploadLogFile(filePath, fileName, LogModule.NM_Log.name);
  1240. // 上传成功后删除缓存文件
  1241. await fileEntity.delete();
  1242. } catch (e) {
  1243. log('Failed to process cached log file ${fileEntity.path}: $e');
  1244. // 如果上传失败,保留文件等待下次尝试
  1245. continue;
  1246. }
  1247. }
  1248. } catch (e) {
  1249. log('Failed to process cached file logs: $e');
  1250. }
  1251. }
  1252. // 缓存日志请求数据
  1253. Future<void> _cacheLogRequest(dynamic items) async {
  1254. try {
  1255. final dir = await getApplicationSupportDirectory();
  1256. final cacheDir = Directory('${dir.path}/log_cache');
  1257. if (!await cacheDir.exists()) {
  1258. await cacheDir.create(recursive: true);
  1259. }
  1260. // 生成唯一的文件名
  1261. final timestamp = DateTime.now().millisecondsSinceEpoch;
  1262. final fileName = 'log_$timestamp.json';
  1263. final file = File('${cacheDir.path}/$fileName');
  1264. // 将请求数据写入文件
  1265. await file.writeAsString(jsonEncode(items));
  1266. // 清理缓存目录
  1267. await _cleanupCacheDirectory(cacheDir);
  1268. } catch (e) {
  1269. log(TAG, 'Failed to cache log request: $e');
  1270. }
  1271. }
  1272. // 清理缓存目录
  1273. Future<void> _cleanupCacheDirectory(Directory cacheDir) async {
  1274. try {
  1275. const maxFiles = 10; // 最多保留10个文件
  1276. const maxCacheSize = 5 * 1024 * 1024; // 5MB
  1277. final files = await cacheDir.list().toList();
  1278. // 按修改时间排序
  1279. files.sort(
  1280. (a, b) => a.statSync().modified.compareTo(b.statSync().modified),
  1281. );
  1282. // 计算当前缓存大小
  1283. int totalSize = 0;
  1284. for (var file in files) {
  1285. totalSize += file.statSync().size;
  1286. }
  1287. // 如果超过文件数量或大小限制,删除最旧的文件
  1288. while ((files.length > maxFiles || totalSize > maxCacheSize) &&
  1289. files.isNotEmpty) {
  1290. final oldestFile = files.removeAt(0);
  1291. totalSize -= oldestFile.statSync().size;
  1292. await oldestFile.delete();
  1293. }
  1294. } catch (e) {
  1295. log(TAG, 'Failed to cleanup cache directory: $e');
  1296. }
  1297. }
  1298. // 处理缓存的日志数据
  1299. Future<void> processCachedLogs() async {
  1300. try {
  1301. final dir = await getApplicationSupportDirectory();
  1302. final cacheDir = Directory('${dir.path}/log_cache');
  1303. if (!await cacheDir.exists()) {
  1304. return;
  1305. }
  1306. final files = await cacheDir.list().toList();
  1307. if (files.isEmpty) {
  1308. return;
  1309. }
  1310. // 按修改时间排序,先处理最旧的文件
  1311. files.sort(
  1312. (a, b) => a.statSync().modified.compareTo(b.statSync().modified),
  1313. );
  1314. for (var fileEntity in files) {
  1315. try {
  1316. if (fileEntity is! File) continue;
  1317. final content = await fileEntity.readAsString();
  1318. final items = jsonDecode(content);
  1319. // 尝试上传缓存的日志
  1320. await uploadLogs(items, isCache: false);
  1321. // 上传成功后删除缓存文件
  1322. await fileEntity.delete();
  1323. } catch (e) {
  1324. log('Failed to process cached log file ${fileEntity.path}: $e');
  1325. // 如果上传失败,保留文件等待下次尝试
  1326. continue;
  1327. }
  1328. }
  1329. } catch (e) {
  1330. log('Failed to process cached logs: $e');
  1331. }
  1332. }
  1333. Future<void> uploadApiStatisticsLog(
  1334. List<Map<String, dynamic>> logs, {
  1335. LogModule module = LogModule.NM_ApiLaunchLog,
  1336. }) async {
  1337. if (isNeedUploadLogs(module)) {
  1338. await uploadLogs(logs);
  1339. }
  1340. }
  1341. // 判断是否需要上传日志
  1342. bool isNeedUploadLogs(LogModule module) {
  1343. final launch = IXSP.getLaunch();
  1344. if (launch == null) {
  1345. return false;
  1346. }
  1347. if (launch.appConfig?.disabledLogModules?.contains(module.name) ?? false) {
  1348. return false;
  1349. }
  1350. return true;
  1351. }
  1352. /// 获取订阅周期类型文本
  1353. /// subscribeType: 1Day 2Week 3Month 4Year
  1354. String _getSubscribeTypeText() {
  1355. final user = IXSP.getUser();
  1356. final planInfo = user?.planInfo;
  1357. // 仅当 isSubscribe=true 时有效
  1358. if (planInfo?.isSubscribe != true) {
  1359. return planInfo?.subTitle ?? '';
  1360. }
  1361. switch (planInfo?.subscribeType) {
  1362. case 1:
  1363. return 'Day';
  1364. case 2:
  1365. return 'Week';
  1366. case 3:
  1367. return 'Month';
  1368. case 4:
  1369. return 'Year';
  1370. default:
  1371. return '';
  1372. }
  1373. }
  1374. /// 获取过期时间文本
  1375. String _getExpireTimeText() {
  1376. final user = IXSP.getUser();
  1377. final expireTime = user?.expireTime;
  1378. if (expireTime == null || expireTime == 0) {
  1379. return '';
  1380. }
  1381. // 时间戳转日期(秒级时间戳)
  1382. final date = DateTime.fromMillisecondsSinceEpoch(expireTime * 1000);
  1383. final formatted =
  1384. "${date.year.toString().padLeft(4, '0')}-"
  1385. "${date.month.toString().padLeft(2, '0')}-"
  1386. "${date.day.toString().padLeft(2, '0')}";
  1387. return formatted;
  1388. }
  1389. /// 获取有效期显示文本
  1390. String _getValidTermText() {
  1391. final subscribeType = _getSubscribeTypeText();
  1392. final expireTime = _getExpireTimeText();
  1393. if (subscribeType.isNotEmpty && expireTime.isNotEmpty) {
  1394. return '$subscribeType / $expireTime';
  1395. } else if (expireTime.isNotEmpty) {
  1396. return expireTime;
  1397. }
  1398. return '';
  1399. }
  1400. /// 启动剩余时间倒计时
  1401. /// [seconds] 剩余时间(秒),例如:3600 表示 1 小时
  1402. void startRemainTimeCountdown(int seconds) {
  1403. // 取消之前的定时器
  1404. _remainTimeTimer?.cancel();
  1405. // 设置初始剩余时间
  1406. _remainTimeSeconds = seconds;
  1407. // 立即更新格式化文案
  1408. _updateRemainTimeFormatted();
  1409. // 启动每秒倒计时
  1410. _remainTimeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
  1411. if (_remainTimeSeconds > 0) {
  1412. _remainTimeSeconds--;
  1413. // 只有当格式化文案变化时才更新 UI
  1414. _updateRemainTimeFormatted();
  1415. } else {
  1416. // 倒计时结束
  1417. timer.cancel();
  1418. _remainTimeTimer = null;
  1419. _onRemainTimeExpired();
  1420. }
  1421. });
  1422. }
  1423. /// 更新格式化后的剩余时间(只有文案变化时才触发 UI 更新)
  1424. void _updateRemainTimeFormatted() {
  1425. final newFormatted = _formatRemainTime(_remainTimeSeconds);
  1426. if (_remainTimeFormatted.value != newFormatted) {
  1427. _remainTimeFormatted.value = newFormatted;
  1428. }
  1429. // 更新是否显示倒计时的状态
  1430. final vipRemainNoticeSeconds =
  1431. (IXSP.getAppConfig()?.vipRemainNotice ?? 600) * 60;
  1432. final newShouldShow =
  1433. _remainTimeSeconds > 0 && _remainTimeSeconds < vipRemainNoticeSeconds;
  1434. if (_shouldShowCountdown.value != newShouldShow) {
  1435. _shouldShowCountdown.value = newShouldShow;
  1436. }
  1437. }
  1438. /// 停止剩余时间倒计时
  1439. void stopRemainTimeCountdown() {
  1440. _remainTimeTimer?.cancel();
  1441. _remainTimeTimer = null;
  1442. }
  1443. /// 更新剩余时间(从服务器获取新的时间后调用)
  1444. void updateRemainTime(int seconds) {
  1445. stopRemainTimeCountdown();
  1446. if (seconds > 0) {
  1447. startRemainTimeCountdown(seconds);
  1448. } else {
  1449. _remainTimeSeconds = 0;
  1450. _updateRemainTimeFormatted();
  1451. }
  1452. }
  1453. /// 剩余时间到期处理
  1454. void _onRemainTimeExpired() {
  1455. log(TAG, 'VIP剩余时间已到期');
  1456. // 可以在这里添加到期后的处理逻辑,例如:
  1457. // - 显示续费提示
  1458. // - 断开VPN连接
  1459. // - 刷新用户信息
  1460. refreshLaunch();
  1461. }
  1462. /// 格式化剩余时间显示
  1463. String _formatRemainTime(int totalSeconds) {
  1464. if (totalSeconds <= 0) {
  1465. return '--';
  1466. }
  1467. final days = totalSeconds ~/ 86400;
  1468. final hours = (totalSeconds % 86400) ~/ 3600;
  1469. final minutes = (totalSeconds % 3600) ~/ 60;
  1470. final seconds = totalSeconds % 60;
  1471. // 大于1天
  1472. if (days > 1) {
  1473. return '$days days';
  1474. }
  1475. // 等于1天
  1476. if (days == 1) {
  1477. return '1 day';
  1478. }
  1479. // 大于1小时
  1480. if (hours >= 1) {
  1481. return '$hours h';
  1482. }
  1483. // 小于1小时,显示 mm:ss
  1484. return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
  1485. }
  1486. }