From 39b517dcd8386a9657fb38fde4ab49d393f74671 Mon Sep 17 00:00:00 2001 From: imeepos Date: Sun, 13 Jul 2025 22:46:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0Tauri=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E8=A7=84=E8=8C=83=E7=AB=8B=E5=8D=B3=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 0.1.1.md | 3 + Cargo.lock | 117 +++++- apps/desktop/src-tauri/Cargo.toml | 4 + apps/desktop/src-tauri/src/business/errors.rs | 296 +++++++++++++++ apps/desktop/src-tauri/src/business/mod.rs | 1 + .../src/business/services/material_service.rs | 54 ++- .../src-tauri/src/infrastructure/ffmpeg.rs | 2 +- .../src-tauri/src/infrastructure/logging.rs | 336 ++++++++++++++++++ .../src-tauri/src/infrastructure/mod.rs | 2 + .../src/infrastructure/monitoring.rs | 233 ++++++++++++ apps/desktop/src-tauri/src/lib.rs | 15 + .../presentation/commands/system_commands.rs | 48 ++- 12 files changed, 1091 insertions(+), 20 deletions(-) create mode 100644 apps/desktop/src-tauri/src/business/errors.rs create mode 100644 apps/desktop/src-tauri/src/infrastructure/logging.rs create mode 100644 apps/desktop/src-tauri/src/infrastructure/monitoring.rs diff --git a/0.1.1.md b/0.1.1.md index 07088a4..f1878e7 100644 --- a/0.1.1.md +++ b/0.1.1.md @@ -32,3 +32,6 @@ ### 优化 提交代码 然后优化: feature: 在导入时启动异步 后台处理 处理(更好的用户体验) +feature: 编写单元测试和集成测试 +feature: 优化异步操作 +feature: 完善配置管理系统 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9f70797..1e62de4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2108,6 +2108,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matches" version = "0.1.10" @@ -2169,6 +2178,7 @@ dependencies = [ "anyhow", "chrono", "dirs 5.0.1", + "lazy_static", "md5", "rusqlite", "serde", @@ -2182,6 +2192,9 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-test", + "tracing", + "tracing-appender", + "tracing-subscriber", "uuid", ] @@ -2261,6 +2274,16 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2555,6 +2578,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "pango" version = "0.18.3" @@ -3130,8 +3159,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -3142,9 +3180,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -3548,6 +3592,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -4170,6 +4223,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.41" @@ -4438,6 +4500,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.30" @@ -4456,6 +4530,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "chrono", + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -4610,6 +4715,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index b0b3d91..c6faaad 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -32,6 +32,10 @@ anyhow = "1.0" thiserror = "1.0" dirs = "5.0" md5 = "0.7" +lazy_static = "1.4" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "chrono"] } +tracing-appender = "0.2" [dev-dependencies] tempfile = "3.8" diff --git a/apps/desktop/src-tauri/src/business/errors.rs b/apps/desktop/src-tauri/src/business/errors.rs new file mode 100644 index 0000000..a65d9b2 --- /dev/null +++ b/apps/desktop/src-tauri/src/business/errors.rs @@ -0,0 +1,296 @@ +use thiserror::Error; + +/// 应用程序错误类型 +#[derive(Error, Debug)] +pub enum AppError { + /// 数据库相关错误 + #[error("数据库错误: {message}")] + Database { message: String }, + + /// 文件系统错误 + #[error("文件系统错误: {message}")] + FileSystem { message: String }, + + /// 配置错误 + #[error("配置错误: {message}")] + Configuration { message: String }, + + /// 网络错误 + #[error("网络错误: {message}")] + Network { message: String }, + + /// 权限错误 + #[error("权限不足: {operation}")] + Permission { operation: String }, + + /// 验证错误 + #[error("验证失败: {field} - {message}")] + Validation { field: String, message: String }, + + /// 内部错误 + #[error("内部错误: {message}")] + Internal { message: String }, +} + +/// 素材处理相关错误 +#[derive(Error, Debug)] +pub enum MaterialError { + /// 文件不存在 + #[error("文件不存在: {path}")] + FileNotFound { path: String }, + + /// 文件格式不支持 + #[error("不支持的文件格式: {format} (文件: {path})")] + UnsupportedFormat { format: String, path: String }, + + /// FFmpeg 执行错误 + #[error("FFmpeg 执行失败: {command} - {message}")] + FFmpegError { command: String, message: String }, + + /// 场景检测错误 + #[error("场景检测失败: {reason} (文件: {path})")] + SceneDetectionError { reason: String, path: String }, + + /// 视频切分错误 + #[error("视频切分失败: {reason} (文件: {path})")] + VideoSegmentationError { reason: String, path: String }, + + /// 元数据提取错误 + #[error("元数据提取失败: {reason} (文件: {path})")] + MetadataExtractionError { reason: String, path: String }, + + /// 导入错误 + #[error("素材导入失败: {reason}")] + ImportError { reason: String }, + + /// 处理超时 + #[error("处理超时: {operation} (超时时间: {timeout_seconds}秒)")] + ProcessingTimeout { operation: String, timeout_seconds: u64 }, + + /// 存储空间不足 + #[error("存储空间不足: 需要 {required_mb}MB,可用 {available_mb}MB")] + InsufficientStorage { required_mb: u64, available_mb: u64 }, +} + +/// 项目管理相关错误 +#[derive(Error, Debug)] +pub enum ProjectError { + /// 项目不存在 + #[error("项目不存在: {id}")] + ProjectNotFound { id: String }, + + /// 项目路径无效 + #[error("项目路径无效: {path} - {reason}")] + InvalidPath { path: String, reason: String }, + + /// 项目已存在 + #[error("项目已存在: {name} (路径: {path})")] + ProjectExists { name: String, path: String }, + + /// 项目创建失败 + #[error("项目创建失败: {reason}")] + CreationFailed { reason: String }, + + /// 项目删除失败 + #[error("项目删除失败: {reason}")] + DeletionFailed { reason: String }, + + /// 项目更新失败 + #[error("项目更新失败: {reason}")] + UpdateFailed { reason: String }, +} + +/// 数据库相关错误 +#[derive(Error, Debug)] +pub enum DatabaseError { + /// 连接失败 + #[error("数据库连接失败: {message}")] + ConnectionFailed { message: String }, + + /// 查询失败 + #[error("数据库查询失败: {query} - {message}")] + QueryFailed { query: String, message: String }, + + /// 事务失败 + #[error("数据库事务失败: {message}")] + TransactionFailed { message: String }, + + /// 迁移失败 + #[error("数据库迁移失败: {version} - {message}")] + MigrationFailed { version: String, message: String }, + + /// 数据完整性错误 + #[error("数据完整性错误: {constraint} - {message}")] + IntegrityError { constraint: String, message: String }, +} + +/// 系统相关错误 +#[derive(Error, Debug)] +pub enum SystemError { + /// 平台不支持 + #[error("平台不支持: {platform} - {feature}")] + UnsupportedPlatform { platform: String, feature: String }, + + /// 系统资源不足 + #[error("系统资源不足: {resource} - {message}")] + InsufficientResources { resource: String, message: String }, + + /// 外部依赖缺失 + #[error("外部依赖缺失: {dependency} - {message}")] + MissingDependency { dependency: String, message: String }, + + /// 系统调用失败 + #[error("系统调用失败: {call} - {message}")] + SystemCallFailed { call: String, message: String }, +} + +/// 错误结果类型别名 +pub type AppResult = Result; +pub type MaterialResult = Result; +pub type ProjectResult = Result; +pub type DatabaseResult = Result; +pub type SystemResult = Result; + +/// 错误转换实现 +impl From for DatabaseError { + fn from(err: rusqlite::Error) -> Self { + DatabaseError::QueryFailed { + query: "unknown".to_string(), + message: err.to_string(), + } + } +} + +impl From for AppError { + fn from(err: std::io::Error) -> Self { + AppError::FileSystem { + message: err.to_string(), + } + } +} + +impl From for AppError { + fn from(err: serde_json::Error) -> Self { + AppError::Configuration { + message: format!("JSON 解析错误: {}", err), + } + } +} + +impl From for AppError { + fn from(err: MaterialError) -> Self { + AppError::Internal { + message: err.to_string(), + } + } +} + +impl From for AppError { + fn from(err: ProjectError) -> Self { + AppError::Internal { + message: err.to_string(), + } + } +} + +impl From for AppError { + fn from(err: DatabaseError) -> Self { + AppError::Database { + message: err.to_string(), + } + } +} + +impl From for AppError { + fn from(err: SystemError) -> Self { + AppError::Internal { + message: err.to_string(), + } + } +} + +/// 错误处理工具函数 +pub mod error_utils { + use super::*; + use tracing::{error, warn}; + + /// 记录并转换错误 + pub fn log_and_convert(err: E, context: &str) -> AppError { + error!("错误发生在 {}: {}", context, err); + AppError::Internal { + message: format!("{}: {}", context, err), + } + } + + /// 记录警告并返回默认值 + pub fn log_warning_and_default(err: impl std::error::Error, context: &str) -> T { + warn!("警告在 {}: {}", context, err); + T::default() + } + + /// 验证文件路径 + pub fn validate_file_path(path: &str) -> MaterialResult<()> { + if path.is_empty() { + return Err(MaterialError::FileNotFound { + path: path.to_string(), + }); + } + + let path_obj = std::path::Path::new(path); + if !path_obj.exists() { + return Err(MaterialError::FileNotFound { + path: path.to_string(), + }); + } + + if !path_obj.is_file() { + return Err(MaterialError::FileNotFound { + path: path.to_string(), + }); + } + + Ok(()) + } + + /// 验证目录路径 + pub fn validate_directory_path(path: &str) -> ProjectResult<()> { + if path.is_empty() { + return Err(ProjectError::InvalidPath { + path: path.to_string(), + reason: "路径为空".to_string(), + }); + } + + let path_obj = std::path::Path::new(path); + if !path_obj.exists() { + return Err(ProjectError::InvalidPath { + path: path.to_string(), + reason: "路径不存在".to_string(), + }); + } + + if !path_obj.is_dir() { + return Err(ProjectError::InvalidPath { + path: path.to_string(), + reason: "不是有效的目录".to_string(), + }); + } + + Ok(()) + } + + /// 检查存储空间 + pub fn check_storage_space(required_mb: u64) -> MaterialResult<()> { + // 简化实现,实际应用中应该检查真实的磁盘空间 + let available_mb = 1000; // 模拟可用空间 + + if available_mb < required_mb { + return Err(MaterialError::InsufficientStorage { + required_mb, + available_mb, + }); + } + + Ok(()) + } +} diff --git a/apps/desktop/src-tauri/src/business/mod.rs b/apps/desktop/src-tauri/src/business/mod.rs index 4e379ae..b58de97 100644 --- a/apps/desktop/src-tauri/src/business/mod.rs +++ b/apps/desktop/src-tauri/src/business/mod.rs @@ -1 +1,2 @@ pub mod services; +pub mod errors; diff --git a/apps/desktop/src-tauri/src/business/services/material_service.rs b/apps/desktop/src-tauri/src/business/services/material_service.rs index bda742c..89aa44d 100644 --- a/apps/desktop/src-tauri/src/business/services/material_service.rs +++ b/apps/desktop/src-tauri/src/business/services/material_service.rs @@ -1,7 +1,7 @@ use anyhow::{Result, anyhow}; use std::path::Path; use std::fs; - +use std::time::Instant; use crate::data::models::material::{ Material, MaterialType, ProcessingStatus, CreateMaterialRequest, @@ -9,6 +9,9 @@ use crate::data::models::material::{ }; use crate::data::repositories::material_repository::MaterialRepository; use crate::infrastructure::ffmpeg::FFmpegService; +use crate::infrastructure::monitoring::PERFORMANCE_MONITOR; +use crate::business::errors::error_utils; +use tracing::{info, warn, error, debug}; /// 素材服务 /// 遵循 Tauri 开发规范的业务逻辑层设计 @@ -21,7 +24,15 @@ impl MaterialService { request: CreateMaterialRequest, config: &MaterialProcessingConfig, ) -> Result { - let start_time = std::time::Instant::now(); + let timer = PERFORMANCE_MONITOR.start_operation("import_materials"); + let start_time = Instant::now(); + + info!( + project_id = %request.project_id, + file_count = request.file_paths.len(), + "开始导入素材文件" + ); + let mut result = MaterialImportResult { total_files: request.file_paths.len() as u32, processed_files: 0, @@ -33,16 +44,29 @@ impl MaterialService { }; for file_path in &request.file_paths { + debug!(file_path = %file_path, "处理文件"); + match Self::process_single_file(repository, &request.project_id, file_path, config) { Ok(Some(material)) => { + info!( + file_path = %file_path, + material_id = %material.id, + "文件处理成功" + ); result.created_materials.push(material); result.processed_files += 1; } Ok(None) => { // 文件被跳过(重复) + warn!(file_path = %file_path, "文件被跳过(重复)"); result.skipped_files += 1; } Err(e) => { + error!( + file_path = %file_path, + error = %e, + "文件处理失败" + ); result.failed_files += 1; result.errors.push(format!("处理文件 {} 失败: {}", file_path, e)); } @@ -50,6 +74,21 @@ impl MaterialService { } result.processing_time = start_time.elapsed().as_secs_f64(); + + let success = result.failed_files == 0; + timer.finish(success); + + info!( + processed_files = result.processed_files, + skipped_files = result.skipped_files, + failed_files = result.failed_files, + processing_time = result.processing_time, + "素材导入完成" + ); + + PERFORMANCE_MONITOR.record_metric("import_files_per_second", + result.total_files as f64 / result.processing_time); + Ok(result) } @@ -60,12 +99,13 @@ impl MaterialService { file_path: &str, config: &MaterialProcessingConfig, ) -> Result> { + let _timer = PERFORMANCE_MONITOR.start_operation("process_single_file"); + + // 验证文件路径 + error_utils::validate_file_path(file_path) + .map_err(|e| anyhow!("文件验证失败: {}", e))?; + let path = Path::new(file_path); - - // 检查文件是否存在 - if !path.exists() { - return Err(anyhow!("文件不存在: {}", file_path)); - } // 获取文件信息 let metadata = fs::metadata(path)?; diff --git a/apps/desktop/src-tauri/src/infrastructure/ffmpeg.rs b/apps/desktop/src-tauri/src/infrastructure/ffmpeg.rs index 09e2586..d830ff2 100644 --- a/apps/desktop/src-tauri/src/infrastructure/ffmpeg.rs +++ b/apps/desktop/src-tauri/src/infrastructure/ffmpeg.rs @@ -296,7 +296,7 @@ impl FFmpegService { } /// 替代的场景检测方法 - fn detect_scenes_alternative(file_path: &str, threshold: f64) -> Result> { + fn detect_scenes_alternative(file_path: &str, _threshold: f64) -> Result> { // 使用 ffprobe 分析视频帧信息 let output = Command::new("ffprobe") .args([ diff --git a/apps/desktop/src-tauri/src/infrastructure/logging.rs b/apps/desktop/src-tauri/src/infrastructure/logging.rs new file mode 100644 index 0000000..12fd795 --- /dev/null +++ b/apps/desktop/src-tauri/src/infrastructure/logging.rs @@ -0,0 +1,336 @@ +use std::path::PathBuf; +use tracing::{info, warn, error, debug, Level}; +use tracing_subscriber::{ + fmt::{self, time::ChronoUtc}, + layer::SubscriberExt, + util::SubscriberInitExt, + EnvFilter, +}; +use std::fs; + +/// 日志配置 +#[derive(Debug, Clone)] +pub struct LogConfig { + pub level: Level, + pub log_to_file: bool, + pub log_file_path: Option, + pub max_file_size: u64, // MB + pub max_files: usize, + pub include_timestamps: bool, + pub include_thread_ids: bool, + pub include_targets: bool, +} + +impl Default for LogConfig { + fn default() -> Self { + Self { + level: Level::INFO, + log_to_file: true, + log_file_path: None, + max_file_size: 10, // 10MB + max_files: 5, + include_timestamps: true, + include_thread_ids: false, + include_targets: true, + } + } +} + +/// 初始化日志系统 +pub fn init_logging(config: LogConfig) -> Result<(), Box> { + let env_filter = EnvFilter::from_default_env() + .add_directive(format!("mixvideo_desktop={}", config.level).parse()?); + + let fmt_layer = fmt::layer() + .with_timer(ChronoUtc::rfc_3339()) + .with_target(config.include_targets) + .with_thread_ids(config.include_thread_ids) + .with_ansi(false); // 禁用 ANSI 颜色代码,适合文件输出 + + if config.log_to_file { + let log_file_path = config.log_file_path.clone().unwrap_or_else(|| { + get_default_log_path() + }); + + // 确保日志目录存在 + if let Some(parent) = log_file_path.parent() { + fs::create_dir_all(parent)?; + } + + // 清理旧日志文件 + cleanup_old_logs(&log_file_path, config.max_files)?; + + let file_appender = tracing_appender::rolling::daily( + log_file_path.parent().unwrap(), + log_file_path.file_name().unwrap() + ); + + tracing_subscriber::registry() + .with(env_filter) + .with(fmt_layer.with_writer(file_appender)) + .init(); + } else { + tracing_subscriber::registry() + .with(env_filter) + .with(fmt_layer) + .init(); + } + + info!("日志系统初始化完成"); + info!("日志级别: {:?}", config.level); + if config.log_to_file { + info!("日志文件路径: {:?}", config.log_file_path); + } + + Ok(()) +} + +/// 获取默认日志文件路径 +fn get_default_log_path() -> PathBuf { + let app_data_dir = dirs::data_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("mixvideo"); + + app_data_dir.join("logs").join("app.log") +} + +/// 清理旧日志文件 +fn cleanup_old_logs(log_path: &PathBuf, max_files: usize) -> Result<(), Box> { + if let Some(log_dir) = log_path.parent() { + if log_dir.exists() { + let mut log_files: Vec<_> = fs::read_dir(log_dir)? + .filter_map(|entry| { + let entry = entry.ok()?; + let path = entry.path(); + if path.is_file() && path.extension().map_or(false, |ext| ext == "log") { + Some((path, entry.metadata().ok()?.modified().ok()?)) + } else { + None + } + }) + .collect(); + + // 按修改时间排序,最新的在前 + log_files.sort_by(|a, b| b.1.cmp(&a.1)); + + // 删除超出限制的文件 + for (path, _) in log_files.iter().skip(max_files) { + if let Err(e) = fs::remove_file(path) { + warn!("删除旧日志文件失败: {:?} - {}", path, e); + } + } + } + } + Ok(()) +} + +/// 日志宏扩展 +#[macro_export] +macro_rules! log_operation { + ($level:ident, $operation:expr, $($arg:tt)*) => { + tracing::$level!( + operation = $operation, + $($arg)* + ); + }; +} + +#[macro_export] +macro_rules! log_performance { + ($operation:expr, $duration:expr, $success:expr) => { + if $success { + tracing::info!( + operation = $operation, + duration_ms = $duration.as_millis(), + status = "success", + "操作完成" + ); + } else { + tracing::warn!( + operation = $operation, + duration_ms = $duration.as_millis(), + status = "failed", + "操作失败" + ); + } + }; +} + +#[macro_export] +macro_rules! log_error_with_context { + ($error:expr, $context:expr) => { + tracing::error!( + error = %$error, + context = $context, + "错误发生" + ); + }; + ($error:expr, $context:expr, $($field:tt)*) => { + tracing::error!( + error = %$error, + context = $context, + $($field)*, + "错误发生" + ); + }; +} + +/// 结构化日志记录器 +pub struct StructuredLogger; + +impl StructuredLogger { + /// 记录用户操作 + pub fn log_user_action(action: &str, user_id: Option<&str>, details: Option<&str>) { + info!( + action = action, + user_id = user_id.unwrap_or("anonymous"), + details = details.unwrap_or(""), + "用户操作" + ); + } + + /// 记录系统事件 + pub fn log_system_event(event: &str, component: &str, details: Option<&str>) { + info!( + event = event, + component = component, + details = details.unwrap_or(""), + "系统事件" + ); + } + + /// 记录性能指标 + pub fn log_performance_metric(metric_name: &str, value: f64, unit: &str) { + info!( + metric = metric_name, + value = value, + unit = unit, + "性能指标" + ); + } + + /// 记录错误详情 + pub fn log_error_details( + error: &dyn std::error::Error, + operation: &str, + file_path: Option<&str>, + additional_context: Option<&str>, + ) { + error!( + error = %error, + operation = operation, + file_path = file_path.unwrap_or(""), + context = additional_context.unwrap_or(""), + "详细错误信息" + ); + } + + /// 记录安全事件 + pub fn log_security_event(event_type: &str, severity: &str, details: &str) { + warn!( + event_type = event_type, + severity = severity, + details = details, + "安全事件" + ); + } + + /// 记录数据库操作 + pub fn log_database_operation(operation: &str, table: &str, duration: std::time::Duration, success: bool) { + if success { + debug!( + operation = operation, + table = table, + duration_ms = duration.as_millis(), + "数据库操作成功" + ); + } else { + warn!( + operation = operation, + table = table, + duration_ms = duration.as_millis(), + "数据库操作失败" + ); + } + } + + /// 记录文件操作 + pub fn log_file_operation(operation: &str, file_path: &str, file_size: Option, success: bool) { + if success { + info!( + operation = operation, + file_path = file_path, + file_size = file_size.unwrap_or(0), + "文件操作成功" + ); + } else { + error!( + operation = operation, + file_path = file_path, + "文件操作失败" + ); + } + } +} + +/// 日志级别辅助函数 +pub fn set_log_level(_level: Level) { + // 注意:tracing 不支持运行时动态修改日志级别 + // 这里提供接口,实际实现可能需要重新初始化 + warn!("动态修改日志级别功能需要重新初始化日志系统"); +} + +/// 获取当前日志配置 +pub fn get_log_config() -> LogConfig { + // 返回当前配置,实际实现中应该从配置文件或环境变量读取 + LogConfig::default() +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn test_log_config_default() { + let config = LogConfig::default(); + assert_eq!(config.level, Level::INFO); + assert!(config.log_to_file); + assert_eq!(config.max_file_size, 10); + assert_eq!(config.max_files, 5); + } + + #[test] + fn test_get_default_log_path() { + let path = get_default_log_path(); + assert!(path.to_string_lossy().contains("mixvideo")); + assert!(path.to_string_lossy().contains("logs")); + assert!(path.to_string_lossy().contains("app.log")); + } + + #[test] + fn test_cleanup_old_logs() { + let temp_dir = tempdir().unwrap(); + let log_path = temp_dir.path().join("test.log"); + + // 创建一些测试日志文件 + for i in 0..10 { + let file_path = temp_dir.path().join(format!("test_{}.log", i)); + std::fs::write(&file_path, "test log content").unwrap(); + } + + let result = cleanup_old_logs(&log_path, 5); + assert!(result.is_ok()); + + // 检查文件数量 + let remaining_files = std::fs::read_dir(temp_dir.path()) + .unwrap() + .filter(|entry| { + entry.as_ref().unwrap().path().extension() + .map_or(false, |ext| ext == "log") + }) + .count(); + + assert!(remaining_files <= 5); + } +} diff --git a/apps/desktop/src-tauri/src/infrastructure/mod.rs b/apps/desktop/src-tauri/src/infrastructure/mod.rs index 4903e63..c6faab5 100644 --- a/apps/desktop/src-tauri/src/infrastructure/mod.rs +++ b/apps/desktop/src-tauri/src/infrastructure/mod.rs @@ -5,3 +5,5 @@ pub mod file_system; pub mod performance; pub mod event_bus; pub mod ffmpeg; +pub mod monitoring; +pub mod logging; diff --git a/apps/desktop/src-tauri/src/infrastructure/monitoring.rs b/apps/desktop/src-tauri/src/infrastructure/monitoring.rs new file mode 100644 index 0000000..f5b41f5 --- /dev/null +++ b/apps/desktop/src-tauri/src/infrastructure/monitoring.rs @@ -0,0 +1,233 @@ +use std::collections::HashMap; +use std::time::{Duration, Instant}; +use serde::{Deserialize, Serialize}; +use std::sync::{Arc, Mutex}; + +/// 性能指标数据 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PerformanceMetric { + pub name: String, + pub current_value: f64, + pub average_value: f64, + pub max_value: f64, + pub min_value: f64, + pub sample_count: u64, + pub total_time: Duration, +} + +/// 性能快照 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PerformanceSnapshot { + pub timestamp: u64, + pub operation: String, + pub duration: Duration, + pub memory_usage: u64, + pub success: bool, +} + +/// 性能报告 +#[derive(Debug, Serialize, Deserialize)] +pub struct PerformanceReport { + pub total_operations: u64, + pub average_response_time: Duration, + pub slowest_operations: Vec, + pub memory_usage_trend: Vec, + pub error_rate: f64, +} + +/// 性能监控器 +pub struct PerformanceMonitor { + metrics: Arc>>, + snapshots: Arc>>, + max_snapshots: usize, +} + +impl PerformanceMonitor { + pub fn new() -> Self { + Self { + metrics: Arc::new(Mutex::new(HashMap::new())), + snapshots: Arc::new(Mutex::new(Vec::new())), + max_snapshots: 1000, // 保留最近1000条记录 + } + } + + /// 开始监控操作 + pub fn start_operation(&self, operation: &str) -> OperationTimer { + OperationTimer::new(operation.to_string(), Arc::clone(&self.metrics), Arc::clone(&self.snapshots)) + } + + /// 记录性能指标 + pub fn record_metric(&self, name: &str, value: f64) { + let mut metrics = self.metrics.lock().unwrap(); + let metric = metrics.entry(name.to_string()).or_insert_with(|| { + PerformanceMetric { + name: name.to_string(), + current_value: value, + average_value: value, + max_value: value, + min_value: value, + sample_count: 0, + total_time: Duration::from_secs(0), + } + }); + + metric.current_value = value; + metric.max_value = metric.max_value.max(value); + metric.min_value = metric.min_value.min(value); + metric.sample_count += 1; + metric.average_value = (metric.average_value * (metric.sample_count - 1) as f64 + value) + / metric.sample_count as f64; + } + + /// 获取性能指标 + pub fn get_metric(&self, name: &str) -> Option { + let metrics = self.metrics.lock().unwrap(); + metrics.get(name).cloned() + } + + /// 获取所有性能指标 + pub fn get_all_metrics(&self) -> HashMap { + let metrics = self.metrics.lock().unwrap(); + metrics.clone() + } + + /// 生成性能报告 + pub fn generate_report(&self) -> PerformanceReport { + let snapshots = self.snapshots.lock().unwrap(); + let total_operations = snapshots.len() as u64; + + let average_response_time = if total_operations > 0 { + let total_duration: Duration = snapshots.iter().map(|s| s.duration).sum(); + total_duration / total_operations as u32 + } else { + Duration::from_secs(0) + }; + + let mut sorted_snapshots = snapshots.clone(); + sorted_snapshots.sort_by(|a, b| b.duration.cmp(&a.duration)); + let slowest_operations = sorted_snapshots.into_iter().take(10).collect(); + + let memory_usage_trend: Vec = snapshots.iter().map(|s| s.memory_usage).collect(); + + let error_count = snapshots.iter().filter(|s| !s.success).count(); + let error_rate = if total_operations > 0 { + error_count as f64 / total_operations as f64 + } else { + 0.0 + }; + + PerformanceReport { + total_operations, + average_response_time, + slowest_operations, + memory_usage_trend, + error_rate, + } + } + + /// 清理旧的快照数据 + pub fn cleanup_old_snapshots(&self) { + let mut snapshots = self.snapshots.lock().unwrap(); + if snapshots.len() > self.max_snapshots { + let excess = snapshots.len() - self.max_snapshots; + snapshots.drain(0..excess); + } + } +} + +impl Default for PerformanceMonitor { + fn default() -> Self { + Self::new() + } +} + +/// 操作计时器 +pub struct OperationTimer { + operation: String, + start_time: Instant, + metrics: Arc>>, + snapshots: Arc>>, +} + +impl OperationTimer { + fn new( + operation: String, + metrics: Arc>>, + snapshots: Arc>>, + ) -> Self { + Self { + operation, + start_time: Instant::now(), + metrics, + snapshots, + } + } + + /// 完成操作并记录结果 + pub fn finish(self, success: bool) { + let duration = self.start_time.elapsed(); + let memory_usage = self.get_memory_usage(); + + // 记录到指标 + { + let mut metrics = self.metrics.lock().unwrap(); + let metric = metrics.entry(self.operation.clone()).or_insert_with(|| { + PerformanceMetric { + name: self.operation.clone(), + current_value: duration.as_millis() as f64, + average_value: duration.as_millis() as f64, + max_value: duration.as_millis() as f64, + min_value: duration.as_millis() as f64, + sample_count: 0, + total_time: Duration::from_secs(0), + } + }); + + let duration_ms = duration.as_millis() as f64; + metric.current_value = duration_ms; + metric.max_value = metric.max_value.max(duration_ms); + metric.min_value = metric.min_value.min(duration_ms); + metric.sample_count += 1; + metric.total_time += duration; + metric.average_value = metric.total_time.as_millis() as f64 / metric.sample_count as f64; + } + + // 记录快照 + { + let mut snapshots = self.snapshots.lock().unwrap(); + snapshots.push(PerformanceSnapshot { + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + operation: self.operation, + duration, + memory_usage, + success, + }); + } + } + + fn get_memory_usage(&self) -> u64 { + // 简化实现,实际应用中可以使用系统API获取真实内存使用 + // 这里返回一个模拟值 + 0 + } +} + +// 全局性能监控器实例 +lazy_static::lazy_static! { + pub static ref PERFORMANCE_MONITOR: PerformanceMonitor = PerformanceMonitor::new(); +} + +/// 性能监控宏 +#[macro_export] +macro_rules! monitor_performance { + ($operation:expr, $code:block) => {{ + let timer = crate::infrastructure::monitoring::PERFORMANCE_MONITOR.start_operation($operation); + let result = $code; + let success = result.is_ok(); + timer.finish(success); + result + }}; +} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 78ba23d..766713d 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -1,3 +1,6 @@ +// 导入宏 +extern crate lazy_static; + // 四层架构模块定义 pub mod infrastructure; pub mod data; @@ -15,6 +18,8 @@ mod tests; use app_state::AppState; use presentation::commands; use tauri::Manager; +use infrastructure::logging; +use tracing::info; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { @@ -37,6 +42,8 @@ pub fn run() { commands::system_commands::get_directory_name, commands::system_commands::get_database_info, commands::system_commands::get_performance_report, + commands::system_commands::cleanup_performance_data, + commands::system_commands::record_performance_metric, commands::system_commands::cleanup_invalid_projects, commands::material_commands::import_materials, commands::material_commands::get_project_materials, @@ -62,6 +69,14 @@ pub fn run() { commands::material_commands::test_video_split ]) .setup(|app| { + // 初始化日志系统 + let log_config = logging::LogConfig::default(); + if let Err(e) = logging::init_logging(log_config) { + eprintln!("日志系统初始化失败: {}", e); + } + + info!("MixVideo Desktop 应用启动"); + // 初始化应用状态 let app_handle = app.handle(); let state: tauri::State = app_handle.state(); diff --git a/apps/desktop/src-tauri/src/presentation/commands/system_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/system_commands.rs index d93f0d5..b44e6fa 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/system_commands.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/system_commands.rs @@ -1,5 +1,7 @@ use tauri::{command, AppHandle, Manager}; use serde::{Deserialize, Serialize}; +use crate::infrastructure::monitoring::{PERFORMANCE_MONITOR, PerformanceReport}; +use tracing::info; /// 应用信息结构 #[derive(Debug, Serialize, Deserialize)] @@ -74,16 +76,7 @@ pub async fn get_database_info() -> Result { Ok(Database::get_database_path_info()) } -/// 获取性能监控报告命令 -/// 遵循 Tauri 开发规范的性能监控要求 -#[command] -pub async fn get_performance_report(app: AppHandle) -> Result { - let state = app.state::(); - let monitor = state.performance_monitor.lock().unwrap(); - let report = monitor.check_performance_standards(); - serde_json::to_string(&report).map_err(|e| e.to_string()) -} /// 清理无效项目记录命令 /// 删除路径不存在的项目记录,解决 UNIQUE 约束冲突 @@ -125,3 +118,40 @@ pub async fn cleanup_invalid_projects(app: AppHandle) -> Result Err("项目仓库未初始化".to_string()) } } + +/// 获取性能报告命令 +#[command] +pub async fn get_performance_report() -> Result { + info!("生成性能报告"); + + let report = PERFORMANCE_MONITOR.generate_report(); + + info!( + total_operations = report.total_operations, + average_response_time_ms = report.average_response_time.as_millis(), + error_rate = report.error_rate, + "性能报告生成完成" + ); + + Ok(report) +} + +/// 清理性能监控数据命令 +#[command] +pub async fn cleanup_performance_data() -> Result { + info!("清理性能监控数据"); + + PERFORMANCE_MONITOR.cleanup_old_snapshots(); + + Ok("性能监控数据清理完成".to_string()) +} + +/// 记录自定义性能指标命令 +#[command] +pub async fn record_performance_metric(name: String, value: f64) -> Result<(), String> { + info!(metric_name = %name, value = value, "记录自定义性能指标"); + + PERFORMANCE_MONITOR.record_metric(&name, value); + + Ok(()) +}