496 lines
15 KiB
Dart
496 lines
15 KiB
Dart
import 'package:hive/hive.dart';
|
|
import '../../models/hive_inspiration_image.dart';
|
|
import '../../models/hive_image_folder.dart';
|
|
import '../../models/hive_image_tag.dart';
|
|
import '../../../core/utils/logger.dart';
|
|
|
|
/// 数据库迁移管理类 - 负责处理数据库版本升级和数据迁移
|
|
/// 确保应用在升级时能够正确迁移旧版本数据
|
|
class DatabaseMigration {
|
|
/// 当前数据库版本号 - 每次数据库结构变更时递增
|
|
static const int currentVersion = 1;
|
|
|
|
/// 数据库版本键名 - 在设置盒中存储版本信息的键
|
|
static const String versionKey = 'database_version';
|
|
|
|
/// 执行数据库迁移 - 根据当前版本和目标版本执行相应的迁移逻辑
|
|
/// [fromVersion] 当前数据库版本
|
|
/// [imagesBox] 图片数据盒
|
|
/// [foldersBox] 文件夹数据盒
|
|
/// [tagsBox] 标签数据盒
|
|
/// [settingsBox] 设置数据盒
|
|
static Future<void> migrateFromVersion(
|
|
int fromVersion,
|
|
Box<HiveInspirationImage> imagesBox,
|
|
Box<HiveImageFolder> foldersBox,
|
|
Box<HiveImageTag> tagsBox,
|
|
Box<dynamic> settingsBox,
|
|
) async {
|
|
// TODO: 使用日志系统替代print
|
|
// print('开始数据库迁移: 从版本 $fromVersion 到版本 $currentVersion');
|
|
|
|
// 按版本顺序执行迁移
|
|
for (int version = fromVersion + 1; version <= currentVersion; version++) {
|
|
// TODO: 使用日志系统替代print
|
|
// print('执行迁移到版本 $version');
|
|
|
|
switch (version) {
|
|
case 1:
|
|
await _migrateToVersion1(foldersBox, tagsBox);
|
|
break;
|
|
// case 2:
|
|
// await _migrateToVersion2(imagesBox, foldersBox, tagsBox);
|
|
// break;
|
|
// 在这里添加更多版本的迁移逻辑
|
|
default:
|
|
// TODO: 使用日志系统替代print
|
|
// print('未知的数据库版本: $version');
|
|
break;
|
|
}
|
|
|
|
// 更新版本号
|
|
await settingsBox.put(versionKey, version);
|
|
// TODO: 使用日志系统替代print
|
|
// print('成功迁移到版本 $version');
|
|
}
|
|
|
|
// TODO: 使用日志系统替代print
|
|
// print('数据库迁移完成');
|
|
}
|
|
|
|
/// 迁移到版本1 - 初始版本,创建默认数据
|
|
/// [foldersBox] 文件夹数据盒
|
|
/// [tagsBox] 标签数据盒
|
|
static Future<void> _migrateToVersion1(
|
|
Box<HiveImageFolder> foldersBox,
|
|
Box<HiveImageTag> tagsBox,
|
|
) async {
|
|
// TODO: 使用日志系统替代print
|
|
// print('创建版本1的默认数据...');
|
|
|
|
// 创建默认文件夹
|
|
await createDefaultFolders(foldersBox);
|
|
|
|
// 创建默认标签
|
|
await createDefaultTags(tagsBox);
|
|
|
|
// TODO: 使用日志系统替代print
|
|
// print('版本1默认数据创建完成');
|
|
}
|
|
|
|
/// 创建默认文件夹 - 初始化系统必需的文件夹
|
|
/// [foldersBox] 文件夹数据盒
|
|
static Future<void> createDefaultFolders(Box<HiveImageFolder> foldersBox) async {
|
|
// TODO: 使用日志系统替代print
|
|
// print('创建默认文件夹...');
|
|
|
|
// 默认文件夹配置
|
|
final defaultFolders = [
|
|
{
|
|
'id': 'default',
|
|
'name': '默认',
|
|
'icon': 'folder',
|
|
'description': '默认文件夹,用于存放未分类的图片',
|
|
},
|
|
{
|
|
'id': 'favorites',
|
|
'name': '收藏',
|
|
'icon': 'favorite',
|
|
'description': '收藏的图片',
|
|
},
|
|
{
|
|
'id': 'inspiration',
|
|
'name': '灵感',
|
|
'icon': 'lightbulb',
|
|
'description': '灵感素材',
|
|
},
|
|
];
|
|
|
|
for (final folderConfig in defaultFolders) {
|
|
// 检查文件夹是否已存在
|
|
if (!foldersBox.containsKey(folderConfig['id'])) {
|
|
final now = DateTime.now();
|
|
final folder = HiveImageFolder(
|
|
id: folderConfig['id'] as String,
|
|
name: folderConfig['name'] as String,
|
|
icon: folderConfig['icon'] as String,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
lastUsedAt: now,
|
|
);
|
|
|
|
await foldersBox.put(folder.id, folder);
|
|
} else {
|
|
Logger.debug('默认文件夹已存在: ${folderConfig['name']}');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 创建默认标签 - 初始化系统常用的标签
|
|
/// [tagsBox] 标签数据盒
|
|
static Future<void> createDefaultTags(Box<HiveImageTag> tagsBox) async {
|
|
Logger.info('创建默认标签...');
|
|
|
|
// 默认标签配置
|
|
final defaultTags = [
|
|
{
|
|
'id': 'tag_favorite',
|
|
'name': '收藏',
|
|
'icon': 'favorite',
|
|
'color': '#FF4444',
|
|
'description': '收藏的标签',
|
|
},
|
|
{
|
|
'id': 'tag_inspiration',
|
|
'name': '灵感',
|
|
'icon': 'lightbulb',
|
|
'color': '#FFD700',
|
|
'description': '灵感素材',
|
|
},
|
|
{
|
|
'id': 'tag_design',
|
|
'name': '设计',
|
|
'icon': 'palette',
|
|
'color': '#9C27B0',
|
|
'description': '设计相关',
|
|
},
|
|
{
|
|
'id': 'tag_photo',
|
|
'name': '摄影',
|
|
'icon': 'camera_alt',
|
|
'color': '#2196F3',
|
|
'description': '摄影作品',
|
|
},
|
|
{
|
|
'id': 'tag_nature',
|
|
'name': '自然',
|
|
'icon': 'nature',
|
|
'color': '#4CAF50',
|
|
'description': '自然风光',
|
|
},
|
|
{
|
|
'id': 'tag_architecture',
|
|
'name': '建筑',
|
|
'icon': 'location_city',
|
|
'color': '#795548',
|
|
'description': '建筑摄影',
|
|
},
|
|
{
|
|
'id': 'tag_food',
|
|
'name': '美食',
|
|
'icon': 'restaurant',
|
|
'color': '#FF9800',
|
|
'description': '美食摄影',
|
|
},
|
|
{
|
|
'id': 'tag_travel',
|
|
'name': '旅行',
|
|
'icon': 'flight',
|
|
'color': '#00BCD4',
|
|
'description': '旅行记录',
|
|
},
|
|
];
|
|
|
|
for (final tagConfig in defaultTags) {
|
|
// 检查标签是否已存在
|
|
if (!tagsBox.containsKey(tagConfig['id'])) {
|
|
final now = DateTime.now();
|
|
final tag = HiveImageTag(
|
|
id: tagConfig['id'] as String,
|
|
name: tagConfig['name'] as String,
|
|
icon: tagConfig['icon'] as String,
|
|
color: tagConfig['color'] as String,
|
|
usageCount: 0,
|
|
lastUsedAt: now,
|
|
);
|
|
|
|
await tagsBox.put(tag.id, tag);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 验证数据库完整性 - 检查数据库是否损坏或缺少必要数据
|
|
/// [imagesBox] 图片数据盒
|
|
/// [foldersBox] 文件夹数据盒
|
|
/// [tagsBox] 标签数据盒
|
|
/// 返回验证结果,如果发现问题会尝试修复
|
|
static Future<bool> validateDatabaseIntegrity(
|
|
Box<HiveInspirationImage> imagesBox,
|
|
Box<HiveImageFolder> foldersBox,
|
|
Box<HiveImageTag> tagsBox,
|
|
) async {
|
|
Logger.info('开始数据库完整性验证...');
|
|
|
|
bool isValid = true;
|
|
|
|
try {
|
|
// 验证默认文件夹是否存在
|
|
if (!foldersBox.containsKey('default')) {
|
|
// TODO: 使用日志系统替代print
|
|
// print('警告:缺少默认文件夹,正在创建...');
|
|
await createDefaultFolders(foldersBox);
|
|
isValid = false;
|
|
}
|
|
|
|
// 验证图片数据完整性
|
|
for (final image in imagesBox.values) {
|
|
// 验证文件夹关联
|
|
if (image.folderId != null && !foldersBox.containsKey(image.folderId)) {
|
|
// TODO: 使用日志系统替代print
|
|
// print('警告:图片 ${image.id} 关联了不存在的文件夹 ${image.folderId},正在修复...');
|
|
// 将图片移动到默认文件夹
|
|
final updatedImage = HiveInspirationImage(
|
|
id: image.id,
|
|
filePath: image.filePath,
|
|
thumbnailPath: image.thumbnailPath,
|
|
folderId: 'default',
|
|
tags: image.tags,
|
|
note: image.note,
|
|
createdAt: image.createdAt,
|
|
updatedAt: image.updatedAt,
|
|
originalName: image.originalName,
|
|
fileSize: image.fileSize,
|
|
mimeType: image.mimeType,
|
|
width: image.width,
|
|
height: image.height,
|
|
isFavorite: image.isFavorite,
|
|
);
|
|
await imagesBox.put(image.id, updatedImage);
|
|
isValid = false;
|
|
}
|
|
|
|
// 验证标签关联
|
|
for (final tagId in image.tags) {
|
|
if (!tagsBox.containsKey(tagId)) {
|
|
// TODO: 使用日志系统替代print
|
|
// print('警告:图片 ${image.id} 关联了不存在的标签 $tagId');
|
|
// 移除无效的标签关联
|
|
final updatedTags = List<String>.from(image.tags)..remove(tagId);
|
|
final updatedImage = HiveInspirationImage(
|
|
id: image.id,
|
|
filePath: image.filePath,
|
|
thumbnailPath: image.thumbnailPath,
|
|
folderId: image.folderId,
|
|
tags: updatedTags,
|
|
note: image.note,
|
|
createdAt: image.createdAt,
|
|
updatedAt: image.updatedAt,
|
|
originalName: image.originalName,
|
|
fileSize: image.fileSize,
|
|
mimeType: image.mimeType,
|
|
width: image.width,
|
|
height: image.height,
|
|
isFavorite: image.isFavorite,
|
|
);
|
|
await imagesBox.put(image.id, updatedImage);
|
|
isValid = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 验证标签使用次数统计
|
|
for (final tag in tagsBox.values) {
|
|
final actualUsageCount = imagesBox.values
|
|
.where((image) => image.tags.contains(tag.id))
|
|
.length;
|
|
|
|
if (tag.usageCount != actualUsageCount) {
|
|
final updatedTag = HiveImageTag(
|
|
id: tag.id,
|
|
name: tag.name,
|
|
icon: tag.icon,
|
|
color: tag.color,
|
|
usageCount: actualUsageCount,
|
|
lastUsedAt: tag.lastUsedAt,
|
|
);
|
|
await tagsBox.put(tag.id, updatedTag);
|
|
isValid = false;
|
|
}
|
|
}
|
|
|
|
} catch (e) {
|
|
// TODO: 使用日志系统替代print
|
|
// print('数据库完整性验证失败: $e');
|
|
isValid = false;
|
|
}
|
|
|
|
if (isValid) {
|
|
// TODO: 使用日志系统替代print
|
|
// print('数据库完整性验证通过');
|
|
} else {
|
|
// TODO: 使用日志系统替代print
|
|
// print('数据库完整性验证发现问题并已修复');
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
/// 备份数据库 - 创建数据库的备份副本
|
|
/// [backupPath] 备份文件路径
|
|
/// 返回是否备份成功
|
|
static Future<bool> backupDatabase(String backupPath) async {
|
|
try {
|
|
// TODO: 使用日志系统替代print
|
|
// print('开始备份数据库到: $backupPath');
|
|
|
|
// TODO: 实现数据库备份逻辑
|
|
// 这需要根据实际的Hive存储路径来实现
|
|
|
|
// TODO: 使用日志系统替代print
|
|
// print('数据库备份完成');
|
|
return true;
|
|
} catch (e) {
|
|
// TODO: 使用日志系统替代print
|
|
// print('数据库备份失败: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 恢复数据库 - 从备份文件恢复数据库
|
|
/// [backupPath] 备份文件路径
|
|
/// 返回是否恢复成功
|
|
static Future<bool> restoreDatabase(String backupPath) async {
|
|
try {
|
|
// TODO: 使用日志系统替代print
|
|
// print('开始从备份恢复数据库: $backupPath');
|
|
|
|
// TODO: 实现数据库恢复逻辑
|
|
// 这需要根据实际的Hive存储路径来实现
|
|
|
|
// TODO: 使用日志系统替代print
|
|
// print('数据库恢复完成');
|
|
return true;
|
|
} catch (e) {
|
|
// TODO: 使用日志系统替代print
|
|
// print('数据库恢复失败: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 获取数据库统计信息 - 获取数据库的使用统计
|
|
/// [imagesBox] 图片数据盒
|
|
/// [foldersBox] 文件夹数据盒
|
|
/// [tagsBox] 标签数据盒
|
|
/// 返回包含统计信息的Map
|
|
static Map<String, dynamic> getDatabaseStats(
|
|
Box<HiveInspirationImage> imagesBox,
|
|
Box<HiveImageFolder> foldersBox,
|
|
Box<HiveImageTag> tagsBox,
|
|
) {
|
|
return {
|
|
'version': currentVersion,
|
|
'images_count': imagesBox.length,
|
|
'folders_count': foldersBox.length,
|
|
'tags_count': tagsBox.length,
|
|
'total_size': _calculateTotalSize(imagesBox),
|
|
'last_updated': DateTime.now().toIso8601String(),
|
|
};
|
|
}
|
|
|
|
/// 计算总数据大小 - 估算数据库的总大小
|
|
/// [imagesBox] 图片数据盒
|
|
/// 返回估算的总大小(字节)
|
|
static int _calculateTotalSize(Box<HiveInspirationImage> imagesBox) {
|
|
int totalSize = 0;
|
|
|
|
for (final image in imagesBox.values) {
|
|
totalSize += image.fileSize;
|
|
}
|
|
|
|
return totalSize;
|
|
}
|
|
|
|
/// 清理数据库 - 清理无效数据和临时文件
|
|
/// [imagesBox] 图片数据盒
|
|
/// [foldersBox] 文件夹数据盒
|
|
/// [tagsBox] 标签数据盒
|
|
/// 返回清理结果的统计信息
|
|
static Future<Map<String, int>> cleanupDatabase(
|
|
Box<HiveInspirationImage> imagesBox,
|
|
Box<HiveImageFolder> foldersBox,
|
|
Box<HiveImageTag> tagsBox,
|
|
) async {
|
|
// TODO: 使用日志系统替代print
|
|
// print('开始清理数据库...');
|
|
|
|
final cleanupStats = <String, int>{};
|
|
|
|
try {
|
|
// 清理未使用的标签
|
|
final unusedTags = tagsBox.values.where((tag) => tag.usageCount == 0).toList();
|
|
cleanupStats['unused_tags_removed'] = unusedTags.length;
|
|
|
|
for (final tag in unusedTags) {
|
|
await tagsBox.delete(tag.id);
|
|
}
|
|
|
|
// 清理无效的图片文件引用
|
|
final invalidImages = imagesBox.values.where((image) {
|
|
// 这里可以添加文件存在性检查
|
|
// 暂时只检查文件夹关联
|
|
return image.folderId != null && !foldersBox.containsKey(image.folderId);
|
|
}).toList();
|
|
|
|
cleanupStats['invalid_images_fixed'] = invalidImages.length;
|
|
|
|
for (final image in invalidImages) {
|
|
final updatedImage = HiveInspirationImage(
|
|
id: image.id,
|
|
filePath: image.filePath,
|
|
thumbnailPath: image.thumbnailPath,
|
|
folderId: 'default',
|
|
tags: image.tags,
|
|
note: image.note,
|
|
createdAt: image.createdAt,
|
|
updatedAt: image.updatedAt,
|
|
originalName: image.originalName,
|
|
fileSize: image.fileSize,
|
|
mimeType: image.mimeType,
|
|
width: image.width,
|
|
height: image.height,
|
|
isFavorite: image.isFavorite,
|
|
);
|
|
await imagesBox.put(image.id, updatedImage);
|
|
}
|
|
|
|
// TODO: 使用日志系统替代print
|
|
// print('数据库清理完成: $cleanupStats');
|
|
|
|
} catch (e) {
|
|
// TODO: 使用日志系统替代print
|
|
// print('数据库清理失败: $e');
|
|
}
|
|
|
|
return cleanupStats;
|
|
}
|
|
}
|
|
|
|
/// 数据库异常类 - 数据库操作相关的异常
|
|
class DatabaseException implements Exception {
|
|
final String message;
|
|
final String? code;
|
|
final dynamic originalError;
|
|
|
|
DatabaseException(this.message, {this.code, this.originalError});
|
|
|
|
@override
|
|
String toString() => 'DatabaseException: $message${code != null ? ' (代码: $code)' : ''}';
|
|
}
|
|
|
|
/// 数据库迁移异常类 - 数据迁移过程中的异常
|
|
class DatabaseMigrationException extends DatabaseException {
|
|
final int fromVersion;
|
|
final int toVersion;
|
|
|
|
DatabaseMigrationException({
|
|
required this.fromVersion,
|
|
required this.toVersion,
|
|
required String message,
|
|
String? code,
|
|
dynamic originalError,
|
|
}) : super(
|
|
'数据库迁移失败 (版本 $fromVersion -> $toVersion): $message',
|
|
code: code,
|
|
originalError: originalError,
|
|
);
|
|
} |