2026-05-27 16:05:21 +08:00
|
|
|
|
import 'package:dio/dio.dart';
|
2026-09-01 08:59:20 +08:00
|
|
|
|
import 'package:flutter/foundation.dart';
|
2026-05-27 16:05:21 +08:00
|
|
|
|
import 'package:flutter_patcher/flutter_patcher.dart';
|
|
|
|
|
|
import 'package:logger/logger.dart';
|
|
|
|
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
|
|
|
|
|
|
/// 应用版本信息模型
|
|
|
|
|
|
class AppVersionInfo {
|
|
|
|
|
|
final String version;
|
|
|
|
|
|
final int versionCode;
|
2026-05-29 14:13:34 +08:00
|
|
|
|
final String updateType;
|
2026-05-27 16:05:21 +08:00
|
|
|
|
final bool forceUpdate;
|
|
|
|
|
|
final String updateDesc;
|
|
|
|
|
|
final String? patchUrl;
|
|
|
|
|
|
final String? patchMd5;
|
|
|
|
|
|
final String? apkUrl;
|
|
|
|
|
|
final String? apkMd5;
|
|
|
|
|
|
|
|
|
|
|
|
AppVersionInfo({
|
|
|
|
|
|
required this.version,
|
|
|
|
|
|
required this.versionCode,
|
|
|
|
|
|
required this.updateType,
|
|
|
|
|
|
required this.forceUpdate,
|
|
|
|
|
|
required this.updateDesc,
|
|
|
|
|
|
this.patchUrl,
|
|
|
|
|
|
this.patchMd5,
|
|
|
|
|
|
this.apkUrl,
|
|
|
|
|
|
this.apkMd5,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
factory AppVersionInfo.fromJson(Map<String, dynamic> json) {
|
|
|
|
|
|
final patchData = json['patch'] as Map<String, dynamic>?;
|
|
|
|
|
|
final fullApkData = json['fullApk'] as Map<String, dynamic>?;
|
|
|
|
|
|
|
|
|
|
|
|
return AppVersionInfo(
|
|
|
|
|
|
version: json['version'] ?? '',
|
2026-05-29 14:13:34 +08:00
|
|
|
|
versionCode: json['versionCode'] ?? 0,
|
2026-05-27 16:05:21 +08:00
|
|
|
|
updateType: json['updateType'] ?? 'full',
|
|
|
|
|
|
forceUpdate: json['forceUpdate'] ?? false,
|
|
|
|
|
|
updateDesc: json['updateDesc'] ?? '',
|
|
|
|
|
|
patchUrl: patchData?['patchUrl'] as String?,
|
|
|
|
|
|
patchMd5: patchData?['md5'] as String?,
|
|
|
|
|
|
apkUrl: fullApkData?['apkUrl'] as String?,
|
|
|
|
|
|
apkMd5: fullApkData?['md5'] as String?,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 版本检查服务
|
|
|
|
|
|
class VersionCheckService {
|
|
|
|
|
|
static final VersionCheckService _instance = VersionCheckService._internal();
|
|
|
|
|
|
factory VersionCheckService() => _instance;
|
|
|
|
|
|
VersionCheckService._internal();
|
|
|
|
|
|
|
|
|
|
|
|
final Logger _logger = Logger();
|
|
|
|
|
|
final Dio _dio = Dio(BaseOptions(
|
|
|
|
|
|
connectTimeout: const Duration(seconds: 10),
|
|
|
|
|
|
receiveTimeout: const Duration(seconds: 10),
|
2026-09-01 08:59:20 +08:00
|
|
|
|
validateStatus: (status) => true,
|
2026-05-27 16:05:21 +08:00
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
static const String apiUrl = 'http://8.159.134.0:8012/api/update/check';
|
|
|
|
|
|
static const String _patchVersionFileName = '.patch_version';
|
|
|
|
|
|
|
|
|
|
|
|
/// 检查版本更新
|
|
|
|
|
|
Future<AppVersionInfo?> checkUpdate({
|
|
|
|
|
|
required String currentVersion,
|
|
|
|
|
|
required int currentVersionCode,
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
try {
|
2026-09-01 08:59:20 +08:00
|
|
|
|
debugPrint('');
|
|
|
|
|
|
debugPrint('========================================');
|
|
|
|
|
|
debugPrint(' UPDATE CHECK START');
|
|
|
|
|
|
debugPrint('========================================');
|
|
|
|
|
|
debugPrint('[1] APK version: $currentVersion ($currentVersionCode)');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
|
2026-05-29 14:13:34 +08:00
|
|
|
|
// 从文件读取已应用的补丁版本
|
2026-05-27 16:05:21 +08:00
|
|
|
|
final patchInfo = await _loadPatchVersionInfo();
|
|
|
|
|
|
final appliedPatchVersion = patchInfo['version'] as String?;
|
|
|
|
|
|
final appliedPatchVersionCode = patchInfo['versionCode'] as int?;
|
2026-05-29 14:13:34 +08:00
|
|
|
|
|
2026-09-01 08:59:20 +08:00
|
|
|
|
debugPrint('[2] Patch file version: $appliedPatchVersion ($appliedPatchVersionCode)');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
|
2026-09-01 08:59:20 +08:00
|
|
|
|
// 始终用 APK 基础版本请求服务端,让服务端返回最新版本
|
|
|
|
|
|
// 客户端自己判断本地是否已经是最新
|
|
|
|
|
|
final requestVersion = currentVersion;
|
|
|
|
|
|
final requestVersionCode = currentVersionCode;
|
2026-05-27 16:05:21 +08:00
|
|
|
|
|
2026-09-01 08:59:20 +08:00
|
|
|
|
debugPrint('[3] Request to server: version=$requestVersion, versionCode=$requestVersionCode');
|
|
|
|
|
|
debugPrint('[4] URL: $apiUrl?version=$requestVersion&versionCode=$requestVersionCode');
|
2026-06-16 17:34:00 +08:00
|
|
|
|
|
2026-05-27 16:05:21 +08:00
|
|
|
|
final response = await _dio.get(
|
|
|
|
|
|
apiUrl,
|
|
|
|
|
|
queryParameters: {
|
|
|
|
|
|
'version': requestVersion,
|
|
|
|
|
|
'versionCode': requestVersionCode,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-09-01 08:59:20 +08:00
|
|
|
|
debugPrint('[5] HTTP status: ${response.statusCode}');
|
|
|
|
|
|
debugPrint('[6] Response body: ${response.data}');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
|
2026-09-01 08:59:20 +08:00
|
|
|
|
final respCode = response.data['code'];
|
|
|
|
|
|
final data = response.data['data'];
|
|
|
|
|
|
final message = response.data['message'] ?? '';
|
2026-05-27 16:05:21 +08:00
|
|
|
|
|
2026-09-01 08:59:20 +08:00
|
|
|
|
debugPrint('[7] respCode=$respCode, data=$data, message=$message');
|
|
|
|
|
|
|
|
|
|
|
|
if (respCode != 200 || data == null) {
|
|
|
|
|
|
debugPrint('[RESULT] No update - server returned code=$respCode, data=${data == null ? "NULL" : "NOT_NULL"}, message=$message');
|
|
|
|
|
|
debugPrint('========================================');
|
|
|
|
|
|
debugPrint('');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
debugPrint('[7.1] data type: ${data.runtimeType}');
|
|
|
|
|
|
debugPrint('[7.2] data keys: ${data is Map ? data.keys.toList() : "NOT A MAP"}');
|
|
|
|
|
|
debugPrint('[7.3] hasUpdate = ${data['hasUpdate']} (type: ${data['hasUpdate']?.runtimeType})');
|
|
|
|
|
|
debugPrint('[7.4] version = ${data['version']}');
|
|
|
|
|
|
debugPrint('[7.5] versionCode = ${data['versionCode']}');
|
|
|
|
|
|
debugPrint('[7.6] updateType = ${data['updateType']}');
|
|
|
|
|
|
debugPrint('[7.7] patch = ${data['patch']}');
|
|
|
|
|
|
debugPrint('[7.8] fullApk = ${data['fullApk']}');
|
|
|
|
|
|
|
|
|
|
|
|
if (data['hasUpdate'] == false) {
|
|
|
|
|
|
debugPrint('[RESULT] No update - hasUpdate=false');
|
|
|
|
|
|
debugPrint('========================================');
|
|
|
|
|
|
debugPrint('');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
final versionInfo = AppVersionInfo.fromJson(data);
|
|
|
|
|
|
debugPrint('[8] Server version: ${versionInfo.version} (${versionInfo.versionCode})');
|
|
|
|
|
|
debugPrint('[9] Update type: ${versionInfo.updateType}, force: ${versionInfo.forceUpdate}');
|
|
|
|
|
|
debugPrint('[10] patchUrl: ${versionInfo.patchUrl}');
|
|
|
|
|
|
debugPrint('[11] patchMd5: ${versionInfo.patchMd5}');
|
|
|
|
|
|
debugPrint('[12] apkUrl: ${versionInfo.apkUrl}');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
|
2026-09-01 08:59:20 +08:00
|
|
|
|
// 只用版本字符串判断是否已更新:服务端 hasUpdate=true 说了算
|
|
|
|
|
|
if (appliedPatchVersion != null && appliedPatchVersion == versionInfo.version) {
|
|
|
|
|
|
debugPrint('[RESULT] No update - already at version ${versionInfo.version}');
|
|
|
|
|
|
debugPrint('========================================');
|
|
|
|
|
|
debugPrint('');
|
|
|
|
|
|
return null;
|
2026-05-27 16:05:21 +08:00
|
|
|
|
}
|
2026-09-01 08:59:20 +08:00
|
|
|
|
|
|
|
|
|
|
debugPrint('[8.1] Local patch version: ${appliedPatchVersion ?? "none"}');
|
|
|
|
|
|
debugPrint('[8.2] Server version: ${versionInfo.version}');
|
|
|
|
|
|
debugPrint('[8.3] Version different: ${appliedPatchVersion != versionInfo.version}');
|
|
|
|
|
|
|
|
|
|
|
|
debugPrint('[RESULT] NEW VERSION FOUND: ${versionInfo.version} (${versionInfo.versionCode})');
|
|
|
|
|
|
debugPrint('========================================');
|
|
|
|
|
|
debugPrint('');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
|
2026-09-01 08:59:20 +08:00
|
|
|
|
return versionInfo;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
debugPrint('[RESULT] ERROR: $e');
|
|
|
|
|
|
debugPrint('========================================');
|
|
|
|
|
|
debugPrint('');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 下载并应用差量补丁
|
|
|
|
|
|
Future<bool> applyPatch({
|
|
|
|
|
|
required String patchUrl,
|
|
|
|
|
|
required String version,
|
|
|
|
|
|
required int targetVersionCode,
|
|
|
|
|
|
String? md5,
|
|
|
|
|
|
Function(double)? onProgress,
|
|
|
|
|
|
}) async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
_logger.i('⬇️ 开始下载补丁: $patchUrl');
|
2026-05-29 14:13:34 +08:00
|
|
|
|
_logger.i('🎯 目标版本: $version ($targetVersionCode)');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
|
|
|
|
|
|
final tempDir = await getTemporaryDirectory();
|
|
|
|
|
|
final patchPath = '${tempDir.path}/update_patch.so';
|
|
|
|
|
|
|
|
|
|
|
|
await _dio.download(
|
|
|
|
|
|
patchUrl,
|
|
|
|
|
|
patchPath,
|
|
|
|
|
|
onReceiveProgress: (received, total) {
|
|
|
|
|
|
if (total != -1 && onProgress != null) {
|
|
|
|
|
|
onProgress(received / total);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
_logger.i('✅ 补丁下载完成,开始应用...');
|
|
|
|
|
|
|
|
|
|
|
|
final result = await FlutterPatcher.applyPatch(
|
|
|
|
|
|
PatchInfo(
|
|
|
|
|
|
version: version,
|
|
|
|
|
|
patchUrl: 'file://$patchPath',
|
|
|
|
|
|
targetVersionCode: targetVersionCode,
|
|
|
|
|
|
md5: md5 ?? '',
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (result.ok) {
|
2026-05-29 14:13:34 +08:00
|
|
|
|
_logger.i('✅ 补丁应用成功');
|
|
|
|
|
|
// 保存补丁版本号到文件
|
2026-05-27 16:05:21 +08:00
|
|
|
|
await _savePatchVersionInfo(version, targetVersionCode);
|
2026-05-29 14:13:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 验证保存
|
|
|
|
|
|
final verifyInfo = await _loadPatchVersionInfo();
|
|
|
|
|
|
_logger.i('💾 已保存并验证: ${verifyInfo['version']} (${verifyInfo['versionCode']})');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
_logger.e('❌ 补丁应用失败: ${result.error}');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result.ok;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
_logger.e('❌ 应用补丁异常: $e');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 下载完整 APK
|
|
|
|
|
|
Future<String?> downloadApk(String apkUrl, Function(double) onProgress) async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
_logger.i('⬇️ 开始下载 APK: $apkUrl');
|
|
|
|
|
|
|
2026-05-29 14:13:34 +08:00
|
|
|
|
final cacheDir = await getApplicationCacheDirectory();
|
|
|
|
|
|
final apkPath = '${cacheDir.path}/app-update.apk';
|
|
|
|
|
|
|
|
|
|
|
|
_logger.i('📁 APK 下载路径: $apkPath');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
|
|
|
|
|
|
await _dio.download(
|
|
|
|
|
|
apkUrl,
|
|
|
|
|
|
apkPath,
|
|
|
|
|
|
onReceiveProgress: (received, total) {
|
|
|
|
|
|
if (total != -1) {
|
|
|
|
|
|
onProgress(received / total);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
_logger.i('✅ APK 下载完成: $apkPath');
|
|
|
|
|
|
return apkPath;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
_logger.e('❌ 下载 APK 失败: $e');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 回滚到内置版本
|
|
|
|
|
|
Future<void> rollback() async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
_logger.i('🔄 执行回滚操作');
|
|
|
|
|
|
await FlutterPatcher.rollback();
|
2026-05-29 14:13:34 +08:00
|
|
|
|
_logger.i('✅ 回滚成功');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
_logger.e('❌ 回滚失败: $e');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 14:13:34 +08:00
|
|
|
|
/// 清除补丁版本信息(整包更新后调用)
|
2026-05-27 16:05:21 +08:00
|
|
|
|
Future<void> clearPatchVersionInfo() async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final directory = await getApplicationSupportDirectory();
|
|
|
|
|
|
final file = File('${directory.path}/$_patchVersionFileName');
|
|
|
|
|
|
if (await file.exists()) {
|
|
|
|
|
|
await file.delete();
|
|
|
|
|
|
_logger.i('🗑️ 已清除补丁版本记录');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
_logger.e('❌ 清除补丁版本信息失败: $e');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 14:13:34 +08:00
|
|
|
|
/// 保存补丁版本信息到文件
|
2026-05-27 16:05:21 +08:00
|
|
|
|
Future<void> _savePatchVersionInfo(String version, int versionCode) async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final directory = await getApplicationSupportDirectory();
|
|
|
|
|
|
final file = File('${directory.path}/$_patchVersionFileName');
|
2026-05-29 14:13:34 +08:00
|
|
|
|
final content = '$version|$versionCode';
|
|
|
|
|
|
await file.writeAsString(content);
|
|
|
|
|
|
|
|
|
|
|
|
// 🔥 立即验证写入
|
|
|
|
|
|
final verifyContent = await file.readAsString();
|
2026-05-27 16:05:21 +08:00
|
|
|
|
_logger.i('💾 补丁版本已保存到: ${file.path}');
|
2026-05-29 14:13:34 +08:00
|
|
|
|
_logger.i('💾 写入内容: $content');
|
|
|
|
|
|
_logger.i('💾 验证读取: $verifyContent');
|
|
|
|
|
|
_logger.i('💾 文件存在: ${await file.exists()}');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
_logger.e('❌ 保存补丁版本信息失败: $e');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 14:13:34 +08:00
|
|
|
|
/// 从文件加载补丁版本信息
|
2026-05-27 16:05:21 +08:00
|
|
|
|
Future<Map<String, dynamic>> _loadPatchVersionInfo() async {
|
|
|
|
|
|
try {
|
|
|
|
|
|
final directory = await getApplicationSupportDirectory();
|
|
|
|
|
|
final file = File('${directory.path}/$_patchVersionFileName');
|
2026-05-29 14:13:34 +08:00
|
|
|
|
|
|
|
|
|
|
_logger.i('📂 尝试加载补丁版本文件: ${file.path}');
|
|
|
|
|
|
_logger.i('📂 文件存在: ${await file.exists()}');
|
|
|
|
|
|
|
2026-05-27 16:05:21 +08:00
|
|
|
|
if (await file.exists()) {
|
|
|
|
|
|
final content = await file.readAsString();
|
2026-05-29 14:13:34 +08:00
|
|
|
|
_logger.i('📂 文件内容: $content');
|
|
|
|
|
|
|
2026-05-27 16:05:21 +08:00
|
|
|
|
final parts = content.split('|');
|
|
|
|
|
|
if (parts.length == 2) {
|
|
|
|
|
|
final version = parts[0];
|
|
|
|
|
|
final versionCode = int.tryParse(parts[1]);
|
|
|
|
|
|
if (versionCode != null) {
|
2026-05-29 14:13:34 +08:00
|
|
|
|
_logger.i('📂 解析成功: version=$version, versionCode=$versionCode');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
return {'version': version, 'versionCode': versionCode};
|
2026-05-29 14:13:34 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
_logger.e('📂 versionCode 解析失败: ${parts[1]}');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
}
|
2026-05-29 14:13:34 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
_logger.e('📂 文件格式错误,parts.length=${parts.length}');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 14:13:34 +08:00
|
|
|
|
_logger.i('📂 未找到有效的补丁版本信息');
|
2026-05-27 16:05:21 +08:00
|
|
|
|
return {'version': null, 'versionCode': null};
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
_logger.e('❌ 加载补丁版本信息失败: $e');
|
|
|
|
|
|
return {'version': null, 'versionCode': null};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|