import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:nomo/app/api/router/api_router.dart'; import 'package:nomo/app/data/sp/ix_sp.dart'; import 'package:nomo/app/constants/sp_keys.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:play_install_referrer/play_install_referrer.dart'; import '../../config/translations/localization_service.dart'; import '../../config/translations/strings_enum.dart'; import '../data/models/banner/banner_list.dart'; import 'base_core_api.dart'; import '../../utils/api_statistics.dart'; import '../../utils/device_manager.dart'; import '../../utils/geo_downloader.dart'; import '../../utils/log/logger.dart'; import '../../utils/network_helper.dart'; import '../../utils/ntp_time_service.dart'; import '../api/core/api_core.dart'; import '../api/log/api_log.dart'; import '../components/country_restricted_overlay.dart'; import '../components/ix_snackbar.dart'; import '../constants/api_domains.dart'; import '../constants/configs.dart'; import '../constants/enums.dart'; import '../constants/errors.dart'; import '../constants/platforms.dart'; import '../data/models/api_exception.dart'; import '../data/models/channelplan/channel_plan_list.dart'; import '../data/models/failure.dart'; import '../data/models/fingerprint.dart'; import '../data/models/launch/groups.dart'; import '../data/models/launch/launch.dart'; import '../dialog/all_dialog.dart'; class ApiController extends GetxService with WidgetsBindingObserver { final TAG = 'ApiController'; // 记录是否已经显示禁用弹窗 bool isShowDisabled = false; //是否是游客 final _isGuest = false.obs; bool get isGuest => _isGuest.value; set isGuest(bool value) => _isGuest.value = value; //是否是会员 final _isPremium = false.obs; bool get isPremium => _isPremium.value; set isPremium(bool value) => _isPremium.value = value; //用户等级 final _userLevel = 1.obs; int get userLevel => _userLevel.value; set userLevel(int value) => _userLevel.value = value; // 过期时间文本 final _expireTimeText = ''.obs; String get expireTimeText => _expireTimeText.value; set expireTimeText(String value) => _expireTimeText.value = value; // 有效期文本 final _validTermText = ''.obs; String get validTermText => _validTermText.value; set validTermText(String value) => _validTermText.value = value; //全部节点列表 final _nodesList = [].obs; List get nodesList => _nodesList.value; set nodesList(List value) => _nodesList.value = value; //初始化fingerprint Fingerprint fp = Fingerprint.empty(); // 全局剩余时间倒计时(秒) int _remainTimeSeconds = 0; int get remainTimeSeconds => _remainTimeSeconds; // 格式化后的剩余时间字符串(响应式,只有文案变化时才更新 UI) final _remainTimeFormatted = ''.obs; String get remainTimeFormatted => _remainTimeFormatted.value; // 是否应该显示倒计时(响应式,只有状态变化时才更新 UI) final _shouldShowCountdown = false.obs; bool get shouldShowCountdown => _shouldShowCountdown.value; // 倒计时定时器 Timer? _remainTimeTimer; // 是否在后台 bool isBackground = false; @override void onInit() { super.onInit(); WidgetsBinding.instance.addObserver(this); } @override void onClose() { WidgetsBinding.instance.removeObserver(this); super.onClose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { log(TAG, "App state: $state"); if (state == AppLifecycleState.paused) { isBackground = true; ApiStatistics.instance.onAppPaused(); stopRemainTimeCountdown(); } else if (state == AppLifecycleState.resumed) { if (isBackground) { isBackground = false; asyncHandleLaunch(isRefreshLaunch: true); ApiStatistics.instance.onAppResumed(); updateRemainTime(IXSP.getUser()?.remainTime ?? 0); } } } Future initFingerprint() async { // 读取app发布渠道 if (Platform.isIOS) { fp.channel = 'apple'; } else if (Platform.isAndroid) { try { final channel = await BaseCoreApi().getChannel(); fp.channel = channel ?? 'unknown'; } catch (e) { log(TAG, 'read app channel error: $e'); fp.channel = ''; } try { final advertisingId = await BaseCoreApi().getAdvertisingId(); fp.googleId = advertisingId ?? ''; } catch (e) { log(TAG, 'read app googleId error: $e'); fp.googleId = ''; } try { ReferrerDetails referrerDetails = await PlayInstallReferrer.installReferrer; fp.refer = referrerDetails.installReferrer ?? ''; } catch (e) { log(TAG, 'get install referrer error: $e'); fp.refer = ''; } } else if (Platform.isWindows) { fp.channel = 'universal'; } // 读取应用信息 final info = await PackageInfo.fromPlatform(); fp.appVersionCode = int.tryParse(info.buildNumber) ?? 0; fp.appVersionName = info.version; // 读取设备信息 final deviceInfo = DeviceInfoPlugin(); if (Platform.isIOS) { fp.platform = Platforms.iOS; final iosOsInfo = await deviceInfo.iosInfo; fp.deviceModel = iosOsInfo.model; fp.deviceOs = iosOsInfo.systemVersion; fp.deviceBrand = iosOsInfo.utsname.machine; } else if (Platform.isAndroid) { fp.platform = Platforms.android; final androidOsInfo = await deviceInfo.androidInfo; fp.deviceModel = androidOsInfo.model; fp.deviceOs = androidOsInfo.version.release; fp.deviceBrand = androidOsInfo.brand; fp.androidId = androidOsInfo.id; } else if (Platform.isWindows) { fp.platform = Platforms.windows; final windowsInfo = await deviceInfo.windowsInfo; fp.deviceModel = windowsInfo.productName; fp.deviceOs = windowsInfo.csdVersion; fp.deviceBrand = windowsInfo.computerName; } //获取设备尺寸 fp.deviceHeight = Get.height.toInt(); fp.deviceWidth = Get.width.toInt(); // 读取设备ID fp.deviceId = DeviceManager.getCacheDeviceId(); fp.isNewInstall = IXSP.getIsNewInstall(); await updateFingerprintData(); return fp; } // 更新部分数据 Future updateFingerprintData() async { fp.lang = IXSP.getCurrentLocal().languageCode; fp.phoneCountryIso = LocalizationService.getSystemCountry(); fp.isVpn = await BaseCoreApi().isConnected() ?? false; if (!fp.isVpn) { fp.isConnectedVpn = false; } try { final simInfo = await BaseCoreApi().getSimInfo(); // 解析sim final sim = jsonDecode(simInfo ?? '{}'); fp.simReady = sim['simReady']; fp.carrierName = sim['carrierName']; fp.mcc = sim['mcc']; fp.mnc = sim['mnc']; fp.countryIso = sim['countryIso']; fp.networkCarrierName = sim['networkCarrierName']; fp.networkMcc = sim['networkMcc']; fp.networkMnc = sim['networkMnc']; fp.networkCountryIso = sim['networkCountryIso']; } catch (e) { log(TAG, 'read app sim error: $e'); } } Future initData(Launch? launch) async { // 初始化是否第一次安装 IXSP.setIsNewInstall(false); fp.userUuid = ''; fp.isNewInstall = false; await initLaunch(launch); } Future initLaunch(Launch? launch) async { try { if (launch != null) { // 初始化用户状态 isGuest = launch.userConfig?.memberLevel == MemberLevel.guest.level; userLevel = launch.userConfig?.userLevel ?? 1; isPremium = userLevel == 3 || userLevel == 9999; expireTimeText = _getExpireTimeText(); validTermText = _getValidTermText(); updateRemainTime(launch.userConfig?.remainTime ?? 0); NtpTimeService().initLaunchInitialTime(); // 设置路由和节点 nodesList = launch.groups?.normal?.list ?? []; // 设置资源url if (launch.appConfig?.assetUrls != null && launch.appConfig!.assetUrls!.isNotEmpty) { Configs.assetUrl = launch.appConfig!.assetUrls![0]; } // 设置官网url if (launch.appConfig?.websiteUrl != null && launch.appConfig!.websiteUrl!.isNotEmpty) { Configs.websiteUrl = launch.appConfig!.websiteUrl!; } } } catch (e) { log(TAG, 'initLaunch error: $e'); } } // 发送分析事件, 后续可以发送到firebase Future sendAnalytics(FirebaseEvent event) async { try {} catch (e) { log('sendAnalytics error: $e'); } } Future launch({bool isCache = false}) async { sendAnalytics(isCache ? FirebaseEvent.launchCache : FirebaseEvent.launch); while (true) { try { ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl()); final request = fp.toJson(); final result = await ApiCore().launch(request); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } sendAnalytics( isCache ? FirebaseEvent.launchCacheSuccess : FirebaseEvent.launchSuccess, ); // 重置禁用状态 IXSP.setLastIsRegionDisabled(false); IXSP.setLastIsUserDisabled(false); final launchData = Launch.fromJson(result.data); // 设置扩展数据 fp.exData = launchData.exData; // 更新URL列表 await ApiDomains.instance.updateFromLaunch(launchData); // 保存Launch数据 await IXSP.saveLaunch(launchData); // 初始化Launch await initData(launchData); return launchData; } on ApiException catch (e) { final url = await ApiDomains.instance.getNextApiUrl(); log(TAG, 'Launch request failed for URL $url: $e'); if (url.isEmpty) { rethrow; } ApiCore().setbaseUrl(url); } on Failure catch (_) { rethrow; } on DioException catch (e) { if (e.response?.statusCode == Errors.eRegionNotAvailable || e.response?.statusCode == Errors.eUserDisabled || e.response?.statusCode == Errors.eTokenExpired) { rethrow; } else { if (await NetworkHelper.instance.isNetworkAvailable()) { final url = await ApiDomains.instance.getNextApiUrl(); log(TAG, 'Launch request failed for URL $url: $e'); if (url.isEmpty) { rethrow; } ApiCore().setbaseUrl(url); } else { rethrow; } } } catch (e) { final url = await ApiDomains.instance.getNextApiUrl(); log(TAG, 'Launch request failed for URL $url: $e'); if (url.isEmpty) { rethrow; } ApiCore().setbaseUrl(url); } } } Future refreshLaunch() async { while (true) { try { ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl()); final request = fp.toJson(); final result = await ApiCore().refreshLaunch(request); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } // 重置禁用状态 IXSP.setLastIsRegionDisabled(false); IXSP.setLastIsUserDisabled(false); final launchData = Launch.fromJson(result.data); // 设置扩展数据 fp.exData = launchData.exData; // 更新URL列表 await ApiDomains.instance.updateFromLaunch(launchData); // 保存Launch数据 await IXSP.saveLaunch(launchData); // 初始化Launch await initData(launchData); return launchData; } on ApiException catch (e) { final url = await ApiDomains.instance.getNextApiUrl(); log(TAG, 'refresh launch request failed for URL $url: $e'); if (url.isEmpty) { rethrow; } ApiCore().setbaseUrl(url); } on Failure catch (_) { rethrow; } on DioException catch (e) { if (e.response?.statusCode == Errors.eRegionNotAvailable || e.response?.statusCode == Errors.eUserDisabled || e.response?.statusCode == Errors.eTokenExpired) { rethrow; } else { if (await NetworkHelper.instance.isNetworkAvailable()) { final url = await ApiDomains.instance.getNextApiUrl(); log(TAG, 'refresh launch request failed for URL $url: $e'); if (url.isEmpty) { rethrow; } ApiCore().setbaseUrl(url); } else { rethrow; } } } catch (e) { final url = await ApiDomains.instance.getNextApiUrl(); log(TAG, 'refresh launch request failed for URL $url: $e'); if (url.isEmpty) { rethrow; } ApiCore().setbaseUrl(url); } } } Future asyncHandleLaunch({bool isRefreshLaunch = false}) async { try { final data = isRefreshLaunch ? await refreshLaunch() : await launch(isCache: true); final isVpnRunning = await BaseCoreApi().isConnected() ?? false; if (!isVpnRunning) { await checkUpdate(); // 下载smartgeo文件 GeoDownloader().downloadSmartGeo(smartGeo: data.appConfig!.smartGeo!); } } catch (e, s) { if (IXSP.getLastIsUserDisabled()) { if (!isShowDisabled) { Get.offAll( () => CountryRestrictedOverlay( type: RestrictedType.user, onPressed: () async { // 清除LaunchData await IXSP.clearLaunchData(); // 清除禁用状态 IXSP.setLastIsUserDisabled(false); // 发送事件 }, ), transition: Transition.fadeIn, ); } return; } else if (IXSP.getLastIsRegionDisabled()) { if (!isShowDisabled) { Get.offAll( () => const CountryRestrictedOverlay(type: RestrictedType.region), transition: Transition.fadeIn, ); } return; } else if (IXSP.getLastIsDeviceDisabled()) { if (!isShowDisabled) { Get.offAll( () => const CountryRestrictedOverlay(type: RestrictedType.device), transition: Transition.fadeIn, ); } return; } final isVpnRunning = await BaseCoreApi().isConnected() ?? false; if (!isVpnRunning) { await checkUpdate(); } handleSnackBarError(e, s); } } void handleSnackBarError(dynamic error, StackTrace stackTrace) { if (error is ApiException) { IXSnackBar.showIXErrorSnackBar( title: Strings.error.tr, message: error.message, ); } else if (error is Failure) { IXSnackBar.showIXErrorSnackBar( title: Strings.error.tr, message: error.message ?? Strings.unknownError.tr, ); } else if (error is DioException) { switch (error.type) { case DioExceptionType.connectionError: case DioExceptionType.connectionTimeout: case DioExceptionType.receiveTimeout: case DioExceptionType.sendTimeout: IXSnackBar.showIXErrorSnackBar( title: Strings.error.tr, message: Strings.unableToConnectNetwork.tr, ); break; default: IXSnackBar.showIXErrorSnackBar( title: Strings.error.tr, message: Strings.unableToConnectServer.tr, ); } } else { IXSnackBar.showIXErrorSnackBar( title: Strings.error.tr, message: error.toString(), ); } } // 更新检查 - 智能时间控制版本 Future checkUpdate({bool isClickCheck = false}) async { try { final upgrade = IXSP.getUpgrade(); var hasUpdate = false; var hasForceUpdate = false; if (upgrade != null) { if (upgrade.upgradeType == 1) { hasUpdate = true; } if (upgrade.forced == true) { hasForceUpdate = true; } } if (hasUpdate) { AllDialog.showUpdate(hasForceUpdate: hasForceUpdate); } return hasUpdate; } catch (e) { log(TAG, 'checkUpdate error: $e'); } return false; } Future getDispatchInfo( int locationId, String locationCode, { CancelToken? cancelToken, }) async { while (true) { try { ApiRouter().setbaseUrl(ApiDomains.instance.getRouterUrl()); final request = fp.toJson(); request['locationId'] = locationId; request['locationCode'] = locationCode; // 获取选中的路由模式 final routingMode = IXSP.getString(SPKeys.routingModeSelected) ?? "smart"; request['routingMode'] = routingMode; final result = await ApiRouter().getDispatchInfo( request, cancelToken: cancelToken, ); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } // 重置禁用状态 IXSP.setLastIsRegionDisabled(false); IXSP.setLastIsUserDisabled(false); final launchData = Launch.fromJson(result.data); // 更新URL列表 await ApiDomains.instance.updateFromLaunch(launchData); // 保存app配置 await IXSP.saveAppConfig(launchData.appConfig!); return launchData; } on ApiException catch (_) { rethrow; } on Failure catch (_) { rethrow; } on DioException catch (e) { if (e.response?.statusCode == Errors.eRegionNotAvailable || e.response?.statusCode == Errors.eUserDisabled || e.response?.statusCode == Errors.eTokenExpired) { rethrow; } else { if (await NetworkHelper.instance.isNetworkAvailable()) { final url = await ApiDomains.instance.getNextRouterUrl(); log(TAG, 'getDispatchInfo request failed for URL $url: $e'); if (url.isEmpty) { rethrow; } ApiRouter().setbaseUrl(url); } else { rethrow; } } } catch (e) { final url = await ApiDomains.instance.getNextRouterUrl(); log(TAG, 'getDispatchInfo request failed for URL $url: $e'); if (url.isEmpty) { rethrow; } ApiRouter().setbaseUrl(url); } } } Future register(Map params) async { while (true) { try { ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl()); final request = fp.toJson(); request.addAll(params); final result = await ApiCore().register(request); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } final launchData = Launch.fromJson(result.data); // 注册成功后上报firebase注册事件 sendAnalytics(FirebaseEvent.register); // 保存 Launch 数据 await IXSP.saveLaunch(launchData); // 初始化Launch await initData(launchData); return launchData; } on ApiException catch (_) { rethrow; } on Failure catch (_) { rethrow; } on DioException catch (e) { if (e.response?.statusCode == Errors.eRegionNotAvailable || e.response?.statusCode == Errors.eUserDisabled || e.response?.statusCode == Errors.eTokenExpired) { rethrow; } else { if (await NetworkHelper.instance.isNetworkAvailable()) { final url = await ApiDomains.instance.getNextApiUrl(); log(TAG, 'Register request failed for URL $url: $e'); if (url.isEmpty) { rethrow; } ApiCore().setbaseUrl(url); } else { rethrow; } } } catch (e) { final url = await ApiDomains.instance.getNextApiUrl(); log(TAG, 'Register request failed for URL $url: $e'); if (url.isEmpty) { rethrow; } ApiCore().setbaseUrl(url); } } } Future login(Map params) async { while (true) { try { ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl()); final request = fp.toJson(); request.addAll(params); final result = await ApiCore().login(request); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } final launchData = Launch.fromJson(result.data); // 注册成功后上报firebase注册事件 sendAnalytics(FirebaseEvent.login); // 保存 Launch 数据 await IXSP.saveLaunch(launchData); // 初始化Launch await initData(launchData); return launchData; } on ApiException catch (_) { rethrow; } on Failure catch (_) { rethrow; } on DioException catch (e) { if (e.response?.statusCode == Errors.eRegionNotAvailable || e.response?.statusCode == Errors.eUserDisabled || e.response?.statusCode == Errors.eTokenExpired) { rethrow; } else { if (await NetworkHelper.instance.isNetworkAvailable()) { final url = await ApiDomains.instance.getNextApiUrl(); log(TAG, 'Login request failed for URL $url: $e'); if (url.isEmpty) { rethrow; } ApiCore().setbaseUrl(url); } else { rethrow; } } } catch (e) { final url = await ApiDomains.instance.getNextApiUrl(); log(TAG, 'Login request failed for URL $url: $e'); if (url.isEmpty) { rethrow; } ApiCore().setbaseUrl(url); } } } Future logout() async { try { final request = fp.toJson(); final result = await ApiCore().logout(request); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } final launchData = Launch.fromJson(result.data); // 登出成功后上报firebase登出事件 sendAnalytics(FirebaseEvent.logout); // 保存 Launch 数据 await IXSP.saveLaunch(launchData); await initData(launchData); return launchData; } catch (e) { rethrow; } } Future deleteAccount() async { try { final request = fp.toJson(); final result = await ApiCore().deleteAccount(request); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } final launchData = Launch.fromJson(result.data); // 登出成功后上报firebase登出事件 sendAnalytics(FirebaseEvent.deleteAccount); // 保存 Launch 数据 await IXSP.saveLaunch(launchData); await initData(launchData); return launchData; } catch (e) { rethrow; } } Future changePassword(Map params) async { try { final request = fp.toJson(); request.addAll(params); final result = await ApiCore().changePassword(request); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } return result.errorMessage ?? ''; } catch (e) { rethrow; } } Future getLocations() async { while (true) { try { ApiCore().setbaseUrl(ApiDomains.instance.getApiUrl()); final request = fp.toJson(); final result = await ApiCore().getLocations(request); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } final groups = Groups.fromJson(result.data); await IXSP.saveGroups(groups); return groups; } on ApiException catch (_) { rethrow; } on Failure catch (_) { rethrow; } on DioException catch (e) { if (e.response?.statusCode == Errors.eRegionNotAvailable || e.response?.statusCode == Errors.eUserDisabled || e.response?.statusCode == Errors.eTokenExpired) { rethrow; } else { if (await NetworkHelper.instance.isNetworkAvailable()) { final url = await ApiDomains.instance.getNextApiUrl(); log(TAG, 'getLocations request failed for URL $url: $e'); if (url.isEmpty) { rethrow; } ApiCore().setbaseUrl(url); } else { rethrow; } } } catch (e) { final url = await ApiDomains.instance.getNextApiUrl(); log(TAG, 'getLocations request failed for URL $url: $e'); if (url.isEmpty) { rethrow; } ApiCore().setbaseUrl(url); } } } Future> getChannelPlanList() async { try { final request = fp.toJson(); final result = await ApiCore().getChannelPlanList(request); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } final channelPlanList = ChannelPlanList.fromJson(result.data); return channelPlanList.list ?? []; } catch (e) { rethrow; } } Future subscribe(Map params) async { try { final request = fp.toJson(); request.addAll(params); final result = await ApiCore().subscribe(request); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } final launchData = Launch.fromJson(result.data); // 登出成功后上报firebase登出事件 sendAnalytics(FirebaseEvent.subscribe); // 保存 Launch 数据 await IXSP.saveLaunch(launchData); // 初始化Launch await initData(launchData); return launchData; } catch (e) { rethrow; } } Future restore() async { try { final request = fp.toJson(); final result = await ApiCore().restore(request); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } final launchData = Launch.fromJson(result.data); // 登出成功后上报firebase登出事件 sendAnalytics(FirebaseEvent.restore); // 保存 Launch 数据 await IXSP.saveLaunch(launchData); // 初始化Launch await initData(launchData); return launchData; } catch (e) { rethrow; } } Future connected(Map params) async { try { final request = fp.toJson(); request.addAll(params); final result = await ApiRouter().connected(request); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } } catch (e) { rethrow; } } Future getBanner({String position = "banner"}) async { try { final request = fp.toJson(); request["position"] = position; final result = await ApiCore().getBanner(request); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } final bannerList = BannerList.fromJson(result.data); return bannerList; } catch (e) { rethrow; } } Future uploadLogs(List items, {bool isCache = false}) async { await updateFingerprintData(); Map request = fp.toJson(); request['items'] = items; while (true) { try { ApiLog().setbaseUrl(ApiDomains.instance.getLogUrl()); final result = await ApiLog().uploadLogs(request); if (!result.success) { throw Failure( code: result.errorCode ?? '', message: result.errorMessage ?? '', ); } return result.errorMessage ?? ''; } on ApiException catch (_) { rethrow; } on Failure catch (_) { rethrow; } on DioException catch (e) { if (e.response?.statusCode == Errors.eRegionNotAvailable || e.response?.statusCode == Errors.eUserDisabled || e.response?.statusCode == Errors.eTokenExpired) { if (isCache) { await _cacheLogRequest(items); } rethrow; } else { if (await NetworkHelper.instance.isNetworkAvailable()) { final url = await ApiDomains.instance.getNextLogUrl(); if (url.isEmpty) { if (isCache) { await _cacheLogRequest(items); } rethrow; } log( TAG, 'uploadLogs request failed for URL ${ApiLog().baseUrl}: $e', ); ApiLog().setbaseUrl(url); } else { if (isCache) { await _cacheLogRequest(items); } rethrow; } } } catch (e) { final url = await ApiDomains.instance.getNextLogUrl(); if (url.isEmpty) { if (isCache) { await _cacheLogRequest(items); } rethrow; } log(TAG, 'uploadLogs request failed for URL ${ApiLog().baseUrl}: $e'); ApiLog().setbaseUrl(url); } } } // 缓存日志请求数据 Future _cacheLogRequest(dynamic items) async { try { final dir = await getApplicationDocumentsDirectory(); final cacheDir = Directory('${dir.path}/log_cache'); if (!await cacheDir.exists()) { await cacheDir.create(recursive: true); } // 生成唯一的文件名 final timestamp = DateTime.now().millisecondsSinceEpoch; final fileName = 'log_$timestamp.json'; final file = File('${cacheDir.path}/$fileName'); // 将请求数据写入文件 await file.writeAsString(jsonEncode(items)); // 清理缓存目录 await _cleanupCacheDirectory(cacheDir); } catch (e) { log(TAG, 'Failed to cache log request: $e'); } } // 清理缓存目录 Future _cleanupCacheDirectory(Directory cacheDir) async { try { const maxFiles = 10; // 最多保留10个文件 const maxCacheSize = 5 * 1024 * 1024; // 5MB final files = await cacheDir.list().toList(); // 按修改时间排序 files.sort( (a, b) => a.statSync().modified.compareTo(b.statSync().modified), ); // 计算当前缓存大小 int totalSize = 0; for (var file in files) { totalSize += file.statSync().size; } // 如果超过文件数量或大小限制,删除最旧的文件 while ((files.length > maxFiles || totalSize > maxCacheSize) && files.isNotEmpty) { final oldestFile = files.removeAt(0); totalSize -= oldestFile.statSync().size; await oldestFile.delete(); } } catch (e) { log(TAG, 'Failed to cleanup cache directory: $e'); } } Future uploadApiStatisticsLog( List> logs, { LogModule module = LogModule.NM_ApiLaunchLog, }) async { if (isNeedUploadLogs(module)) { await uploadLogs(logs); } } // 判断是否需要上传日志 bool isNeedUploadLogs(LogModule module) { final launch = IXSP.getLaunch(); if (launch == null) { return false; } if (launch.appConfig?.disabledLogModules?.contains(module.name) ?? false) { return false; } return true; } /// 获取订阅周期类型文本 /// subscribeType: 1Day 2Week 3Month 4Year String _getSubscribeTypeText() { final user = IXSP.getUser(); final planInfo = user?.planInfo; // 仅当 isSubscribe=true 时有效 if (planInfo?.isSubscribe != true) { return planInfo?.subTitle ?? ''; } switch (planInfo?.subscribeType) { case 1: return 'Day'; case 2: return 'Week'; case 3: return 'Month'; case 4: return 'Year'; default: return ''; } } /// 获取过期时间文本 String _getExpireTimeText() { final user = IXSP.getUser(); final expireTime = user?.expireTime; if (expireTime == null || expireTime == 0) { return ''; } // 时间戳转日期(秒级时间戳) final date = DateTime.fromMillisecondsSinceEpoch(expireTime * 1000); final formatted = "${date.year.toString().padLeft(4, '0')}-" "${date.month.toString().padLeft(2, '0')}-" "${date.day.toString().padLeft(2, '0')}"; return formatted; } /// 获取有效期显示文本 String _getValidTermText() { final subscribeType = _getSubscribeTypeText(); final expireTime = _getExpireTimeText(); if (subscribeType.isNotEmpty && expireTime.isNotEmpty) { return '$subscribeType / $expireTime'; } else if (expireTime.isNotEmpty) { return expireTime; } return ''; } /// 启动剩余时间倒计时 /// [seconds] 剩余时间(秒),例如:3600 表示 1 小时 void startRemainTimeCountdown(int seconds) { // 取消之前的定时器 _remainTimeTimer?.cancel(); // 设置初始剩余时间 _remainTimeSeconds = seconds; // 立即更新格式化文案 _updateRemainTimeFormatted(); // 启动每秒倒计时 _remainTimeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_remainTimeSeconds > 0) { _remainTimeSeconds--; // 只有当格式化文案变化时才更新 UI _updateRemainTimeFormatted(); } else { // 倒计时结束 timer.cancel(); _remainTimeTimer = null; _onRemainTimeExpired(); } }); } /// 更新格式化后的剩余时间(只有文案变化时才触发 UI 更新) void _updateRemainTimeFormatted() { final newFormatted = _formatRemainTime(_remainTimeSeconds); if (_remainTimeFormatted.value != newFormatted) { _remainTimeFormatted.value = newFormatted; } // 更新是否显示倒计时的状态 final vipRemainNoticeSeconds = (IXSP.getAppConfig()?.vipRemainNotice ?? 600) * 60; final newShouldShow = _remainTimeSeconds > 0 && _remainTimeSeconds < vipRemainNoticeSeconds; if (_shouldShowCountdown.value != newShouldShow) { _shouldShowCountdown.value = newShouldShow; } } /// 停止剩余时间倒计时 void stopRemainTimeCountdown() { _remainTimeTimer?.cancel(); _remainTimeTimer = null; } /// 更新剩余时间(从服务器获取新的时间后调用) void updateRemainTime(int seconds) { stopRemainTimeCountdown(); if (seconds > 0) { startRemainTimeCountdown(seconds); } else { _remainTimeSeconds = 0; _updateRemainTimeFormatted(); } } /// 剩余时间到期处理 void _onRemainTimeExpired() { log(TAG, 'VIP剩余时间已到期'); // 可以在这里添加到期后的处理逻辑,例如: // - 显示续费提示 // - 断开VPN连接 // - 刷新用户信息 } /// 格式化剩余时间显示 String _formatRemainTime(int totalSeconds) { if (totalSeconds <= 0) { return ''; } final days = totalSeconds ~/ 86400; final hours = (totalSeconds % 86400) ~/ 3600; final minutes = (totalSeconds % 3600) ~/ 60; final seconds = totalSeconds % 60; // 大于1天 if (days > 1) { return '$days days'; } // 等于1天 if (days == 1) { return '1 day'; } // 大于1小时 if (hours >= 1) { return '$hours h'; } // 小于1小时,显示 mm:ss return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; } }