feat: 实现Tauri开发规范立即改进项目
This commit is contained in:
3
0.1.1.md
3
0.1.1.md
@@ -32,3 +32,6 @@
|
||||
### 优化
|
||||
提交代码 然后优化:
|
||||
feature: 在导入时启动异步 后台处理 处理(更好的用户体验)
|
||||
feature: 编写单元测试和集成测试
|
||||
feature: 优化异步操作
|
||||
feature: 完善配置管理系统
|
||||
117
Cargo.lock
generated
117
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
296
apps/desktop/src-tauri/src/business/errors.rs
Normal file
296
apps/desktop/src-tauri/src/business/errors.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod services;
|
||||
pub mod errors;
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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([
|
||||
|
||||
336
apps/desktop/src-tauri/src/infrastructure/logging.rs
Normal file
336
apps/desktop/src-tauri/src/infrastructure/logging.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -5,3 +5,5 @@ pub mod file_system;
|
||||
pub mod performance;
|
||||
pub mod event_bus;
|
||||
pub mod ffmpeg;
|
||||
pub mod monitoring;
|
||||
pub mod logging;
|
||||
|
||||
233
apps/desktop/src-tauri/src/infrastructure/monitoring.rs
Normal file
233
apps/desktop/src-tauri/src/infrastructure/monitoring.rs
Normal 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
|
||||
}};
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user