feat: 实现Tauri开发规范立即改进项目

This commit is contained in:
imeepos
2025-07-13 22:46:20 +08:00
parent c2e7b2c70f
commit 39b517dcd8
12 changed files with 1091 additions and 20 deletions

View File

@@ -32,3 +32,6 @@
### 优化
提交代码 然后优化:
feature: 在导入时启动异步 后台处理 处理(更好的用户体验)
feature: 编写单元测试和集成测试
feature: 优化异步操作
feature: 完善配置管理系统

117
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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<T> = Result<T, AppError>;
pub type MaterialResult<T> = Result<T, MaterialError>;
pub type ProjectResult<T> = Result<T, ProjectError>;
pub type DatabaseResult<T> = Result<T, DatabaseError>;
pub type SystemResult<T> = Result<T, SystemError>;
/// 错误转换实现
impl From<rusqlite::Error> for DatabaseError {
fn from(err: rusqlite::Error) -> Self {
DatabaseError::QueryFailed {
query: "unknown".to_string(),
message: err.to_string(),
}
}
}
impl From<std::io::Error> for AppError {
fn from(err: std::io::Error) -> Self {
AppError::FileSystem {
message: err.to_string(),
}
}
}
impl From<serde_json::Error> for AppError {
fn from(err: serde_json::Error) -> Self {
AppError::Configuration {
message: format!("JSON 解析错误: {}", err),
}
}
}
impl From<MaterialError> for AppError {
fn from(err: MaterialError) -> Self {
AppError::Internal {
message: err.to_string(),
}
}
}
impl From<ProjectError> for AppError {
fn from(err: ProjectError) -> Self {
AppError::Internal {
message: err.to_string(),
}
}
}
impl From<DatabaseError> for AppError {
fn from(err: DatabaseError) -> Self {
AppError::Database {
message: err.to_string(),
}
}
}
impl From<SystemError> 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<E: std::error::Error>(err: E, context: &str) -> AppError {
error!("错误发生在 {}: {}", context, err);
AppError::Internal {
message: format!("{}: {}", context, err),
}
}
/// 记录警告并返回默认值
pub fn log_warning_and_default<T: 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(())
}
}

View File

@@ -1 +1,2 @@
pub mod services;
pub mod errors;

View File

@@ -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<MaterialImportResult> {
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<Option<Material>> {
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)?;

View File

@@ -296,7 +296,7 @@ impl FFmpegService {
}
/// 替代的场景检测方法
fn detect_scenes_alternative(file_path: &str, threshold: f64) -> Result<Vec<f64>> {
fn detect_scenes_alternative(file_path: &str, _threshold: f64) -> Result<Vec<f64>> {
// 使用 ffprobe 分析视频帧信息
let output = Command::new("ffprobe")
.args([

View File

@@ -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<PathBuf>,
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<u64>, 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);
}
}

View File

@@ -5,3 +5,5 @@ pub mod file_system;
pub mod performance;
pub mod event_bus;
pub mod ffmpeg;
pub mod monitoring;
pub mod logging;

View File

@@ -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<PerformanceSnapshot>,
pub memory_usage_trend: Vec<u64>,
pub error_rate: f64,
}
/// 性能监控器
pub struct PerformanceMonitor {
metrics: Arc<Mutex<HashMap<String, PerformanceMetric>>>,
snapshots: Arc<Mutex<Vec<PerformanceSnapshot>>>,
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<PerformanceMetric> {
let metrics = self.metrics.lock().unwrap();
metrics.get(name).cloned()
}
/// 获取所有性能指标
pub fn get_all_metrics(&self) -> HashMap<String, PerformanceMetric> {
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<u64> = 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<Mutex<HashMap<String, PerformanceMetric>>>,
snapshots: Arc<Mutex<Vec<PerformanceSnapshot>>>,
}
impl OperationTimer {
fn new(
operation: String,
metrics: Arc<Mutex<HashMap<String, PerformanceMetric>>>,
snapshots: Arc<Mutex<Vec<PerformanceSnapshot>>>,
) -> 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
}};
}

View File

@@ -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<AppState> = app_handle.state();

View File

@@ -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<String, String> {
Ok(Database::get_database_path_info())
}
/// 获取性能监控报告命令
/// 遵循 Tauri 开发规范的性能监控要求
#[command]
pub async fn get_performance_report(app: AppHandle) -> Result<String, String> {
let state = app.state::<crate::app_state::AppState>();
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<String, String>
Err("项目仓库未初始化".to_string())
}
}
/// 获取性能报告命令
#[command]
pub async fn get_performance_report() -> Result<PerformanceReport, String> {
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<String, String> {
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(())
}