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 'package:system_clock/system_clock.dart'; import 'package:uuid/uuid.dart'; import '../../config/translations/localization_service.dart'; import '../../config/translations/strings_enum.dart'; import '../../utils/boost_report_manager.dart'; import '../../utils/gzip_manager.dart'; import '../api/file/api_file.dart'; import '../data/models/banner/banner_list.dart'; import '../data/models/launch/upgrade.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; // 上次调用 uploadBoostLog 的时间(用于节流) DateTime? _lastUploadBoostLogTime; //是否是游客 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; // 切换到后台时的系统时钟时间戳(毫秒,不受系统时间修改影响) int _backgroundElapsedRealtime = 0; @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; // 记录切换到后台的系统时钟时间戳(不受系统时间修改影响) _backgroundElapsedRealtime = SystemClock.elapsedRealtime().inMilliseconds; ApiStatistics.instance.onAppPaused(); stopRemainTimeCountdown(); } else if (state == AppLifecycleState.resumed) { if (isBackground) { isBackground = false; asyncHandleLaunch(isRefreshLaunch: true); ApiStatistics.instance.onAppResumed(); // 计算后台经过的时间,继续倒计时 _resumeRemainTimeCountdown(); } } } /// 恢复倒计时(从后台切换到前台时调用) void _resumeRemainTimeCountdown() { if (_backgroundElapsedRealtime > 0 && _remainTimeSeconds > 0) { // 计算后台经过的秒数(使用系统时钟,不受系统时间修改影响) final currentElapsedRealtime = SystemClock.elapsedRealtime().inMilliseconds; final elapsedSeconds = (currentElapsedRealtime - _backgroundElapsedRealtime) ~/ 1000; // 更新剩余时间 final newRemainTime = _remainTimeSeconds - elapsedSeconds; log( TAG, 'Resume countdown: elapsed=${elapsedSeconds}s, ' 'old=$_remainTimeSeconds, new=$newRemainTime', ); // 重新启动倒计时 if (newRemainTime > 0) { startRemainTimeCountdown(newRemainTime); } else { // 时间已耗尽 _remainTimeSeconds = 0; _updateRemainTimeFormatted(); _onRemainTimeExpired(); } } _backgroundElapsedRealtime = 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); // 缓存日志上传 processCachedLogs(); // 缓存文件日志上传 processCachedFileLogs(); 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); // 缓存日志上传 processCachedLogs(); // 缓存文件日志上传 processCachedFileLogs(); 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(); // 如果当前versionCode等于升级的versionCode,则没有更新 if (upgrade?.versionCode == fp.appVersionCode) { return false; } var hasUpdate = false; var hasForceUpdate = false; if (upgrade != null) { if (upgrade.upgradeType == 1) { hasUpdate = true; } if (upgrade.forced == true) { hasForceUpdate = true; } } if (hasUpdate) { // 强制更新或用户主动点击检查时,直接显示更新弹窗 if (hasForceUpdate || isClickCheck) { _showUpgradeDialog(upgrade!, hasForceUpdate); return hasUpdate; } // 检查版本是否变化(新版本则重置时间逻辑) final currentVersion = upgrade?.versionCode?.toString() ?? ''; final lastNoticeVersion = IXSP.getString(SPKeys.lastUpgradeNoticeVersion) ?? ''; final isNewVersion = currentVersion != lastNoticeVersion; if (isNewVersion) { // 新版本,直接显示弹窗 log( TAG, 'New version detected: $currentVersion (last: $lastNoticeVersion)', ); _showUpgradeDialog(upgrade!, hasForceUpdate); return hasUpdate; } // 检查是否超过 upgradeNoticeTime 分钟 final appConfig = IXSP.getAppConfig(); final upgradeNoticeMinutes = appConfig?.upgradeNoticeTime ?? 1440; // 默认1天 final lastNoticeTimeStr = IXSP.getString(SPKeys.lastUpgradeNoticeTime) ?? '0'; final lastNoticeTime = int.tryParse(lastNoticeTimeStr) ?? 0; final now = NtpTimeService().getCurrentTimestamp(); final elapsedMinutes = (now - lastNoticeTime) / (1000 * 60); if (elapsedMinutes >= upgradeNoticeMinutes) { _showUpgradeDialog(upgrade!, hasForceUpdate); } else { log( TAG, 'Upgrade notice skipped: ${elapsedMinutes.toStringAsFixed(1)}min ' 'elapsed, need ${upgradeNoticeMinutes}min', ); } } return hasUpdate; } catch (e) { log(TAG, 'checkUpdate error: $e'); } return false; } /// 显示更新弹窗并记录时间和版本 void _showUpgradeDialog(Upgrade upgrade, bool hasForceUpdate) { AllDialog.showUpdate(upgrade, hasForceUpdate: hasForceUpdate); // 记录本次提醒时间 IXSP.setString( SPKeys.lastUpgradeNoticeTime, NtpTimeService().getCurrentTimestamp().toString(), ); // 记录本次提醒的版本号 IXSP.setString( SPKeys.lastUpgradeNoticeVersion, upgrade.versionCode?.toString() ?? '', ); } 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; } } /// 上传 Boost 日志(读取 app.json 和 core.json 并组合上传) /// 1秒内只允许调用一次 Future uploadBoostLog() async { // 节流检查:1秒内只允许调用一次 final now = DateTime.now(); if (_lastUploadBoostLogTime != null && now.difference(_lastUploadBoostLogTime!).inMilliseconds < 1000) { log(TAG, 'uploadBoostLog throttled, skip'); return; } _lastUploadBoostLogTime = now; try { // 读取 app.json 内容 Map appLog = {}; final appLogPath = await BoostReportManager().getAppLogFilePath(); if (appLogPath != null) { final appFile = File(appLogPath); if (await appFile.exists()) { final content = await appFile.readAsString(); if (content.isNotEmpty) { appLog = jsonDecode(content) as Map; } } } // 读取 core.json 内容 Map coreLog = {}; final coreLogPath = await BoostReportManager().getCoreLogFilePath(); if (coreLogPath != null) { final coreFile = File(coreLogPath); if (await coreFile.exists()) { final content = await coreFile.readAsString(); if (content.isNotEmpty) { coreLog = jsonDecode(content) as Map; } } } // 如果两个日志都为空,则不上传 if (appLog.isEmpty && coreLog.isEmpty) { log(TAG, 'Both app log and core log are empty, skip upload'); return; } // 组装日志数据 final logItem = { 'id': const Uuid().v4(), 'time': DateTime.now().millisecondsSinceEpoch, 'level': LogLevel.info.name, 'module': LogModule.NM_Metrics.name, 'category': Configs.productCode, 'fields': {'appLog': appLog, 'coreLog': coreLog}, }; // 上传日志 await uploadMetricsLog([logItem]); await uploadLocalLog(appLog['sessionInfo']?['boostSessionId'] ?? ''); log(TAG, 'Boost log uploaded successfully'); } catch (e) { log(TAG, 'uploadBoostLog error: $e'); } } Future uploadMetricsLog(List> logs) async { try { await uploadLogs(logs, isCache: true); } catch (e) { log(TAG, 'uploadMetricsLog error: $e'); } } Future uploadLocalLog(String boostSessionId) async { try { final appDir = await getApplicationSupportDirectory(); final logDir = Directory('${appDir.path}/logs'); if (await logDir.exists()) { final filePaths = []; // 遍历 logDir 目录,查找 client_ 和 service_ 开头的文件 await for (final entity in logDir.list()) { if (entity is File) { final fileName = entity.path.split('/').last; if (fileName.startsWith('client_') || fileName.startsWith('service_')) { filePaths.add(entity.path); } } } if (filePaths.isEmpty) { log(TAG, 'No client_ or service_ log files found'); return; } final gzipFilePath = await GzipManager().generateAndShareGzip( filePaths: filePaths, gzipFileName: '$boostSessionId.tar.gz', ); if (gzipFilePath == null) { return; } await uploadLogFile( gzipFilePath, boostSessionId, LogModule.NM_Log.name, ); } } 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 uploadLogFile( String filePath, String fileName, String logType, { bool isCache = false, }) async { try { Map request = fp.toJson(); final data = jsonEncode(request); while (true) { try { ApiFile().setbaseUrl(ApiDomains.instance.getFileUrl()); final result = await ApiFile().uploadLogFile( filePath, fileName, logType, data, ); 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 _cacheFileLogRequest(filePath, fileName); } rethrow; } else { if (await NetworkHelper.instance.isNetworkAvailable()) { final url = await ApiDomains.instance.getNextFileUrl(); if (url.isEmpty) { if (isCache) { await _cacheFileLogRequest(filePath, fileName); } rethrow; } log('uploadLogs request failed for URL ${ApiLog().baseUrl}: $e'); ApiLog().setbaseUrl(url); } else { if (isCache) { await _cacheFileLogRequest(filePath, fileName); } rethrow; } } } catch (e) { final url = await ApiDomains.instance.getNextFileUrl(); if (url.isEmpty) { if (isCache) { await _cacheFileLogRequest(filePath, fileName); } rethrow; } log('uploadLogs request failed for URL ${ApiLog().baseUrl}: $e'); ApiLog().setbaseUrl(url); } } } catch (e) { rethrow; } } // 缓存文件日志请求数据 Future _cacheFileLogRequest(String filePath, String fileName) async { try { final dir = await getApplicationSupportDirectory(); final cacheDir = Directory('${dir.path}/file_log_cache'); if (!await cacheDir.exists()) { await cacheDir.create(recursive: true); } // 复制文件到缓存目录 final file = File(filePath); await file.copy('${cacheDir.path}/${file.path.split('/').last}'); // 清理缓存目录 await _cleanupFileCacheDirectory(cacheDir); } catch (e) { log('Failed to cache file log request: $e'); } } // 清理文件缓存目录 Future _cleanupFileCacheDirectory(Directory cacheDir) async { try { const maxFiles = 10; // 最多保留10个文件 final files = await cacheDir.list().toList(); // 按修改时间排序 files.sort( (a, b) => a.statSync().modified.compareTo(b.statSync().modified), ); // 如果超过文件数量或大小限制,删除最旧的文件 while (files.length > maxFiles && files.isNotEmpty) { final oldestFile = files.removeAt(0); await oldestFile.delete(); } } catch (e) { log('Failed to cleanup cache directory: $e'); } } // 处理缓存的文件日志数据 Future processCachedFileLogs() async { try { final dir = await getApplicationSupportDirectory(); final cacheDir = Directory('${dir.path}/file_log_cache'); if (!await cacheDir.exists()) { return; } final files = await cacheDir.list().toList(); if (files.isEmpty) { return; } // 按修改时间排序,先处理最旧的文件 files.sort( (a, b) => a.statSync().modified.compareTo(b.statSync().modified), ); for (var fileEntity in files) { try { if (fileEntity is! File) continue; final filePath = fileEntity.path; final fileName = fileEntity.path.split('/').last.split('.').first; // 尝试上传缓存的日志 await uploadLogFile(filePath, fileName, LogModule.NM_Log.name); // 上传成功后删除缓存文件 await fileEntity.delete(); } catch (e) { log('Failed to process cached log file ${fileEntity.path}: $e'); // 如果上传失败,保留文件等待下次尝试 continue; } } } catch (e) { log('Failed to process cached file logs: $e'); } } // 缓存日志请求数据 Future _cacheLogRequest(dynamic items) async { try { final dir = await getApplicationSupportDirectory(); 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 processCachedLogs() async { try { final dir = await getApplicationSupportDirectory(); final cacheDir = Directory('${dir.path}/log_cache'); if (!await cacheDir.exists()) { return; } final files = await cacheDir.list().toList(); if (files.isEmpty) { return; } // 按修改时间排序,先处理最旧的文件 files.sort( (a, b) => a.statSync().modified.compareTo(b.statSync().modified), ); for (var fileEntity in files) { try { if (fileEntity is! File) continue; final content = await fileEntity.readAsString(); final items = jsonDecode(content); // 尝试上传缓存的日志 await uploadLogs(items, isCache: false); // 上传成功后删除缓存文件 await fileEntity.delete(); } catch (e) { log('Failed to process cached log file ${fileEntity.path}: $e'); // 如果上传失败,保留文件等待下次尝试 continue; } } } catch (e) { log('Failed to process cached logs: $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连接 // - 刷新用户信息 refreshLaunch(); } /// 格式化剩余时间显示 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')}'; } }