api_controller.dart 50 KB

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