You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
499 lines
17 KiB
499 lines
17 KiB
import 'dart:io';
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:get/get.dart';
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:logger/logger.dart';
|
|
import 'package:device_info_plus/device_info_plus.dart';
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
import '../models/channel_app_model.dart';
|
|
import 'network/api.dart';
|
|
|
|
/// 版本更新服务类
|
|
class VersionUpdateService extends GetxService {
|
|
static VersionUpdateService get to => Get.find();
|
|
|
|
// 静态应用信息(内存缓存)
|
|
static String? _appVersion = ""; //应用版本号
|
|
static String? _buildNumber = ""; //构建号
|
|
static String? _platform = ""; //平台类型
|
|
static String? _deviceBrand = ""; //设备品牌
|
|
static UserChannel? _channel; //渠道
|
|
static String? _latestVersion = ""; //最新版本号+构建号
|
|
static bool? _hasUpdate = false; //是否有更新
|
|
static bool? _isForceUpdate = false; //是否强制更新
|
|
static String? _updateUrl = ""; //更新链接
|
|
static AppInfo? _appInfo; //应用信息
|
|
static bool? _isShowTourists = false; //是否显示游客登录
|
|
static bool? _allowSkipDeviceBinding = false; //是否允许跳过设备绑定
|
|
static DateTime? _lastCheckTime; //上次检查时间
|
|
static const int _cacheIntervalMinutes = 10; // 可配置化
|
|
static final String? _countryCode = Get.deviceLocale?.countryCode; //国家地区编码
|
|
static final String? _languageCode = Get.deviceLocale?.languageCode; //语言编码
|
|
// ignore: non_constant_identifier_names
|
|
// static final String? _SERVER_URL = (_countryCode == 'CN')
|
|
// ? dotenv.env['SERVER_URL']
|
|
// : dotenv.env['HW_SERVER_URL']; //服务器地址
|
|
|
|
// 添加 getter 方法来访问缓存的静态数据
|
|
/// 获取缓存的应用版本
|
|
String get cachedAppVersion => _appVersion ?? '';
|
|
|
|
/// 获取缓存的构建号
|
|
String get cachedBuildNumber => _buildNumber ?? '';
|
|
|
|
/// 获取缓存的平台信息
|
|
String get cachedPlatform => _platform ?? '';
|
|
|
|
/// 获取缓存的设备品牌
|
|
String get cachedDeviceBrand => _deviceBrand ?? '';
|
|
|
|
/// 获取缓存的渠道信息
|
|
UserChannel get cachedChannel =>
|
|
_channel ??
|
|
(_platform?.toLowerCase() == 'ios'
|
|
? UserChannel.GLOBAL_APPLE
|
|
: UserChannel.GLOBAL_GOOGLE);
|
|
|
|
/// 获取缓存的最新版本
|
|
String get cachedLatestVersion => _latestVersion ?? '';
|
|
|
|
/// 获取缓存的是否有更新
|
|
bool get cachedHasUpdate => _hasUpdate ?? false;
|
|
|
|
/// 获取缓存的是否强制更新
|
|
bool get cachedIsForceUpdate => _isForceUpdate ?? false;
|
|
|
|
/// 获取缓存的更新地址
|
|
String get cachedUpdateUrl => _updateUrl ?? '';
|
|
|
|
/// 获取缓存的应用信息
|
|
AppInfo? get cachedAppInfo => _appInfo;
|
|
|
|
/// 获取缓存的游客登录状态
|
|
bool get cachedShowTourists => _isShowTourists ?? false;
|
|
|
|
/// 获取缓存的是否允许跳过设备绑定
|
|
bool get cachedAllowSkipDeviceBinding => _allowSkipDeviceBinding ?? false;
|
|
|
|
/// 获取上次检查时间
|
|
DateTime? get lastCheckTime => _lastCheckTime;
|
|
|
|
/// 获取缓存的国家地区编码
|
|
String get cachedCountryCode => _countryCode ?? '';
|
|
|
|
/// 获取缓存的语言编码
|
|
String get cachedLanguageCode => _languageCode ?? '';
|
|
|
|
/// 获取缓存的服务器地址
|
|
// String get cachedServerUrl => _SERVER_URL ?? '';
|
|
|
|
final Logger _logger = Logger();
|
|
|
|
// @override
|
|
// void onInit() {
|
|
// getCurrentVersion();
|
|
// getCurrentPlatformInfo();
|
|
// super.onInit();
|
|
// }
|
|
|
|
/// 检查版本更新
|
|
Future<VersionUpdateInfo> checkForUpdate() async {
|
|
if (_lastCheckTime != null &&
|
|
DateTime.now().difference(_lastCheckTime!).inMinutes <
|
|
_cacheIntervalMinutes) {
|
|
// [1]没有超过10分钟,返回缓存数据
|
|
_logger.d('没有超过10分钟,返回缓存数据');
|
|
return VersionUpdateInfo(
|
|
hasUpdate: _hasUpdate ?? false,
|
|
currentVersion: _appVersion ?? '1.0.0',
|
|
latestVersion: (_latestVersion ?? '1.0.0').split('+')[0],
|
|
appInfo: _appInfo,
|
|
updateType: _hasUpdate!
|
|
? (_isForceUpdate! ? UpdateType.force : UpdateType.optional)
|
|
: UpdateType.none,
|
|
);
|
|
}
|
|
|
|
try {
|
|
final currentVersion = await getCurrentVersion();
|
|
final platformInfo = await getCurrentPlatformInfo();
|
|
|
|
_platform = platformInfo.platform;
|
|
//_channel = platformInfo.channel;
|
|
_lastCheckTime = DateTime.now();
|
|
|
|
// 获取所有渠道包信息
|
|
final response = await Api.getChannelApps();
|
|
|
|
_logger.d('lxm--获取所有渠道包信息:$response');
|
|
|
|
final List<AppInfo> apps = _parseAppsList(response);
|
|
|
|
if (apps.isEmpty) {
|
|
_logger.w('lxm--获取所有渠道包信息失败或为空');
|
|
return VersionUpdateInfo(
|
|
hasUpdate: false,
|
|
currentVersion: currentVersion,
|
|
latestVersion: currentVersion,
|
|
updateType: UpdateType.none,
|
|
);
|
|
}
|
|
|
|
final Map<UserChannel, AppInfo> appIndex = _buildChannelIndex(apps);
|
|
|
|
// 依据设备品牌渠道用于版本更新链接
|
|
final brandChannel = platformInfo.channel;
|
|
|
|
// 环境变量渠道用于游客登录配置
|
|
final envChannelName = dotenv.env['Channel'] ?? 'OFFICIAL';
|
|
final envChannel = UserChannel.fromName(envChannelName);
|
|
final AppInfo? envAppInfo = appIndex[envChannel];
|
|
|
|
// 若不存在品牌渠道信息,降级为环境渠道;仍不存在则返回无更新
|
|
final AppInfo? chosenAppInfo =
|
|
_getChannelAppInfo(appIndex, brandChannel, envChannel);
|
|
if (chosenAppInfo == null) {
|
|
_logger.w('lxm--未找到品牌或环境渠道的应用信息,返回无更新');
|
|
return VersionUpdateInfo(
|
|
hasUpdate: false,
|
|
currentVersion: currentVersion,
|
|
latestVersion: currentVersion,
|
|
updateType: UpdateType.none,
|
|
);
|
|
}
|
|
|
|
_appInfo = chosenAppInfo;
|
|
_latestVersion = chosenAppInfo.version;
|
|
_updateUrl = chosenAppInfo.address;
|
|
|
|
// 设置游客登录显示状态(先用当前渠道结果作为回退,再用.env渠道覆盖以保持一致)
|
|
_isShowTourists = (envAppInfo ?? chosenAppInfo).tourists;
|
|
_allowSkipDeviceBinding =
|
|
(envAppInfo ?? chosenAppInfo).allowskipdevicebinding;
|
|
|
|
_logger.d('lxm--从服务器获取游客登录配置(最终): ${_isShowTourists}');
|
|
|
|
// if (channelAppResponse.code != 0 || channelAppResponse.data == null) {
|
|
// _logger.w('获取版本信息失败: ${channelAppResponse.msg}');
|
|
// return VersionUpdateInfo(
|
|
// hasUpdate: false,
|
|
// currentVersion: currentVersion,
|
|
// latestVersion: currentVersion,
|
|
// updateType: UpdateType.none,
|
|
// );
|
|
// }
|
|
|
|
// final appInfo = channelAppResponse.data!.app;
|
|
_hasUpdate = _compareVersions(currentVersion, chosenAppInfo.version);
|
|
|
|
// 判断更新类型:如果主版本号不同则强制更新
|
|
UpdateType updateType = UpdateType.none;
|
|
if (_hasUpdate!) {
|
|
_isForceUpdate =
|
|
_shouldForceUpdate(currentVersion, chosenAppInfo.version);
|
|
updateType = _isForceUpdate! ? UpdateType.force : UpdateType.optional;
|
|
}
|
|
//[3]返回版本更新信息
|
|
return VersionUpdateInfo(
|
|
hasUpdate: _hasUpdate!,
|
|
currentVersion: currentVersion.split('+')[0],
|
|
latestVersion: chosenAppInfo.version.split('+')[0],
|
|
appInfo: chosenAppInfo,
|
|
updateType: updateType,
|
|
);
|
|
} catch (e) {
|
|
_logger.e('检查版本更新失败: $e');
|
|
// 发生错误时,保持默认值 false
|
|
_isShowTourists = false;
|
|
final currentVersion = await getCurrentVersion();
|
|
return VersionUpdateInfo(
|
|
hasUpdate: false,
|
|
currentVersion: currentVersion,
|
|
latestVersion: currentVersion,
|
|
updateType: UpdateType.none,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 获取当前应用版本信息
|
|
Future<String> getCurrentVersion() async {
|
|
try {
|
|
final packageInfo = await PackageInfo.fromPlatform();
|
|
_appVersion = packageInfo.version;
|
|
_buildNumber = packageInfo.buildNumber;
|
|
return '${packageInfo.version}+${packageInfo.buildNumber}';
|
|
} catch (e) {
|
|
_logger.e('获取当前版本信息失败: $e');
|
|
return '1.0.0';
|
|
}
|
|
}
|
|
|
|
/// 获取当前平台信息
|
|
Future<PlatformInfo> getCurrentPlatformInfo() async {
|
|
if (Platform.isIOS) {
|
|
_channel = UserChannel.GLOBAL_APPLE;
|
|
await _getIosDeviceBrand();
|
|
return const PlatformInfo(
|
|
platform: 'ios',
|
|
storeType: AppStoreType.appStore,
|
|
channel: UserChannel.GLOBAL_APPLE,
|
|
);
|
|
}
|
|
|
|
if (Platform.isAndroid) {
|
|
// 根据设备品牌判断应用商店类型
|
|
return await getAndroidStoreInfoByBrand();
|
|
}
|
|
|
|
return const PlatformInfo(
|
|
platform: 'unknown',
|
|
storeType: AppStoreType.unknown,
|
|
channel: UserChannel.UNKNOWN,
|
|
);
|
|
}
|
|
|
|
/// 根据设备品牌获取应用商店信息
|
|
///
|
|
/// 该方法根据设备品牌判断应用商店类型,并返回对应的 [PlatformInfo] 对象。
|
|
/// 如果品牌不在预设列表中,将使用环境变量 [Channel] 或默认值 [UserChannel.GLOBAL_GOOGLE]。
|
|
Future<PlatformInfo> getAndroidStoreInfoByBrand() async {
|
|
try {
|
|
final deviceInfo = DeviceInfoPlugin();
|
|
final androidInfo = await deviceInfo.androidInfo;
|
|
final brand = androidInfo.brand.toLowerCase();
|
|
|
|
_deviceBrand = brand;
|
|
String channelFromEnv = dotenv.env['Channel'] ?? 'OFFICIAL';
|
|
Logger().d('channelFromEnv: $channelFromEnv');
|
|
_channel = UserChannel.fromName(channelFromEnv);
|
|
|
|
UserChannel brandChannel; //品牌渠道,只用于请求版本更新信息
|
|
|
|
switch (brand) {
|
|
case 'huawei':
|
|
brandChannel = UserChannel.CHINA_HUAWEI;
|
|
break;
|
|
case 'xiaomi':
|
|
case 'redmi':
|
|
brandChannel = UserChannel.CHINA_XIAOMI;
|
|
break;
|
|
case 'oppo':
|
|
case 'oneplus':
|
|
brandChannel = UserChannel.CHINA_OPPO;
|
|
break;
|
|
case 'vivo':
|
|
case 'iqoo':
|
|
brandChannel = UserChannel.CHINA_VIVO;
|
|
break;
|
|
case 'honor':
|
|
brandChannel = UserChannel.CHINA_HONOR;
|
|
break;
|
|
default:
|
|
brandChannel = _channel ?? UserChannel.GLOBAL_GOOGLE;
|
|
break;
|
|
}
|
|
|
|
return PlatformInfo(
|
|
platform: 'android',
|
|
storeType: AppStoreType.official,
|
|
channel: brandChannel,
|
|
);
|
|
} catch (e) {
|
|
_logger.e('获取Android设备信息失败: $e');
|
|
return const PlatformInfo(
|
|
platform: 'android',
|
|
storeType: AppStoreType.googlePlay,
|
|
channel: UserChannel.UNKNOWN,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 获取iOS设备品牌信息
|
|
Future<void> _getIosDeviceBrand() async {
|
|
try {
|
|
final deviceInfo = DeviceInfoPlugin();
|
|
final iosInfo = await deviceInfo.iosInfo;
|
|
|
|
// 获取设备型号信息
|
|
final model =
|
|
iosInfo.model.toLowerCase(); // 例如: "iPhone", "iPad", "iPod touch"
|
|
//print('lxm---model: $model');
|
|
final utsname = iosInfo.utsname; // 包含更详细的设备信息
|
|
//print('lxm---utsname: $utsname');
|
|
final machineId = utsname.machine.toLowerCase(); // 例如: "iPhone14,2"
|
|
//print('lxm---machineId: $machineId');
|
|
|
|
// 根据设备型号确定品牌
|
|
if (model.contains('iphone') || machineId.contains('iphone')) {
|
|
_deviceBrand = 'iphone';
|
|
} else if (model.contains('ipad') || machineId.contains('ipad')) {
|
|
_deviceBrand = 'ipad';
|
|
} else if (model.contains('ipod') || machineId.contains('ipod')) {
|
|
_deviceBrand = 'ipod';
|
|
} else if (model.contains('mac') || machineId.contains('mac')) {
|
|
_deviceBrand = 'mac';
|
|
} else {
|
|
_deviceBrand = 'ios'; // 默认值
|
|
}
|
|
|
|
_logger.d('iOS设备品牌: $_deviceBrand (model: $model, machine: $machineId)');
|
|
} catch (e) {
|
|
_logger.e('获取iOS设备品牌失败: $e');
|
|
_deviceBrand = 'ios'; // 默认值
|
|
}
|
|
}
|
|
|
|
/// 判断是否需要强制更新
|
|
/// 当主版本号(第一位数字)不同时返回true
|
|
bool _shouldForceUpdate(String current, String latest) {
|
|
try {
|
|
// 拆分版本号与构建号
|
|
final currentParts = current.split('+');
|
|
final latestParts = latest.split('+');
|
|
|
|
// 解析版本号 (major.minor.patch)
|
|
final currentVersions =
|
|
currentParts[0].split('.').map(int.parse).toList();
|
|
final latestVersions = latestParts[0].split('.').map(int.parse).toList();
|
|
|
|
// 比较主版本号(第一位)
|
|
if (currentVersions.isNotEmpty && latestVersions.isNotEmpty) {
|
|
if (currentVersions[0] != latestVersions[0]) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// 检查是否有两个或更多的 + 号,如果有则强制更新
|
|
if (latest.split('+').length > 2) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
} catch (e) {
|
|
_logger.e('判断强制更新失败: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 比较版本号
|
|
/// 返回true表示需要更新(远程版本更高)
|
|
bool _compareVersions(String current, String latest) {
|
|
// 拆分版本号与构建号
|
|
final currentParts = current.split('+')[0];
|
|
final latestParts = latest.split('+')[0];
|
|
|
|
// 解析版本号 (major.minor.patch)
|
|
final currentVersions = currentParts.split('.').map(int.parse).toList();
|
|
final latestVersions = latestParts.split('.').map(int.parse).toList();
|
|
|
|
// 使用较小的长度进行比较,避免索引越界
|
|
final minLength = math.min(currentVersions.length, latestVersions.length);
|
|
|
|
// 逐级比较版本号
|
|
for (var i = 0; i < minLength; i++) {
|
|
if (latestVersions[i] > currentVersions[i]) return true;
|
|
if (latestVersions[i] < currentVersions[i]) return false;
|
|
}
|
|
|
|
// 如果前面的版本号都相同,比较长度
|
|
// 更长的版本号被认为是更新的版本(如 1.0.1 > 1.0)
|
|
return latestVersions.length > currentVersions.length;
|
|
}
|
|
|
|
/// 解析响应数据为AppInfo列表
|
|
List<AppInfo> _parseAppsList(dynamic response) {
|
|
try {
|
|
final appsJson = (response is Map) ? response['apps'] : null;
|
|
if (appsJson is List) {
|
|
return appsJson
|
|
.whereType<Map<String, dynamic>>()
|
|
.map((e) => AppInfo.fromJson(e))
|
|
.toList();
|
|
}
|
|
} catch (e) {
|
|
_logger.e('解析渠道列表失败: $e');
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/// 构建渠道索引
|
|
Map<UserChannel, AppInfo> _buildChannelIndex(List<AppInfo> apps) {
|
|
final index = <UserChannel, AppInfo>{};
|
|
for (final app in apps) {
|
|
index[app.channel] = app;
|
|
}
|
|
return index;
|
|
}
|
|
|
|
/// 获取渠道信息,带备选策略
|
|
///
|
|
/// 优先返回主渠道的信息,如果主渠道不存在则返回备选渠道的信息。
|
|
/// 主要用于:
|
|
/// 1. 版本更新:优先使用设备品牌渠道,备选使用环境变量渠道
|
|
/// 2. 功能配置:优先使用环境变量渠道,备选使用版本更新渠道
|
|
AppInfo? _getChannelAppInfo(
|
|
Map<UserChannel, AppInfo> index,
|
|
UserChannel primary,
|
|
UserChannel? fallback,
|
|
) {
|
|
return index[primary] ?? (fallback != null ? index[fallback] : null);
|
|
}
|
|
|
|
/// 解析版本号字符串为数字数组
|
|
List<int> _parseVersion(String version) {
|
|
final parts = version.split('.');
|
|
final result = <int>[];
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
if (i < parts.length) {
|
|
result.add(int.tryParse(parts[i]) ?? 0);
|
|
} else {
|
|
result.add(0);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// 执行更新操作
|
|
Future<bool> performUpdate(VersionUpdateInfo updateInfo) async {
|
|
//print('lxm---updateInfo详情:');
|
|
//print('lxm---hasUpdate: ${updateInfo.hasUpdate}');
|
|
//print('lxm---currentVersion: ${updateInfo.currentVersion}');
|
|
//print('lxm---latestVersion: ${updateInfo.latestVersion}');
|
|
//print('lxm---updateType: ${updateInfo.updateType}');
|
|
//print('lxm---appInfo: ${updateInfo.appInfo?.toJson()}');
|
|
if (!updateInfo.hasUpdate || updateInfo.appInfo == null) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
_logger.d('lxm---执行更新操作,下载地址: ${updateInfo.appInfo!.address}');
|
|
return await _openDownloadUrl(updateInfo.appInfo!.address!);
|
|
} catch (e) {
|
|
_logger.e('执行更新失败: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 打开下载链接
|
|
Future<bool> _openDownloadUrl(String downloadUrl) async {
|
|
return await _launchUrl(downloadUrl);
|
|
}
|
|
|
|
/// 启动URL
|
|
Future<bool> _launchUrl(String url) async {
|
|
try {
|
|
final uri = Uri.parse(url);
|
|
if (await canLaunchUrl(uri)) {
|
|
return await launchUrl(uri, mode: LaunchMode.externalApplication);
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
_logger.e('启动URL失败: $e');
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|