核心工具类、错误处理和日志系统

This commit is contained in:
ddshi 2025-09-17 10:08:18 +08:00
parent 9cc9288d6e
commit 8005e21110
5 changed files with 340 additions and 16 deletions

View File

@ -10,7 +10,8 @@
"Bash(flutter test:*)",
"Bash(flutter build:*)",
"Bash(flutter clean:*)",
"Bash(dart analyze:*)"
"Bash(dart analyze:*)",
"Bash(dart test:*)"
],
"deny": [],
"ask": []

View File

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart' as path_provider;
import '../errors/app_error.dart';
import 'logger.dart';
@ -12,7 +13,7 @@ class FileUtils {
/// 获取应用文档目录
static Future<Directory> getApplicationDocumentsDirectory() async {
try {
final directory = await getApplicationDocumentsDirectory();
final directory = await path_provider.getApplicationDocumentsDirectory();
Logger.debug('获取应用文档目录: ${directory.path}');
return directory;
} catch (error, stackTrace) {
@ -28,7 +29,7 @@ class FileUtils {
/// 获取临时目录
static Future<Directory> getTemporaryDirectory() async {
try {
final directory = await getTemporaryDirectory();
final directory = await path_provider.getTemporaryDirectory();
Logger.debug('获取临时目录: ${directory.path}');
return directory;
} catch (error, stackTrace) {
@ -44,7 +45,7 @@ class FileUtils {
/// 获取应用支持目录(用于存储应用数据)
static Future<Directory> getApplicationSupportDirectory() async {
try {
final directory = await getApplicationSupportDirectory();
final directory = await path_provider.getApplicationSupportDirectory();
Logger.debug('获取应用支持目录: ${directory.path}');
return directory;
} catch (error, stackTrace) {

View File

@ -0,0 +1,322 @@
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:uuid/uuid.dart';
import '../errors/app_error.dart';
import 'logger.dart';
/// 路径管理工具类
/// 提供UUID生成、路径构建、文件命名等路径相关功能
class PathUtils {
// 私有构造函数,防止实例化
PathUtils._();
static const Uuid _uuid = Uuid();
/// 生成唯一ID(UUID v4)
/// 返回标准的UUID格式字符串
static String generateUniqueId() {
return _uuid.v4();
}
/// 生成短格式唯一ID
/// 截取UUID前8位,适合文件名使用
static String generateShortId() {
return _uuid.v4().substring(0, 8);
}
/// 生成基于时间的唯一ID
/// 格式:时间戳_UUID,保证唯一性和可读性
static String generateTimeBasedId() {
final timestamp = DateTime.now().millisecondsSinceEpoch;
final shortUuid = generateShortId();
return '${timestamp}_$shortUuid';
}
/// 生成图片文件的唯一文件名
/// [extension] 文件扩展名(包含点号,如 .jpg)
/// [prefix] 可选前缀,如 "IMG_"
static String generateImageFileName({
required String extension,
String? prefix,
}) {
final baseName = generateTimeBasedId();
final fileName = prefix != null ? '${prefix}$baseName' : baseName;
return '$fileName$extension';
}
/// 生成缩略图文件名
/// 在原文件名基础上添加 _thumb 后缀
static String generateThumbnailFileName(String originalFileName) {
final nameWithoutExt = path.basenameWithoutExtension(originalFileName);
final extension = path.extension(originalFileName);
return '${nameWithoutExt}_thumb$extension';
}
/// 构建日期分类的存储路径
/// 按照 /yyyy/MM/dd/ 格式组织
/// [basePath] 基础路径
/// [date] 日期,默认为当前时间
static String buildDateBasedPath({
required String basePath,
DateTime? date,
}) {
final targetDate = date ?? DateTime.now();
final year = targetDate.year.toString();
final month = targetDate.month.toString().padLeft(2, '0');
final day = targetDate.day.toString().padLeft(2, '0');
return path.join(basePath, year, month, day);
}
/// 构建图片存储路径
/// 返回完整的图片存储路径,包含日期分类
static Future<String> buildImageStoragePath({
required String fileName,
DateTime? date,
}) async {
try {
final supportDir = await path_provider.getApplicationSupportDirectory();
final imagesBasePath = path.join(supportDir.path, 'images');
final dateBasedPath = buildDateBasedPath(
basePath: imagesBasePath,
date: date,
);
return path.join(dateBasedPath, fileName);
} catch (error, stackTrace) {
Logger.error('构建图片存储路径失败', error: error, stackTrace: stackTrace);
throw StorageError(
message: '构建图片存储路径失败: ${error.toString()}',
code: 'PATH_ERROR',
stackTrace: stackTrace,
);
}
}
/// 构建缩略图存储路径
/// 返回完整的缩略图存储路径,包含日期分类
static Future<String> buildThumbnailStoragePath({
required String fileName,
DateTime? date,
}) async {
try {
final supportDir = await path_provider.getApplicationSupportDirectory();
final thumbnailsBasePath = path.join(supportDir.path, 'thumbnails');
final dateBasedPath = buildDateBasedPath(
basePath: thumbnailsBasePath,
date: date,
);
return path.join(dateBasedPath, fileName);
} catch (error, stackTrace) {
Logger.error('构建缩略图存储路径失败', error: error, stackTrace: stackTrace);
throw StorageError(
message: '构建缩略图存储路径失败: ${error.toString()}',
code: 'PATH_ERROR',
stackTrace: stackTrace,
);
}
}
/// 获取文件的相对路径
/// 相对于应用支持目录的路径
static Future<String> getRelativePath(String fullPath) async {
try {
final supportDir = await path_provider.getApplicationSupportDirectory();
return path.relative(fullPath, from: supportDir.path);
} catch (error, stackTrace) {
Logger.error('获取相对路径失败', error: error, stackTrace: stackTrace);
throw StorageError(
message: '获取相对路径失败: ${error.toString()}',
code: 'PATH_ERROR',
stackTrace: stackTrace,
);
}
}
/// 从相对路径获取完整路径
static Future<String> getFullPath(String relativePath) async {
try {
final supportDir = await path_provider.getApplicationSupportDirectory();
return path.normalize(path.join(supportDir.path, relativePath));
} catch (error, stackTrace) {
Logger.error('获取完整路径失败', error: error, stackTrace: stackTrace);
throw StorageError(
message: '获取完整路径失败: ${error.toString()}',
code: 'PATH_ERROR',
stackTrace: stackTrace,
);
}
}
/// 检查路径是否安全
/// 防止路径遍历攻击
static bool isPathSafe(String pathToCheck) {
try {
// 检查是否包含路径遍历字符
if (pathToCheck.contains('..') || pathToCheck.contains('~')) {
return false;
}
// 检查是否为绝对路径
if (path.isAbsolute(pathToCheck)) {
return false;
}
// 检查路径是否规范化
final normalizedPath = path.normalize(pathToCheck);
return normalizedPath == pathToCheck;
} catch (error) {
Logger.error('路径安全检查失败', error: error);
return false;
}
}
/// 获取路径的父目录
static String getParentDirectory(String filePath) {
return path.dirname(filePath);
}
/// 检查文件扩展名是否有效
static bool isValidFileExtension(String fileName, List<String> validExtensions) {
final extension = path.extension(fileName).toLowerCase();
return validExtensions.map((ext) => ext.toLowerCase()).contains(extension);
}
/// 生成备份文件名
/// 在原文件名基础上添加备份时间戳
static String generateBackupFileName(String originalFileName) {
final nameWithoutExt = path.basenameWithoutExtension(originalFileName);
final extension = path.extension(originalFileName);
final timestamp = DateTime.now().millisecondsSinceEpoch;
return '${nameWithoutExt}_backup_$timestamp$extension';
}
/// 生成临时文件名
/// 用于临时文件操作,格式:temp_UUID.扩展名
static String generateTempFileName(String extension) {
final uuid = generateShortId();
return 'temp_$uuid$extension';
}
/// 解析日期分类路径
/// 从 /yyyy/MM/dd/ 格式的路径中提取日期信息
static DateTime? parseDateFromPath(String dateBasedPath) {
try {
// 标准化路径分隔符
final normalizedPath = path.normalize(dateBasedPath);
final pathParts = normalizedPath.split(path.separator);
// 查找年、月、日的位置
int yearIndex = -1, monthIndex = -1, dayIndex = -1;
for (int i = 0; i < pathParts.length; i++) {
final part = pathParts[i];
if (part.length == 4 && int.tryParse(part) != null && int.parse(part) > 1900 && int.parse(part) < 3000) {
yearIndex = i;
} else if (part.length == 2 && int.tryParse(part) != null && int.parse(part) >= 1 && int.parse(part) <= 12) {
if (monthIndex == -1) {
monthIndex = i;
} else if (dayIndex == -1) {
dayIndex = i;
}
}
}
if (yearIndex != -1 && monthIndex != -1 && dayIndex != -1) {
final year = int.parse(pathParts[yearIndex]);
final month = int.parse(pathParts[monthIndex]);
final day = int.parse(pathParts[dayIndex]);
return DateTime(year, month, day);
}
return null;
} catch (error) {
Logger.error('解析日期路径失败', error: error);
return null;
}
}
/// 获取路径的层级深度
static int getPathDepth(String pathToCheck) {
return path.split(path.normalize(pathToCheck)).length;
}
/// 检查两个路径是否有相同的父目录
static bool hasSameParentDirectory(String path1, String path2) {
final parent1 = path.dirname(path.normalize(path1));
final parent2 = path.dirname(path.normalize(path2));
return parent1 == parent2;
}
/// 生成安全的文件名
/// 移除不安全的字符,确保文件名有效
static String sanitizeFileName(String fileName) {
// 移除或替换不安全的字符
final sanitized = fileName
.replaceAll(RegExp(r'[<>:"/\\|?*]'), '_')
.replaceAll(RegExp(r'\s+'), '_')
.replaceAll(RegExp(r'_{2,}'), '_')
.trim();
// 确保文件名不为空
if (sanitized.isEmpty) {
return 'unnamed_file';
}
return sanitized;
}
}
/// 路径构建器类
/// 提供链式调用的路径构建方式
class PathBuilder {
final String _basePath;
final List<String> _segments;
PathBuilder(this._basePath) : _segments = [];
/// 添加路径段
PathBuilder add(String segment) {
_segments.add(segment);
return this;
}
/// 添加日期段
PathBuilder addDate(DateTime date) {
final year = date.year.toString();
final month = date.month.toString().padLeft(2, '0');
final day = date.day.toString().padLeft(2, '0');
_segments.addAll([year, month, day]);
return this;
}
/// 添加UUID段
PathBuilder addUuid() {
_segments.add(PathUtils.generateShortId());
return this;
}
/// 构建完整路径
String build() {
if (_segments.isEmpty) {
return _basePath;
}
return path.join(_basePath, _segments.join(path.separator));
}
/// 创建目录
Future<Directory> createDirectory() async {
final fullPath = build();
final directory = Directory(fullPath);
if (!await directory.exists()) {
await directory.create(recursive: true);
}
return directory;
}
}

View File

@ -2,6 +2,7 @@ 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';
/// 数据库迁移管理类 - 负责处理数据库版本升级和数据迁移
/// 确保应用在升级时能够正确迁移旧版本数据
@ -120,8 +121,7 @@ class DatabaseMigration {
await foldersBox.put(folder.id, folder);
} else {
// TODO: 使用日志系统替代print
// print('默认文件夹已存在: ${folderConfig['name']}');
Logger.debug('默认文件夹已存在: ${folderConfig['name']}');
}
}
}
@ -129,7 +129,7 @@ class DatabaseMigration {
/// 创建默认标签 - 初始化系统常用的标签
/// [tagsBox] 标签数据盒
static Future<void> createDefaultTags(Box<HiveImageTag> tagsBox) async {
print('创建默认标签...');
Logger.info('创建默认标签...');
// 默认标签配置
final defaultTags = [
@ -219,8 +219,7 @@ class DatabaseMigration {
Box<HiveImageFolder> foldersBox,
Box<HiveImageTag> tagsBox,
) async {
// TODO: 使用日志系统替代print
// print('开始数据库完整性验证...');
Logger.info('开始数据库完整性验证...');
bool isValid = true;

View File

@ -3,6 +3,7 @@ import 'package:path_provider/path_provider.dart' as path_provider;
import '../../../data/models/hive_inspiration_image.dart';
import '../../../data/models/hive_image_folder.dart';
import '../../../data/models/hive_image_tag.dart';
import '../../../core/utils/logger.dart';
import 'database_migration.dart';
/// Hive数据库管理类 - 负责本地数据存储和初始化
@ -65,7 +66,7 @@ class HiveDatabase {
if (currentDbVersion < _currentVersion) {
try {
print('检测到数据库版本更新: $currentDbVersion -> $_currentVersion');
Logger.info('检测到数据库版本更新: $currentDbVersion -> $_currentVersion');
// 使用专门的迁移管理类执行迁移
await DatabaseMigration.migrateFromVersion(
@ -76,15 +77,15 @@ class HiveDatabase {
settingsBox,
);
print('数据库迁移完成');
Logger.info('数据库迁移完成');
} catch (e) {
print('数据库迁移失败: $e');
Logger.error('数据库迁移失败', error: e);
// 如果迁移失败,尝试恢复到已知状态
await _handleMigrationFailure(currentDbVersion, e);
}
} else if (currentDbVersion > _currentVersion) {
// 处理降级情况(通常不应该发生)
print('警告:数据库版本高于应用版本 ($currentDbVersion > $_currentVersion)');
Logger.warning('数据库版本高于应用版本 ($currentDbVersion > $_currentVersion)');
await settingsBox.put(_versionKey, _currentVersion);
}
}
@ -93,7 +94,7 @@ class HiveDatabase {
/// [fromVersion] 迁移起始版本
/// [error] 迁移过程中出现的错误
static Future<void> _handleMigrationFailure(int fromVersion, dynamic error) async {
print('处理迁移失败,尝试恢复到安全状态...');
Logger.error('处理迁移失败,尝试恢复到安全状态...', error: error);
try {
// 如果是最初版本(0)的迁移失败,可以尝试重新创建基础结构
@ -107,7 +108,7 @@ class HiveDatabase {
await settingsBox.put('last_migration_time', DateTime.now().toIso8601String());
} catch (recoveryError) {
print('迁移恢复失败: $recoveryError');
Logger.fatal('迁移恢复失败', error: recoveryError);
// 如果恢复也失败,只能抛出异常让上层处理
throw DatabaseMigrationException(
fromVersion: fromVersion,
@ -132,7 +133,7 @@ class HiveDatabase {
/// 创建紧急恢复数据 - 当迁移失败时的紧急恢复方案
/// 确保应用至少能正常运行,包含最基本的数据结构
static Future<void> _createEmergencyData() async {
print('创建紧急恢复数据...');
Logger.warning('创建紧急恢复数据...');
try {
final foldersBox = Hive.box<HiveImageFolder>(_foldersBoxName);