Files
flutterApp/lib/core/update/version_check_service.dart

328 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
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;
final String updateType;
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'] ?? '',
versionCode: json['versionCode'] ?? 0,
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),
validateStatus: (status) => true,
));
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 {
debugPrint('');
debugPrint('========================================');
debugPrint(' UPDATE CHECK START');
debugPrint('========================================');
debugPrint('[1] APK version: $currentVersion ($currentVersionCode)');
// 从文件读取已应用的补丁版本
final patchInfo = await _loadPatchVersionInfo();
final appliedPatchVersion = patchInfo['version'] as String?;
final appliedPatchVersionCode = patchInfo['versionCode'] as int?;
debugPrint('[2] Patch file version: $appliedPatchVersion ($appliedPatchVersionCode)');
// 始终用 APK 基础版本请求服务端,让服务端返回最新版本
// 客户端自己判断本地是否已经是最新
final requestVersion = currentVersion;
final requestVersionCode = currentVersionCode;
debugPrint('[3] Request to server: version=$requestVersion, versionCode=$requestVersionCode');
debugPrint('[4] URL: $apiUrl?version=$requestVersion&versionCode=$requestVersionCode');
final response = await _dio.get(
apiUrl,
queryParameters: {
'version': requestVersion,
'versionCode': requestVersionCode,
},
);
debugPrint('[5] HTTP status: ${response.statusCode}');
debugPrint('[6] Response body: ${response.data}');
final respCode = response.data['code'];
final data = response.data['data'];
final message = response.data['message'] ?? '';
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}');
// 只用版本字符串判断是否已更新:服务端 hasUpdate=true 说了算
if (appliedPatchVersion != null && appliedPatchVersion == versionInfo.version) {
debugPrint('[RESULT] No update - already at version ${versionInfo.version}');
debugPrint('========================================');
debugPrint('');
return null;
}
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('');
return versionInfo;
} catch (e) {
debugPrint('[RESULT] ERROR: $e');
debugPrint('========================================');
debugPrint('');
return null;
}
}
/// 下载并应用差量补丁
Future<bool> applyPatch({
required String patchUrl,
required String version,
required int targetVersionCode,
String? md5,
Function(double)? onProgress,
}) async {
try {
_logger.i('⬇️ 开始下载补丁: $patchUrl');
_logger.i('🎯 目标版本: $version ($targetVersionCode)');
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) {
_logger.i('✅ 补丁应用成功');
// 保存补丁版本号到文件
await _savePatchVersionInfo(version, targetVersionCode);
// 验证保存
final verifyInfo = await _loadPatchVersionInfo();
_logger.i('💾 已保存并验证: ${verifyInfo['version']} (${verifyInfo['versionCode']})');
} 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');
final cacheDir = await getApplicationCacheDirectory();
final apkPath = '${cacheDir.path}/app-update.apk';
_logger.i('📁 APK 下载路径: $apkPath');
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();
_logger.i('✅ 回滚成功');
} catch (e) {
_logger.e('❌ 回滚失败: $e');
}
}
/// 清除补丁版本信息(整包更新后调用)
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');
}
}
/// 保存补丁版本信息到文件
Future<void> _savePatchVersionInfo(String version, int versionCode) async {
try {
final directory = await getApplicationSupportDirectory();
final file = File('${directory.path}/$_patchVersionFileName');
final content = '$version|$versionCode';
await file.writeAsString(content);
// 🔥 立即验证写入
final verifyContent = await file.readAsString();
_logger.i('💾 补丁版本已保存到: ${file.path}');
_logger.i('💾 写入内容: $content');
_logger.i('💾 验证读取: $verifyContent');
_logger.i('💾 文件存在: ${await file.exists()}');
} catch (e) {
_logger.e('❌ 保存补丁版本信息失败: $e');
}
}
/// 从文件加载补丁版本信息
Future<Map<String, dynamic>> _loadPatchVersionInfo() async {
try {
final directory = await getApplicationSupportDirectory();
final file = File('${directory.path}/$_patchVersionFileName');
_logger.i('📂 尝试加载补丁版本文件: ${file.path}');
_logger.i('📂 文件存在: ${await file.exists()}');
if (await file.exists()) {
final content = await file.readAsString();
_logger.i('📂 文件内容: $content');
final parts = content.split('|');
if (parts.length == 2) {
final version = parts[0];
final versionCode = int.tryParse(parts[1]);
if (versionCode != null) {
_logger.i('📂 解析成功: version=$version, versionCode=$versionCode');
return {'version': version, 'versionCode': versionCode};
} else {
_logger.e('📂 versionCode 解析失败: ${parts[1]}');
}
} else {
_logger.e('📂 文件格式错误,parts.length=${parts.length}');
}
}
_logger.i('📂 未找到有效的补丁版本信息');
return {'version': null, 'versionCode': null};
} catch (e) {
_logger.e('❌ 加载补丁版本信息失败: $e');
return {'version': null, 'versionCode': null};
}
}
}