添加修复大包更新安装覆盖的-待测试
添加修复了更新接口的令狐适配 添加了新的服务端地址-
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
class HttpApiConsts {
|
||||
static const String baseUrl = "http://1.95.137.212:8081";
|
||||
// static const String baseUrl = "http://8.159.134.0:8012"; // 旧地址
|
||||
static const String baseUrl = "http://1.95.137.212:59015";
|
||||
|
||||
/// 账号相关
|
||||
// 登录
|
||||
|
||||
120
lib/core/update/update_cubit.dart
Normal file
120
lib/core/update/update_cubit.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'version_check_service.dart';
|
||||
import 'update_state.dart';
|
||||
|
||||
/// 更新 Cubit
|
||||
class UpdateCubit extends Cubit<UpdateState> {
|
||||
final VersionCheckService _versionService;
|
||||
final Logger _logger;
|
||||
|
||||
UpdateCubit(this._versionService)
|
||||
: _logger = Logger(),
|
||||
super(UpdateInitial());
|
||||
|
||||
/// 检查版本更新
|
||||
Future<void> checkUpdate() async {
|
||||
emit(UpdateChecking());
|
||||
|
||||
try {
|
||||
// 从 pubspec.yaml 读取版本信息
|
||||
final String versionString = await rootBundle.loadString('pubspec.yaml');
|
||||
final versionMatch = RegExp(r'version:\s*(\d+\.\d+\.\d+)\+(\d+)').firstMatch(versionString);
|
||||
|
||||
final currentVersion = versionMatch?.group(1) ?? '1.0.0';
|
||||
final currentVersionCode = int.tryParse(versionMatch?.group(2) ?? '1') ?? 1;
|
||||
|
||||
_logger.i('📱 当前应用版本: $currentVersion ($currentVersionCode)');
|
||||
|
||||
// 检查更新
|
||||
final versionInfo = await _versionService.checkUpdate(
|
||||
currentVersion: currentVersion,
|
||||
currentVersionCode: currentVersionCode,
|
||||
);
|
||||
|
||||
if (versionInfo == null) {
|
||||
_logger.i('✅ 已是最新版本');
|
||||
emit(UpdateUpToDate());
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.i('🔄 发现新版本: ${versionInfo.version}');
|
||||
_logger.i('📦 更新类型: ${versionInfo.updateType}');
|
||||
_logger.i('⚠️ 强制更新: ${versionInfo.forceUpdate}');
|
||||
|
||||
emit(UpdateAvailable(versionInfo));
|
||||
} catch (e) {
|
||||
_logger.e('❌ 检查更新失败: $e');
|
||||
emit(UpdateFailure('检查更新失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用差量补丁
|
||||
Future<void> applyPatch(String patchUrl, String version, int targetVersionCode, {String? md5}) async {
|
||||
emit(const UpdateDownloading(0.0, isPatch: true));
|
||||
|
||||
try {
|
||||
_logger.i('⬇️ 开始下载并应用补丁');
|
||||
|
||||
final success = await _versionService.applyPatch(
|
||||
patchUrl: patchUrl,
|
||||
version: version,
|
||||
targetVersionCode: targetVersionCode,
|
||||
md5: md5,
|
||||
onProgress: (progress) {
|
||||
// 🔥 实时更新下载进度
|
||||
emit(UpdateDownloading(progress, isPatch: true));
|
||||
},
|
||||
);
|
||||
|
||||
if (success) {
|
||||
emit(const UpdateInstalling(isPatch: true));
|
||||
emit(UpdateSuccess());
|
||||
} else {
|
||||
emit(const UpdateFailure('补丁应用失败'));
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('❌ 应用补丁异常: $e');
|
||||
emit(UpdateFailure('应用补丁失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载并安装完整 APK
|
||||
Future<void> downloadAndInstallApk(String apkUrl) async {
|
||||
emit(const UpdateDownloading(0.0, isPatch: false));
|
||||
|
||||
try {
|
||||
_logger.i('⬇️ 开始下载完整 APK');
|
||||
|
||||
final apkPath = await _versionService.downloadApk(apkUrl, (progress) {
|
||||
emit(UpdateDownloading(progress, isPatch: false));
|
||||
});
|
||||
|
||||
if (apkPath != null) {
|
||||
emit(const UpdateInstalling(isPatch: false));
|
||||
|
||||
// 🔥 整包更新后,清除补丁版本记录
|
||||
await _versionService.clearPatchVersionInfo();
|
||||
|
||||
// 打开 APK 文件,触发系统安装界面
|
||||
final result = await OpenFile.open(apkPath);
|
||||
_logger.i('📦 安装结果: ${result.message}');
|
||||
|
||||
emit(UpdateSuccess());
|
||||
} else {
|
||||
emit(const UpdateFailure('APK 下载失败'));
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.e('❌ 下载安装 APK 异常: $e');
|
||||
emit(UpdateFailure('下载安装 APK 失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// 取消更新
|
||||
void cancelUpdate() {
|
||||
_logger.i('❌ 用户取消更新');
|
||||
emit(UpdateInitial());
|
||||
}
|
||||
}
|
||||
141
lib/core/update/update_dialog.dart
Normal file
141
lib/core/update/update_dialog.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'update_cubit.dart';
|
||||
import 'update_state.dart';
|
||||
import 'version_check_service.dart';
|
||||
|
||||
/// 更新对话框
|
||||
class UpdateDialog extends StatelessWidget {
|
||||
final AppVersionInfo versionInfo;
|
||||
|
||||
const UpdateDialog({super.key, required this.versionInfo});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<UpdateCubit, UpdateState>(
|
||||
listener: (context, state) {
|
||||
if (state is UpdateSuccess) {
|
||||
// 更新成功,关闭对话框
|
||||
Navigator.of(context).pop();
|
||||
|
||||
if (versionInfo.updateType == 'patch') {
|
||||
// 差量更新需要重启
|
||||
_showRestartDialog(context);
|
||||
}
|
||||
// 整包更新会自动打开安装界面,不需要额外操作
|
||||
} else if (state is UpdateFailure) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('更新失败: ${state.error}')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: AlertDialog(
|
||||
title: const Text('发现新版本'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('版本号: ${versionInfo.version}'),
|
||||
const SizedBox(height: 8),
|
||||
Text('更新类型: ${versionInfo.updateType == "patch" ? "差量更新" : "整包更新"}'),
|
||||
const SizedBox(height: 8),
|
||||
if (versionInfo.forceUpdate)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'⚠️ 强制更新',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('更新内容:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
Text(versionInfo.updateDesc.isEmpty ? '优化用户体验,修复已知问题' : versionInfo.updateDesc),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (!versionInfo.forceUpdate)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<UpdateCubit>().cancelUpdate();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('稍后'),
|
||||
),
|
||||
BlocBuilder<UpdateCubit, UpdateState>(
|
||||
builder: (context, state) {
|
||||
if (state is UpdateDownloading) {
|
||||
return SizedBox(
|
||||
width: 60,
|
||||
height: 60,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
value: state.progress,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
Text('${(state.progress * 100).toInt()}%'),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else if (state is UpdateInstalling) {
|
||||
return const Text('安装中...');
|
||||
} else {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
if (versionInfo.updateType == 'patch' && versionInfo.patchUrl != null) {
|
||||
// 差量更新
|
||||
context.read<UpdateCubit>().applyPatch(
|
||||
versionInfo.patchUrl!,
|
||||
versionInfo.version,
|
||||
versionInfo.versionCode,
|
||||
md5: versionInfo.patchMd5,
|
||||
);
|
||||
} else if (versionInfo.apkUrl != null) {
|
||||
// 整包更新
|
||||
context.read<UpdateCubit>().downloadAndInstallApk(versionInfo.apkUrl!);
|
||||
}
|
||||
},
|
||||
child: const Text('立即更新'),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showRestartDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('更新完成'),
|
||||
content: const Text('差量更新已应用,需要重启应用才能生效。\n是否立即重启?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('稍后'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
// TODO: 实现重启逻辑
|
||||
// 可以使用 restart_app 包或退出应用让用户手动打开
|
||||
},
|
||||
child: const Text('立即重启'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
55
lib/core/update/update_state.dart
Normal file
55
lib/core/update/update_state.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'version_check_service.dart';
|
||||
|
||||
/// 更新状态基类
|
||||
abstract class UpdateState {
|
||||
const UpdateState();
|
||||
}
|
||||
|
||||
/// 初始状态
|
||||
class UpdateInitial extends UpdateState {
|
||||
const UpdateInitial();
|
||||
}
|
||||
|
||||
/// 检查中
|
||||
class UpdateChecking extends UpdateState {
|
||||
const UpdateChecking();
|
||||
}
|
||||
|
||||
/// 发现新版本
|
||||
class UpdateAvailable extends UpdateState {
|
||||
final AppVersionInfo versionInfo;
|
||||
|
||||
const UpdateAvailable(this.versionInfo);
|
||||
}
|
||||
|
||||
/// 下载中
|
||||
class UpdateDownloading extends UpdateState {
|
||||
final double progress;
|
||||
final bool isPatch; // true=差量补丁, false=完整APK
|
||||
|
||||
const UpdateDownloading(this.progress, {required this.isPatch});
|
||||
}
|
||||
|
||||
/// 安装中
|
||||
class UpdateInstalling extends UpdateState {
|
||||
final bool isPatch;
|
||||
|
||||
const UpdateInstalling({required this.isPatch});
|
||||
}
|
||||
|
||||
/// 更新成功
|
||||
class UpdateSuccess extends UpdateState {
|
||||
const UpdateSuccess();
|
||||
}
|
||||
|
||||
/// 已是最新版本
|
||||
class UpdateUpToDate extends UpdateState {
|
||||
const UpdateUpToDate();
|
||||
}
|
||||
|
||||
/// 更新失败
|
||||
class UpdateFailure extends UpdateState {
|
||||
final String error;
|
||||
|
||||
const UpdateFailure(this.error);
|
||||
}
|
||||
268
lib/core/update/version_check_service.dart
Normal file
268
lib/core/update/version_check_service.dart
Normal file
@@ -0,0 +1,268 @@
|
||||
import 'package:dio/dio.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; // "patch" 或 "full"
|
||||
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) {
|
||||
// 兼容嵌套结构:如果存在 patch 对象,则从中提取 url 和 md5
|
||||
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, // 处理 null 情况
|
||||
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),
|
||||
));
|
||||
|
||||
// 后端接口地址
|
||||
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 {
|
||||
_logger.i('🔍 检查版本更新');
|
||||
_logger.i('📱 APK 自带版本: $currentVersion ($currentVersionCode)');
|
||||
|
||||
// 🔥 从文件读取已应用的补丁版本
|
||||
final patchInfo = await _loadPatchVersionInfo();
|
||||
final appliedPatchVersion = patchInfo['version'] as String?;
|
||||
final appliedPatchVersionCode = patchInfo['versionCode'] as int?;
|
||||
|
||||
// 如果打过补丁,使用补丁的版本信息;否则使用 APK 自带的版本
|
||||
final requestVersion = appliedPatchVersion ?? currentVersion;
|
||||
final requestVersionCode = appliedPatchVersionCode ?? currentVersionCode;
|
||||
|
||||
_logger.i('🚀 请求后端版本: $requestVersion ($requestVersionCode)');
|
||||
|
||||
// 调用后端接口
|
||||
final response = await _dio.get(
|
||||
apiUrl,
|
||||
queryParameters: {
|
||||
'version': requestVersion,
|
||||
'versionCode': requestVersionCode,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data['data'];
|
||||
|
||||
if (data['hasUpdate'] == false) {
|
||||
_logger.i('✅ 已是最新版本');
|
||||
return null;
|
||||
}
|
||||
|
||||
final versionInfo = AppVersionInfo.fromJson(data);
|
||||
|
||||
// 如果已应用此版本补丁,跳过更新
|
||||
if (appliedPatchVersion == versionInfo.version && versionInfo.updateType == 'patch') {
|
||||
_logger.i('✅ 已应用最新版本补丁: ${versionInfo.version}');
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.i('✅ 发现新版本: ${versionInfo.version}');
|
||||
_logger.i('🔄 更新类型: ${versionInfo.updateType}');
|
||||
_logger.i('⚠️ 强制更新: ${versionInfo.forceUpdate}');
|
||||
|
||||
return versionInfo;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e, stack) {
|
||||
_logger.e('❌ 版本检查失败: $e');
|
||||
_logger.e('❌ 堆栈信息: $stack');
|
||||
throw Exception('网络请求失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载并应用差量补丁
|
||||
Future<bool> applyPatch({
|
||||
required String patchUrl,
|
||||
required String version,
|
||||
required int targetVersionCode,
|
||||
String? md5,
|
||||
Function(double)? onProgress,
|
||||
}) async {
|
||||
try {
|
||||
_logger.i('⬇️ 开始下载补丁: $patchUrl');
|
||||
|
||||
// 1. 手动下载补丁文件
|
||||
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('✅ 补丁下载完成,开始应用...');
|
||||
|
||||
// 2. 应用本地补丁文件
|
||||
final result = await FlutterPatcher.applyPatch(
|
||||
PatchInfo(
|
||||
version: version,
|
||||
patchUrl: 'file://$patchPath',
|
||||
targetVersionCode: targetVersionCode,
|
||||
md5: md5 ?? '',
|
||||
),
|
||||
);
|
||||
|
||||
if (result.ok) {
|
||||
_logger.i('✅ 补丁应用成功,需要冷启动生效');
|
||||
// 🔥 保存补丁版本号到文件(持久化存储)
|
||||
await _savePatchVersionInfo(version, targetVersionCode);
|
||||
_logger.i('💾 已保存补丁版本: $version ($targetVersionCode)');
|
||||
} 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 tempDir = await getTemporaryDirectory();
|
||||
final apkPath = '${tempDir.path}/app-update.apk';
|
||||
|
||||
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');
|
||||
await file.writeAsString('$version|$versionCode');
|
||||
_logger.i('💾 补丁版本已保存到: ${file.path}');
|
||||
} catch (e) {
|
||||
_logger.e('❌ 保存补丁版本信息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 从文件加载补丁版本信息
|
||||
Future<Map<String, dynamic>> _loadPatchVersionInfo() async {
|
||||
try {
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
final file = File('${directory.path}/$_patchVersionFileName');
|
||||
if (await file.exists()) {
|
||||
final content = await file.readAsString();
|
||||
final parts = content.split('|');
|
||||
if (parts.length == 2) {
|
||||
final version = parts[0];
|
||||
final versionCode = int.tryParse(parts[1]);
|
||||
if (versionCode != null) {
|
||||
_logger.i('📂 从文件读取补丁版本: $version ($versionCode)');
|
||||
return {'version': version, 'versionCode': versionCode};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {'version': null, 'versionCode': null};
|
||||
} catch (e) {
|
||||
_logger.e('❌ 加载补丁版本信息失败: $e');
|
||||
return {'version': null, 'versionCode': null};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user