添加修复大包更新安装覆盖的-待测试

添加修复了更新接口的令狐适配
添加了新的服务端地址-
This commit is contained in:
2026-05-27 16:05:21 +08:00
parent afef5c4ef1
commit 938aac2db9
10 changed files with 913 additions and 88 deletions

View File

@@ -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";
/// 账号相关
// 登录

View 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());
}
}

View 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('立即重启'),
),
],
),
);
}
}

View 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);
}

View 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};
}
}
}