api_controller.dart 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221
  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 '../../config/translations/localization_service.dart';
  15. import '../../config/translations/strings_enum.dart';
  16. import '../data/models/banner/banner_list.dart';
  17. import 'base_core_api.dart';
  18. import '../../utils/api_statistics.dart';
  19. import '../../utils/device_manager.dart';
  20. import '../../utils/geo_downloader.dart';
  21. import '../../utils/log/logger.dart';
  22. import '../../utils/network_helper.dart';
  23. import '../../utils/ntp_time_service.dart';
  24. import '../api/core/api_core.dart';
  25. import '../api/log/api_log.dart';
  26. import '../components/country_restricted_overlay.dart';
  27. import '../components/ix_snackbar.dart';
  28. import '../constants/api_domains.dart';
  29. import '../constants/configs.dart';
  30. import '../constants/enums.dart';
  31. import '../constants/errors.dart';
  32. import '../constants/platforms.dart';
  33. import '../data/models/api_exception.dart';
  34. import '../data/models/channelplan/channel_plan_list.dart';
  35. import '../data/models/failure.dart';
  36. import '../data/models/fingerprint.dart';
  37. import '../data/models/launch/groups.dart';
  38. import '../data/models/launch/launch.dart';
  39. import '../dialog/all_dialog.dart';
  40. class ApiController extends GetxService with WidgetsBindingObserver {
  41. final TAG = 'ApiController';
  42. // 记录是否已经显示禁用弹窗
  43. bool isShowDisabled = false;
  44. //是否是游客
  45. final _isGuest = false.obs;
  46. bool get isGuest => _isGuest.value;
  47. set isGuest(bool value) => _isGuest.value = value;
  48. //是否是会员
  49. final _isPremium = false.obs;
  50. bool get isPremium => _isPremium.value;
  51. set isPremium(bool value) => _isPremium.value = value;
  52. //用户等级
  53. final _userLevel = 1.obs;
  54. int get userLevel => _userLevel.value;
  55. set userLevel(int value) => _userLevel.value = value;
  56. // 过期时间文本
  57. final _expireTimeText = ''.obs;
  58. String get expireTimeText => _expireTimeText.value;
  59. set expireTimeText(String value) => _expireTimeText.value = value;
  60. // 有效期文本
  61. final _validTermText = ''.obs;
  62. String get validTermText => _validTermText.value;
  63. set validTermText(String value) => _validTermText.value = value;
  64. //全部节点列表
  65. final _nodesList = <LocationList>[].obs;
  66. List<LocationList> get nodesList => _nodesList.value;
  67. set nodesList(List<LocationList> value) => _nodesList.value = value;
  68. //初始化fingerprint
  69. Fingerprint fp = Fingerprint.empty();
  70. // 全局剩余时间倒计时(秒)
  71. int _remainTimeSeconds = 0;
  72. int get remainTimeSeconds => _remainTimeSeconds;
  73. // 格式化后的剩余时间字符串(响应式,只有文案变化时才更新 UI)
  74. final _remainTimeFormatted = ''.obs;
  75. String get remainTimeFormatted => _remainTimeFormatted.value;
  76. // 是否应该显示倒计时(响应式,只有状态变化时才更新 UI)
  77. final _shouldShowCountdown = false.obs;
  78. bool get shouldShowCountdown => _shouldShowCountdown.value;
  79. // 倒计时定时器
  80. Timer? _remainTimeTimer;
  81. // 是否在后台
  82. bool isBackground = false;
  83. @override
  84. void onInit() {
  85. super.onInit();
  86. WidgetsBinding.instance.addObserver(this);
  87. }
  88. @override
  89. void onClose() {
  90. WidgetsBinding.instance.removeObserver(this);
  91. super.onClose();
  92. }
  93. @override
  94. void didChangeAppLifecycleState(AppLifecycleState state) {
  95. log(TAG, "App state: $state");
  96. if (state == AppLifecycleState.paused) {
  97. isBackground = true;
  98. ApiStatistics.instance.onAppPaused();
  99. stopRemainTimeCountdown();
  100. } else if (state == AppLifecycleState.resumed) {
  101. if (isBackground) {
  102. isBackground = false;
  103. asyncHandleLaunch(isRefreshLaunch: true);
  104. ApiStatistics.instance.onAppResumed();
  105. updateRemainTime(IXSP.getUser()?.remainTime ?? 0);
  106. }
  107. }
  108. }
  109. Future<Fingerprint> initFingerprint() async {
  110. // 读取app发布渠道
  111. if (Platform.isIOS) {
  112. fp.channel = 'apple';
  113. } else if (Platform.isAndroid) {
  114. try {
  115. final channel = await BaseCoreApi().getChannel();
  116. fp.channel = channel ?? 'unknown';
  117. } catch (e) {
  118. log(TAG, 'read app channel error: $e');
  119. fp.channel = '';
  120. }
  121. try {
  122. final advertisingId = await BaseCoreApi().getAdvertisingId();
  123. fp.googleId = advertisingId ?? '';
  124. } catch (e) {
  125. log(TAG, 'read app googleId error: $e');
  126. fp.googleId = '';
  127. }
  128. try {
  129. ReferrerDetails referrerDetails =
  130. await PlayInstallReferrer.installReferrer;
  131. fp.refer = referrerDetails.installReferrer ?? '';
  132. } catch (e) {
  133. log(TAG, 'get install referrer error: $e');
  134. fp.refer = '';
  135. }
  136. } else if (Platform.isWindows) {
  137. fp.channel = 'universal';
  138. }
  139. // 读取应用信息
  140. final info = await PackageInfo.fromPlatform();
  141. fp.appVersionCode = int.tryParse(info.buildNumber) ?? 0;
  142. fp.appVersionName = info.version;
  143. // 读取设备信息
  144. final deviceInfo = DeviceInfoPlugin();
  145. if (Platform.isIOS) {
  146. fp.platform = Platforms.iOS;
  147. final iosOsInfo = await deviceInfo.iosInfo;
  148. fp.deviceModel = iosOsInfo.model;
  149. fp.deviceOs = iosOsInfo.systemVersion;
  150. fp.deviceBrand = iosOsInfo.utsname.machine;
  151. } else if (Platform.isAndroid) {
  152. fp.platform = Platforms.android;
  153. final androidOsInfo = await deviceInfo.androidInfo;
  154. fp.deviceModel = androidOsInfo.model;
  155. fp.deviceOs = androidOsInfo.version.release;
  156. fp.deviceBrand = androidOsInfo.brand;
  157. fp.androidId = androidOsInfo.id;
  158. } else if (Platform.isWindows) {
  159. fp.platform = Platforms.windows;
  160. final windowsInfo = await deviceInfo.windowsInfo;
  161. fp.deviceModel = windowsInfo.productName;
  162. fp.deviceOs = windowsInfo.csdVersion;
  163. fp.deviceBrand = windowsInfo.computerName;
  164. }
  165. //获取设备尺寸
  166. fp.deviceHeight = Get.height.toInt();
  167. fp.deviceWidth = Get.width.toInt();
  168. // 读取设备ID
  169. fp.deviceId = DeviceManager.getCacheDeviceId();
  170. fp.isNewInstall = IXSP.getIsNewInstall();
  171. await updateFingerprintData();
  172. return fp;
  173. }
  174. // 更新部分数据
  175. Future<void> updateFingerprintData() async {
  176. fp.lang = IXSP.getCurrentLocal().languageCode;
  177. fp.phoneCountryIso = LocalizationService.getSystemCountry();
  178. fp.isVpn = await BaseCoreApi().isConnected() ?? false;
  179. if (!fp.isVpn) {
  180. fp.isConnectedVpn = false;
  181. }
  182. try {
  183. final simInfo = await BaseCoreApi().getSimInfo();
  184. // 解析sim
  185. final sim = jsonDecode(simInfo ?? '{}');
  186. fp.simReady = sim['simReady'];
  187. fp.carrierName = sim['carrierName'];
  188. fp.mcc = sim['mcc'];
  189. fp.mnc = sim['mnc'];
  190. fp.countryIso = sim['countryIso'];
  191. fp.networkCarrierName = sim['networkCarrierName'];
  192. fp.networkMcc = sim['networkMcc'];
  193. fp.networkMnc = sim['networkMnc'];
  194. fp.networkCountryIso = sim['networkCountryIso'];
  195. } catch (e) {
  196. log(TAG, 'read app sim error: $e');
  197. }
  198. }
  199. Future<void> initData(Launch? launch) async {
  200. // 初始化是否第一次安装
  201. IXSP.setIsNewInstall(false);
  202. fp.userUuid = '';
  203. fp.isNewInstall = false;
  204. await initLaunch(launch);
  205. }
  206. Future<void> initLaunch(Launch? launch) async {
  207. try {
  208. if (launch != null) {
  209. // 初始化用户状态
  210. isGuest = launch.userConfig?.memberLevel == MemberLevel.guest.level;
  211. userLevel = launch.userConfig?.userLevel ?? 1;
  212. isPremium = userLevel == 3 || userLevel == 9999;
  213. expireTimeText = _getExpireTimeText();
  214. validTermText = _getValidTermText();
  215. updateRemainTime(launch.userConfig?.remainTime ?? 0);
  216. NtpTimeService().initLaunchInitialTime();
  217. // 设置路由和节点
  218. nodesList = launch.groups?.normal?.list ?? [];
  219. // 设置资源url
  220. if (launch.appConfig?.assetUrls != null &&
  221. launch.appConfig!.assetUrls!.isNotEmpty) {
  222. Configs.assetUrl = launch.appConfig!.assetUrls![0];
  223. }
  224. // 设置官网url
  225. if (launch.appConfig?.websiteUrl != null &&
  226. launch.appConfig!.websiteUrl!.isNotEmpty) {
  227. Configs.websiteUrl = launch.appConfig!.websiteUrl!;
  228. }
  229. }
  230. } catch (e) {
  231. log(TAG, 'initLaunch error: $e');
  232. }
  233. }
  234. // 发送分析事件, 后续可以发送到firebase
  235. Future<void> sendAnalytics(FirebaseEvent event) async {
  236. try {} catch (e) {
  237. log('sendAnalytics error: $e');
  238. }
  239. }
  240. Future<Launch> launch({bool isCache = false}) async {
  241. sendAnalytics(isCache ? FirebaseEvent.launchCache : FirebaseEvent.launch);
  242. while (true) {
  243. try {
  244. ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
  245. final request = fp.toJson();
  246. final result = await ApiCore().launch(request);
  247. if (!result.success) {
  248. throw Failure(
  249. code: result.errorCode ?? '',
  250. message: result.errorMessage ?? '',
  251. );
  252. }
  253. sendAnalytics(
  254. isCache
  255. ? FirebaseEvent.launchCacheSuccess
  256. : FirebaseEvent.launchSuccess,
  257. );
  258. // 重置禁用状态
  259. IXSP.setLastIsRegionDisabled(false);
  260. IXSP.setLastIsUserDisabled(false);
  261. final launchData = Launch.fromJson(result.data);
  262. // 设置扩展数据
  263. fp.exData = launchData.exData;
  264. // 更新URL列表
  265. await ApiDomains.instance.updateFromLaunch(launchData);
  266. // 保存Launch数据
  267. await IXSP.saveLaunch(launchData);
  268. // 初始化Launch
  269. await initData(launchData);
  270. return launchData;
  271. } on ApiException catch (e) {
  272. final url = await ApiDomains.instance.getNextApiUrl();
  273. log(TAG, 'Launch request failed for URL $url: $e');
  274. if (url.isEmpty) {
  275. rethrow;
  276. }
  277. ApiCore().setbaseUrl(url);
  278. } on Failure catch (_) {
  279. rethrow;
  280. } on DioException catch (e) {
  281. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  282. e.response?.statusCode == Errors.eUserDisabled ||
  283. e.response?.statusCode == Errors.eTokenExpired) {
  284. rethrow;
  285. } else {
  286. if (await NetworkHelper.instance.isNetworkAvailable()) {
  287. final url = await ApiDomains.instance.getNextApiUrl();
  288. log(TAG, 'Launch request failed for URL $url: $e');
  289. if (url.isEmpty) {
  290. rethrow;
  291. }
  292. ApiCore().setbaseUrl(url);
  293. } else {
  294. rethrow;
  295. }
  296. }
  297. } catch (e) {
  298. final url = await ApiDomains.instance.getNextApiUrl();
  299. log(TAG, 'Launch request failed for URL $url: $e');
  300. if (url.isEmpty) {
  301. rethrow;
  302. }
  303. ApiCore().setbaseUrl(url);
  304. }
  305. }
  306. }
  307. Future<Launch> refreshLaunch() async {
  308. while (true) {
  309. try {
  310. ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
  311. final request = fp.toJson();
  312. final result = await ApiCore().refreshLaunch(request);
  313. if (!result.success) {
  314. throw Failure(
  315. code: result.errorCode ?? '',
  316. message: result.errorMessage ?? '',
  317. );
  318. }
  319. // 重置禁用状态
  320. IXSP.setLastIsRegionDisabled(false);
  321. IXSP.setLastIsUserDisabled(false);
  322. final launchData = Launch.fromJson(result.data);
  323. // 设置扩展数据
  324. fp.exData = launchData.exData;
  325. // 更新URL列表
  326. await ApiDomains.instance.updateFromLaunch(launchData);
  327. // 保存Launch数据
  328. await IXSP.saveLaunch(launchData);
  329. // 初始化Launch
  330. await initData(launchData);
  331. return launchData;
  332. } on ApiException catch (e) {
  333. final url = await ApiDomains.instance.getNextApiUrl();
  334. log(TAG, 'refresh launch request failed for URL $url: $e');
  335. if (url.isEmpty) {
  336. rethrow;
  337. }
  338. ApiCore().setbaseUrl(url);
  339. } on Failure catch (_) {
  340. rethrow;
  341. } on DioException catch (e) {
  342. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  343. e.response?.statusCode == Errors.eUserDisabled ||
  344. e.response?.statusCode == Errors.eTokenExpired) {
  345. rethrow;
  346. } else {
  347. if (await NetworkHelper.instance.isNetworkAvailable()) {
  348. final url = await ApiDomains.instance.getNextApiUrl();
  349. log(TAG, 'refresh launch request failed for URL $url: $e');
  350. if (url.isEmpty) {
  351. rethrow;
  352. }
  353. ApiCore().setbaseUrl(url);
  354. } else {
  355. rethrow;
  356. }
  357. }
  358. } catch (e) {
  359. final url = await ApiDomains.instance.getNextApiUrl();
  360. log(TAG, 'refresh launch request failed for URL $url: $e');
  361. if (url.isEmpty) {
  362. rethrow;
  363. }
  364. ApiCore().setbaseUrl(url);
  365. }
  366. }
  367. }
  368. Future<void> asyncHandleLaunch({bool isRefreshLaunch = false}) async {
  369. try {
  370. final data = isRefreshLaunch
  371. ? await refreshLaunch()
  372. : await launch(isCache: true);
  373. final isVpnRunning = await BaseCoreApi().isConnected() ?? false;
  374. if (!isVpnRunning) {
  375. await checkUpdate();
  376. // 下载smartgeo文件
  377. GeoDownloader().downloadSmartGeo(smartGeo: data.appConfig!.smartGeo!);
  378. }
  379. } catch (e, s) {
  380. if (IXSP.getLastIsUserDisabled()) {
  381. if (!isShowDisabled) {
  382. Get.offAll(
  383. () => CountryRestrictedOverlay(
  384. type: RestrictedType.user,
  385. onPressed: () async {
  386. // 清除LaunchData
  387. await IXSP.clearLaunchData();
  388. // 清除禁用状态
  389. IXSP.setLastIsUserDisabled(false);
  390. // 发送事件
  391. },
  392. ),
  393. transition: Transition.fadeIn,
  394. );
  395. }
  396. return;
  397. } else if (IXSP.getLastIsRegionDisabled()) {
  398. if (!isShowDisabled) {
  399. Get.offAll(
  400. () => const CountryRestrictedOverlay(type: RestrictedType.region),
  401. transition: Transition.fadeIn,
  402. );
  403. }
  404. return;
  405. } else if (IXSP.getLastIsDeviceDisabled()) {
  406. if (!isShowDisabled) {
  407. Get.offAll(
  408. () => const CountryRestrictedOverlay(type: RestrictedType.device),
  409. transition: Transition.fadeIn,
  410. );
  411. }
  412. return;
  413. }
  414. final isVpnRunning = await BaseCoreApi().isConnected() ?? false;
  415. if (!isVpnRunning) {
  416. await checkUpdate();
  417. }
  418. handleSnackBarError(e, s);
  419. }
  420. }
  421. void handleSnackBarError(dynamic error, StackTrace stackTrace) {
  422. if (error is ApiException) {
  423. IXSnackBar.showIXErrorSnackBar(
  424. title: Strings.error.tr,
  425. message: error.message,
  426. );
  427. } else if (error is Failure) {
  428. IXSnackBar.showIXErrorSnackBar(
  429. title: Strings.error.tr,
  430. message: error.message ?? Strings.unknownError.tr,
  431. );
  432. } else if (error is DioException) {
  433. switch (error.type) {
  434. case DioExceptionType.connectionError:
  435. case DioExceptionType.connectionTimeout:
  436. case DioExceptionType.receiveTimeout:
  437. case DioExceptionType.sendTimeout:
  438. IXSnackBar.showIXErrorSnackBar(
  439. title: Strings.error.tr,
  440. message: Strings.unableToConnectNetwork.tr,
  441. );
  442. break;
  443. default:
  444. IXSnackBar.showIXErrorSnackBar(
  445. title: Strings.error.tr,
  446. message: Strings.unableToConnectServer.tr,
  447. );
  448. }
  449. } else {
  450. IXSnackBar.showIXErrorSnackBar(
  451. title: Strings.error.tr,
  452. message: error.toString(),
  453. );
  454. }
  455. }
  456. // 更新检查 - 智能时间控制版本
  457. Future<bool> checkUpdate({bool isClickCheck = false}) async {
  458. try {
  459. final upgrade = IXSP.getUpgrade();
  460. var hasUpdate = false;
  461. var hasForceUpdate = false;
  462. if (upgrade != null) {
  463. if (upgrade.upgradeType == 1) {
  464. hasUpdate = true;
  465. }
  466. if (upgrade.forced == true) {
  467. hasForceUpdate = true;
  468. }
  469. }
  470. if (hasUpdate) {
  471. AllDialog.showUpdate(hasForceUpdate: hasForceUpdate);
  472. }
  473. return hasUpdate;
  474. } catch (e) {
  475. log(TAG, 'checkUpdate error: $e');
  476. }
  477. return false;
  478. }
  479. Future<Launch> getDispatchInfo(
  480. int locationId,
  481. String locationCode, {
  482. CancelToken? cancelToken,
  483. }) async {
  484. while (true) {
  485. try {
  486. ApiRouter().setbaseUrl(ApiDomains.instance.getRouterUrl());
  487. final request = fp.toJson();
  488. request['locationId'] = locationId;
  489. request['locationCode'] = locationCode;
  490. // 获取选中的路由模式
  491. final routingMode =
  492. IXSP.getString(SPKeys.routingModeSelected) ?? "smart";
  493. request['routingMode'] = routingMode;
  494. final result = await ApiRouter().getDispatchInfo(
  495. request,
  496. cancelToken: cancelToken,
  497. );
  498. if (!result.success) {
  499. throw Failure(
  500. code: result.errorCode ?? '',
  501. message: result.errorMessage ?? '',
  502. );
  503. }
  504. // 重置禁用状态
  505. IXSP.setLastIsRegionDisabled(false);
  506. IXSP.setLastIsUserDisabled(false);
  507. final launchData = Launch.fromJson(result.data);
  508. // 更新URL列表
  509. await ApiDomains.instance.updateFromLaunch(launchData);
  510. // 保存app配置
  511. await IXSP.saveAppConfig(launchData.appConfig!);
  512. return launchData;
  513. } on ApiException catch (_) {
  514. rethrow;
  515. } on Failure catch (_) {
  516. rethrow;
  517. } on DioException catch (e) {
  518. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  519. e.response?.statusCode == Errors.eUserDisabled ||
  520. e.response?.statusCode == Errors.eTokenExpired) {
  521. rethrow;
  522. } else {
  523. if (await NetworkHelper.instance.isNetworkAvailable()) {
  524. final url = await ApiDomains.instance.getNextRouterUrl();
  525. log(TAG, 'getDispatchInfo request failed for URL $url: $e');
  526. if (url.isEmpty) {
  527. rethrow;
  528. }
  529. ApiRouter().setbaseUrl(url);
  530. } else {
  531. rethrow;
  532. }
  533. }
  534. } catch (e) {
  535. final url = await ApiDomains.instance.getNextRouterUrl();
  536. log(TAG, 'getDispatchInfo request failed for URL $url: $e');
  537. if (url.isEmpty) {
  538. rethrow;
  539. }
  540. ApiRouter().setbaseUrl(url);
  541. }
  542. }
  543. }
  544. Future<Launch> register(Map<String, dynamic> params) async {
  545. while (true) {
  546. try {
  547. ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
  548. final request = fp.toJson();
  549. request.addAll(params);
  550. final result = await ApiCore().register(request);
  551. if (!result.success) {
  552. throw Failure(
  553. code: result.errorCode ?? '',
  554. message: result.errorMessage ?? '',
  555. );
  556. }
  557. final launchData = Launch.fromJson(result.data);
  558. // 注册成功后上报firebase注册事件
  559. sendAnalytics(FirebaseEvent.register);
  560. // 保存 Launch 数据
  561. await IXSP.saveLaunch(launchData);
  562. // 初始化Launch
  563. await initData(launchData);
  564. return launchData;
  565. } on ApiException catch (_) {
  566. rethrow;
  567. } on Failure catch (_) {
  568. rethrow;
  569. } on DioException catch (e) {
  570. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  571. e.response?.statusCode == Errors.eUserDisabled ||
  572. e.response?.statusCode == Errors.eTokenExpired) {
  573. rethrow;
  574. } else {
  575. if (await NetworkHelper.instance.isNetworkAvailable()) {
  576. final url = await ApiDomains.instance.getNextApiUrl();
  577. log(TAG, 'Register request failed for URL $url: $e');
  578. if (url.isEmpty) {
  579. rethrow;
  580. }
  581. ApiCore().setbaseUrl(url);
  582. } else {
  583. rethrow;
  584. }
  585. }
  586. } catch (e) {
  587. final url = await ApiDomains.instance.getNextApiUrl();
  588. log(TAG, 'Register request failed for URL $url: $e');
  589. if (url.isEmpty) {
  590. rethrow;
  591. }
  592. ApiCore().setbaseUrl(url);
  593. }
  594. }
  595. }
  596. Future<Launch> login(Map<String, dynamic> params) async {
  597. while (true) {
  598. try {
  599. ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
  600. final request = fp.toJson();
  601. request.addAll(params);
  602. final result = await ApiCore().login(request);
  603. if (!result.success) {
  604. throw Failure(
  605. code: result.errorCode ?? '',
  606. message: result.errorMessage ?? '',
  607. );
  608. }
  609. final launchData = Launch.fromJson(result.data);
  610. // 注册成功后上报firebase注册事件
  611. sendAnalytics(FirebaseEvent.login);
  612. // 保存 Launch 数据
  613. await IXSP.saveLaunch(launchData);
  614. // 初始化Launch
  615. await initData(launchData);
  616. return launchData;
  617. } on ApiException catch (_) {
  618. rethrow;
  619. } on Failure catch (_) {
  620. rethrow;
  621. } on DioException catch (e) {
  622. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  623. e.response?.statusCode == Errors.eUserDisabled ||
  624. e.response?.statusCode == Errors.eTokenExpired) {
  625. rethrow;
  626. } else {
  627. if (await NetworkHelper.instance.isNetworkAvailable()) {
  628. final url = await ApiDomains.instance.getNextApiUrl();
  629. log(TAG, 'Login request failed for URL $url: $e');
  630. if (url.isEmpty) {
  631. rethrow;
  632. }
  633. ApiCore().setbaseUrl(url);
  634. } else {
  635. rethrow;
  636. }
  637. }
  638. } catch (e) {
  639. final url = await ApiDomains.instance.getNextApiUrl();
  640. log(TAG, 'Login request failed for URL $url: $e');
  641. if (url.isEmpty) {
  642. rethrow;
  643. }
  644. ApiCore().setbaseUrl(url);
  645. }
  646. }
  647. }
  648. Future<Launch> logout() async {
  649. try {
  650. final request = fp.toJson();
  651. final result = await ApiCore().logout(request);
  652. if (!result.success) {
  653. throw Failure(
  654. code: result.errorCode ?? '',
  655. message: result.errorMessage ?? '',
  656. );
  657. }
  658. final launchData = Launch.fromJson(result.data);
  659. // 登出成功后上报firebase登出事件
  660. sendAnalytics(FirebaseEvent.logout);
  661. // 保存 Launch 数据
  662. await IXSP.saveLaunch(launchData);
  663. await initData(launchData);
  664. return launchData;
  665. } catch (e) {
  666. rethrow;
  667. }
  668. }
  669. Future<Launch> deleteAccount() async {
  670. try {
  671. final request = fp.toJson();
  672. final result = await ApiCore().deleteAccount(request);
  673. if (!result.success) {
  674. throw Failure(
  675. code: result.errorCode ?? '',
  676. message: result.errorMessage ?? '',
  677. );
  678. }
  679. final launchData = Launch.fromJson(result.data);
  680. // 登出成功后上报firebase登出事件
  681. sendAnalytics(FirebaseEvent.deleteAccount);
  682. // 保存 Launch 数据
  683. await IXSP.saveLaunch(launchData);
  684. await initData(launchData);
  685. return launchData;
  686. } catch (e) {
  687. rethrow;
  688. }
  689. }
  690. Future<String> changePassword(Map<String, dynamic> params) async {
  691. try {
  692. final request = fp.toJson();
  693. request.addAll(params);
  694. final result = await ApiCore().changePassword(request);
  695. if (!result.success) {
  696. throw Failure(
  697. code: result.errorCode ?? '',
  698. message: result.errorMessage ?? '',
  699. );
  700. }
  701. return result.errorMessage ?? '';
  702. } catch (e) {
  703. rethrow;
  704. }
  705. }
  706. Future<Groups> getLocations() async {
  707. while (true) {
  708. try {
  709. ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl());
  710. final request = fp.toJson();
  711. final result = await ApiCore().getLocations(request);
  712. if (!result.success) {
  713. throw Failure(
  714. code: result.errorCode ?? '',
  715. message: result.errorMessage ?? '',
  716. );
  717. }
  718. final groups = Groups.fromJson(result.data);
  719. await IXSP.saveGroups(groups);
  720. return groups;
  721. } on ApiException catch (_) {
  722. rethrow;
  723. } on Failure catch (_) {
  724. rethrow;
  725. } on DioException catch (e) {
  726. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  727. e.response?.statusCode == Errors.eUserDisabled ||
  728. e.response?.statusCode == Errors.eTokenExpired) {
  729. rethrow;
  730. } else {
  731. if (await NetworkHelper.instance.isNetworkAvailable()) {
  732. final url = await ApiDomains.instance.getNextApiUrl();
  733. log(TAG, 'getLocations request failed for URL $url: $e');
  734. if (url.isEmpty) {
  735. rethrow;
  736. }
  737. ApiCore().setbaseUrl(url);
  738. } else {
  739. rethrow;
  740. }
  741. }
  742. } catch (e) {
  743. final url = await ApiDomains.instance.getNextApiUrl();
  744. log(TAG, 'getLocations request failed for URL $url: $e');
  745. if (url.isEmpty) {
  746. rethrow;
  747. }
  748. ApiCore().setbaseUrl(url);
  749. }
  750. }
  751. }
  752. Future<List<ChannelPlan>> getChannelPlanList() async {
  753. try {
  754. final request = fp.toJson();
  755. final result = await ApiCore().getChannelPlanList(request);
  756. if (!result.success) {
  757. throw Failure(
  758. code: result.errorCode ?? '',
  759. message: result.errorMessage ?? '',
  760. );
  761. }
  762. final channelPlanList = ChannelPlanList.fromJson(result.data);
  763. return channelPlanList.list ?? [];
  764. } catch (e) {
  765. rethrow;
  766. }
  767. }
  768. Future<Launch> subscribe(Map<String, dynamic> params) async {
  769. try {
  770. final request = fp.toJson();
  771. request.addAll(params);
  772. final result = await ApiCore().subscribe(request);
  773. if (!result.success) {
  774. throw Failure(
  775. code: result.errorCode ?? '',
  776. message: result.errorMessage ?? '',
  777. );
  778. }
  779. final launchData = Launch.fromJson(result.data);
  780. // 登出成功后上报firebase登出事件
  781. sendAnalytics(FirebaseEvent.subscribe);
  782. // 保存 Launch 数据
  783. await IXSP.saveLaunch(launchData);
  784. // 初始化Launch
  785. await initData(launchData);
  786. return launchData;
  787. } catch (e) {
  788. rethrow;
  789. }
  790. }
  791. Future<Launch> restore() async {
  792. try {
  793. final request = fp.toJson();
  794. final result = await ApiCore().restore(request);
  795. if (!result.success) {
  796. throw Failure(
  797. code: result.errorCode ?? '',
  798. message: result.errorMessage ?? '',
  799. );
  800. }
  801. final launchData = Launch.fromJson(result.data);
  802. // 登出成功后上报firebase登出事件
  803. sendAnalytics(FirebaseEvent.restore);
  804. // 保存 Launch 数据
  805. await IXSP.saveLaunch(launchData);
  806. // 初始化Launch
  807. await initData(launchData);
  808. return launchData;
  809. } catch (e) {
  810. rethrow;
  811. }
  812. }
  813. Future<void> connected(Map<String, dynamic> params) async {
  814. try {
  815. final request = fp.toJson();
  816. request.addAll(params);
  817. final result = await ApiRouter().connected(request);
  818. if (!result.success) {
  819. throw Failure(
  820. code: result.errorCode ?? '',
  821. message: result.errorMessage ?? '',
  822. );
  823. }
  824. } catch (e) {
  825. rethrow;
  826. }
  827. }
  828. Future<BannerList> getBanner({String position = "banner"}) async {
  829. try {
  830. final request = fp.toJson();
  831. request["position"] = position;
  832. final result = await ApiCore().getBanner(request);
  833. if (!result.success) {
  834. throw Failure(
  835. code: result.errorCode ?? '',
  836. message: result.errorMessage ?? '',
  837. );
  838. }
  839. final bannerList = BannerList.fromJson(result.data);
  840. return bannerList;
  841. } catch (e) {
  842. rethrow;
  843. }
  844. }
  845. Future<String> uploadLogs(List<dynamic> items, {bool isCache = false}) async {
  846. await updateFingerprintData();
  847. Map<String, dynamic> request = fp.toJson();
  848. request['items'] = items;
  849. while (true) {
  850. try {
  851. ApiLog().setbaseUrl(ApiDomains.instance.getLogUrl());
  852. final result = await ApiLog().uploadLogs(request);
  853. if (!result.success) {
  854. throw Failure(
  855. code: result.errorCode ?? '',
  856. message: result.errorMessage ?? '',
  857. );
  858. }
  859. return result.errorMessage ?? '';
  860. } on ApiException catch (_) {
  861. rethrow;
  862. } on Failure catch (_) {
  863. rethrow;
  864. } on DioException catch (e) {
  865. if (e.response?.statusCode == Errors.eRegionNotAvailable ||
  866. e.response?.statusCode == Errors.eUserDisabled ||
  867. e.response?.statusCode == Errors.eTokenExpired) {
  868. if (isCache) {
  869. await _cacheLogRequest(items);
  870. }
  871. rethrow;
  872. } else {
  873. if (await NetworkHelper.instance.isNetworkAvailable()) {
  874. final url = await ApiDomains.instance.getNextLogUrl();
  875. if (url.isEmpty) {
  876. if (isCache) {
  877. await _cacheLogRequest(items);
  878. }
  879. rethrow;
  880. }
  881. log(
  882. TAG,
  883. 'uploadLogs request failed for URL ${ApiLog().baseUrl}: $e',
  884. );
  885. ApiLog().setbaseUrl(url);
  886. } else {
  887. if (isCache) {
  888. await _cacheLogRequest(items);
  889. }
  890. rethrow;
  891. }
  892. }
  893. } catch (e) {
  894. final url = await ApiDomains.instance.getNextLogUrl();
  895. if (url.isEmpty) {
  896. if (isCache) {
  897. await _cacheLogRequest(items);
  898. }
  899. rethrow;
  900. }
  901. log(TAG, 'uploadLogs request failed for URL ${ApiLog().baseUrl}: $e');
  902. ApiLog().setbaseUrl(url);
  903. }
  904. }
  905. }
  906. // 缓存日志请求数据
  907. Future<void> _cacheLogRequest(dynamic items) async {
  908. try {
  909. final dir = await getApplicationDocumentsDirectory();
  910. final cacheDir = Directory('${dir.path}/log_cache');
  911. if (!await cacheDir.exists()) {
  912. await cacheDir.create(recursive: true);
  913. }
  914. // 生成唯一的文件名
  915. final timestamp = DateTime.now().millisecondsSinceEpoch;
  916. final fileName = 'log_$timestamp.json';
  917. final file = File('${cacheDir.path}/$fileName');
  918. // 将请求数据写入文件
  919. await file.writeAsString(jsonEncode(items));
  920. // 清理缓存目录
  921. await _cleanupCacheDirectory(cacheDir);
  922. } catch (e) {
  923. log(TAG, 'Failed to cache log request: $e');
  924. }
  925. }
  926. // 清理缓存目录
  927. Future<void> _cleanupCacheDirectory(Directory cacheDir) async {
  928. try {
  929. const maxFiles = 10; // 最多保留10个文件
  930. const maxCacheSize = 5 * 1024 * 1024; // 5MB
  931. final files = await cacheDir.list().toList();
  932. // 按修改时间排序
  933. files.sort(
  934. (a, b) => a.statSync().modified.compareTo(b.statSync().modified),
  935. );
  936. // 计算当前缓存大小
  937. int totalSize = 0;
  938. for (var file in files) {
  939. totalSize += file.statSync().size;
  940. }
  941. // 如果超过文件数量或大小限制,删除最旧的文件
  942. while ((files.length > maxFiles || totalSize > maxCacheSize) &&
  943. files.isNotEmpty) {
  944. final oldestFile = files.removeAt(0);
  945. totalSize -= oldestFile.statSync().size;
  946. await oldestFile.delete();
  947. }
  948. } catch (e) {
  949. log(TAG, 'Failed to cleanup cache directory: $e');
  950. }
  951. }
  952. Future<void> uploadApiStatisticsLog(
  953. List<Map<String, dynamic>> logs, {
  954. LogModule module = LogModule.NM_ApiLaunchLog,
  955. }) async {
  956. if (isNeedUploadLogs(module)) {
  957. await uploadLogs(logs);
  958. }
  959. }
  960. // 判断是否需要上传日志
  961. bool isNeedUploadLogs(LogModule module) {
  962. final launch = IXSP.getLaunch();
  963. if (launch == null) {
  964. return false;
  965. }
  966. if (launch.appConfig?.disabledLogModules?.contains(module.name) ?? false) {
  967. return false;
  968. }
  969. return true;
  970. }
  971. /// 获取订阅周期类型文本
  972. /// subscribeType: 1Day 2Week 3Month 4Year
  973. String _getSubscribeTypeText() {
  974. final user = IXSP.getUser();
  975. final planInfo = user?.planInfo;
  976. // 仅当 isSubscribe=true 时有效
  977. if (planInfo?.isSubscribe != true) {
  978. return planInfo?.subTitle ?? '';
  979. }
  980. switch (planInfo?.subscribeType) {
  981. case 1:
  982. return 'Day';
  983. case 2:
  984. return 'Week';
  985. case 3:
  986. return 'Month';
  987. case 4:
  988. return 'Year';
  989. default:
  990. return '';
  991. }
  992. }
  993. /// 获取过期时间文本
  994. String _getExpireTimeText() {
  995. final user = IXSP.getUser();
  996. final expireTime = user?.expireTime;
  997. if (expireTime == null || expireTime == 0) {
  998. return '';
  999. }
  1000. // 时间戳转日期(秒级时间戳)
  1001. final date = DateTime.fromMillisecondsSinceEpoch(expireTime * 1000);
  1002. final formatted =
  1003. "${date.year.toString().padLeft(4, '0')}-"
  1004. "${date.month.toString().padLeft(2, '0')}-"
  1005. "${date.day.toString().padLeft(2, '0')}";
  1006. return formatted;
  1007. }
  1008. /// 获取有效期显示文本
  1009. String _getValidTermText() {
  1010. final subscribeType = _getSubscribeTypeText();
  1011. final expireTime = _getExpireTimeText();
  1012. if (subscribeType.isNotEmpty && expireTime.isNotEmpty) {
  1013. return '$subscribeType / $expireTime';
  1014. } else if (expireTime.isNotEmpty) {
  1015. return expireTime;
  1016. }
  1017. return '';
  1018. }
  1019. /// 启动剩余时间倒计时
  1020. /// [seconds] 剩余时间(秒),例如:3600 表示 1 小时
  1021. void startRemainTimeCountdown(int seconds) {
  1022. // 取消之前的定时器
  1023. _remainTimeTimer?.cancel();
  1024. // 设置初始剩余时间
  1025. _remainTimeSeconds = seconds;
  1026. // 立即更新格式化文案
  1027. _updateRemainTimeFormatted();
  1028. // 启动每秒倒计时
  1029. _remainTimeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
  1030. if (_remainTimeSeconds > 0) {
  1031. _remainTimeSeconds--;
  1032. // 只有当格式化文案变化时才更新 UI
  1033. _updateRemainTimeFormatted();
  1034. } else {
  1035. // 倒计时结束
  1036. timer.cancel();
  1037. _remainTimeTimer = null;
  1038. _onRemainTimeExpired();
  1039. }
  1040. });
  1041. }
  1042. /// 更新格式化后的剩余时间(只有文案变化时才触发 UI 更新)
  1043. void _updateRemainTimeFormatted() {
  1044. final newFormatted = _formatRemainTime(_remainTimeSeconds);
  1045. if (_remainTimeFormatted.value != newFormatted) {
  1046. _remainTimeFormatted.value = newFormatted;
  1047. }
  1048. // 更新是否显示倒计时的状态
  1049. final vipRemainNoticeSeconds =
  1050. (IXSP.getAppConfig()?.vipRemainNotice ?? 600) * 60;
  1051. final newShouldShow =
  1052. _remainTimeSeconds > 0 && _remainTimeSeconds < vipRemainNoticeSeconds;
  1053. if (_shouldShowCountdown.value != newShouldShow) {
  1054. _shouldShowCountdown.value = newShouldShow;
  1055. }
  1056. }
  1057. /// 停止剩余时间倒计时
  1058. void stopRemainTimeCountdown() {
  1059. _remainTimeTimer?.cancel();
  1060. _remainTimeTimer = null;
  1061. }
  1062. /// 更新剩余时间(从服务器获取新的时间后调用)
  1063. void updateRemainTime(int seconds) {
  1064. stopRemainTimeCountdown();
  1065. if (seconds > 0) {
  1066. startRemainTimeCountdown(seconds);
  1067. } else {
  1068. _remainTimeSeconds = 0;
  1069. _updateRemainTimeFormatted();
  1070. }
  1071. }
  1072. /// 剩余时间到期处理
  1073. void _onRemainTimeExpired() {
  1074. log(TAG, 'VIP剩余时间已到期');
  1075. // 可以在这里添加到期后的处理逻辑,例如:
  1076. // - 显示续费提示
  1077. // - 断开VPN连接
  1078. // - 刷新用户信息
  1079. }
  1080. /// 格式化剩余时间显示
  1081. String _formatRemainTime(int totalSeconds) {
  1082. if (totalSeconds <= 0) {
  1083. return '';
  1084. }
  1085. final days = totalSeconds ~/ 86400;
  1086. final hours = (totalSeconds % 86400) ~/ 3600;
  1087. final minutes = (totalSeconds % 3600) ~/ 60;
  1088. final seconds = totalSeconds % 60;
  1089. // 大于1天
  1090. if (days > 1) {
  1091. return '$days days';
  1092. }
  1093. // 等于1天
  1094. if (days == 1) {
  1095. return '1 day';
  1096. }
  1097. // 大于1小时
  1098. if (hours >= 1) {
  1099. return '$hours h';
  1100. }
  1101. // 小于1小时,显示 mm:ss
  1102. return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
  1103. }
  1104. }