api_controller.dart 50 KB

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