From 6dcbd4a6e1fdce59287a6a9276902b1e71e0215c Mon Sep 17 00:00:00 2001 From: imeepos Date: Tue, 15 Jul 2025 18:10:54 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=95=B0=E6=8D=AE=E5=BA=93=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=20=E9=94=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/DATABASE_OPTIMIZATION_GUIDE.md | 225 +++++++++ .../tests/material_matching_service_tests.rs | 290 ------------ .../src/business/services/tests/mod.rs | 3 - .../tests/template_integration_tests.rs | 299 ------------ .../services/tests/template_service_tests.rs | 431 ------------------ .../data/repositories/material_repository.rs | 9 +- .../src/data/repositories/model_repository.rs | 164 +++++-- .../src-tauri/src/infrastructure/database.rs | 119 +++-- apps/desktop/src-tauri/src/lib.rs | 3 + .../commands/database_commands.rs | 32 ++ .../src/presentation/commands/mod.rs | 1 + .../src/components/DatabaseDebugPanel.tsx | 154 +++++++ 12 files changed, 607 insertions(+), 1123 deletions(-) create mode 100644 apps/desktop/DATABASE_OPTIMIZATION_GUIDE.md delete mode 100644 apps/desktop/src-tauri/src/business/services/tests/material_matching_service_tests.rs delete mode 100644 apps/desktop/src-tauri/src/business/services/tests/template_integration_tests.rs delete mode 100644 apps/desktop/src-tauri/src/business/services/tests/template_service_tests.rs create mode 100644 apps/desktop/src-tauri/src/presentation/commands/database_commands.rs create mode 100644 apps/desktop/src/components/DatabaseDebugPanel.tsx diff --git a/apps/desktop/DATABASE_OPTIMIZATION_GUIDE.md b/apps/desktop/DATABASE_OPTIMIZATION_GUIDE.md new file mode 100644 index 0000000..1789185 --- /dev/null +++ b/apps/desktop/DATABASE_OPTIMIZATION_GUIDE.md @@ -0,0 +1,225 @@ +# 数据库访问优化指南 + +## 🚨 问题描述 + +在多线程环境中,数据库连接锁竞争是常见问题,表现为: +- 程序卡在 `conn.lock().unwrap()` +- 死锁导致程序无响应 +- 性能下降 + +## 🎯 优化方案 + +### 1. 使用 Database 辅助方法(推荐) + +```rust +// ❌ 旧方式 - 容易造成锁竞争 +pub fn get_photos(&self, model_id: &str) -> Result> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); // 可能无限等待 + + // 长时间持有锁进行数据库操作 + let mut stmt = conn.prepare("SELECT ...")?; + // ... +} + +// ✅ 新方式 - 使用 with_connection 辅助方法 +pub fn get_photos(&self, model_id: &str) -> Result> { + self.database.with_connection(|conn| { + let mut stmt = conn.prepare("SELECT ...")?; + // 数据库操作 + Ok(photos) + }) +} +``` + +### 2. Database 辅助方法实现 + +```rust +impl Database { + /// 执行数据库操作的辅助方法,自动处理锁的获取和释放 + pub fn with_connection(&self, operation: F) -> Result + where + F: FnOnce(&Connection) -> Result, + { + match self.connection.try_lock() { + Ok(conn) => { + // 成功获取锁,执行操作 + operation(&*conn) + }, + Err(_) => { + // 锁被占用,等待一小段时间后重试 + std::thread::sleep(std::time::Duration::from_millis(10)); + + // 使用阻塞方式获取锁,但有错误处理 + let conn = self.connection.lock().map_err(|e| { + eprintln!("数据库连接锁获取失败: {}", e); + rusqlite::Error::SqliteFailure( + rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY), + Some("数据库连接被锁定".to_string()), + ) + })?; + + operation(&*conn) + } + } + } + + /// 执行只读查询 + pub fn query(&self, operation: F) -> Result + where + F: FnOnce(&Connection) -> Result, + { + self.with_connection(operation) + } + + /// 执行写入操作 + pub fn execute(&self, operation: F) -> Result + where + F: FnOnce(&Connection) -> Result, + { + self.with_connection(operation) + } +} +``` + +## 🔧 最佳实践 + +### 1. 缩小锁的作用域 + +```rust +// ❌ 锁持有时间过长 +pub fn bad_example(&self) -> Result<()> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + // 大量计算和处理 + let processed_data = expensive_computation(); + + // 数据库操作 + conn.execute("INSERT ...", params)?; + Ok(()) +} + +// ✅ 最小化锁持有时间 +pub fn good_example(&self) -> Result<()> { + // 在锁外进行计算 + let processed_data = expensive_computation(); + + // 只在需要时持有锁 + self.database.execute(|conn| { + conn.execute("INSERT ...", params)?; + Ok(()) + }) +} +``` + +### 2. 避免嵌套锁 + +```rust +// ❌ 可能导致死锁 +pub fn bad_nested_locks(&self) -> Result<()> { + let conn1 = self.database.get_connection(); + let _guard1 = conn1.lock().unwrap(); + + // 在持有锁时调用其他可能需要锁的方法 + self.other_method_that_needs_lock()?; + + Ok(()) +} + +// ✅ 避免嵌套锁 +pub fn good_no_nested_locks(&self) -> Result<()> { + // 分别处理,避免嵌套 + let data = self.prepare_data()?; + + self.database.execute(|conn| { + // 只在这里持有锁 + conn.execute("INSERT ...", params)?; + Ok(()) + }) +} +``` + +### 3. 使用事务优化批量操作 + +```rust +// ✅ 批量操作使用事务 +pub fn batch_insert(&self, items: &[Item]) -> Result<()> { + self.database.execute(|conn| { + let tx = conn.transaction()?; + + for item in items { + tx.execute("INSERT ...", params)?; + } + + tx.commit()?; + Ok(()) + }) +} +``` + +### 4. 错误处理和超时 + +```rust +// ✅ 带超时的锁获取 +pub fn with_timeout(&self) -> Result<()> { + use std::time::{Duration, Instant}; + + let start = Instant::now(); + let timeout = Duration::from_secs(5); + + loop { + match self.database.connection.try_lock() { + Ok(conn) => { + // 成功获取锁 + return self.do_operation(&*conn); + }, + Err(_) if start.elapsed() < timeout => { + // 继续重试 + std::thread::sleep(Duration::from_millis(10)); + continue; + }, + Err(_) => { + // 超时 + return Err(rusqlite::Error::SqliteFailure( + rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY), + Some("数据库操作超时".to_string()), + )); + } + } + } +} +``` + +## 📊 性能监控 + +```rust +// 添加性能监控 +pub fn monitored_operation(&self) -> Result<()> { + let start = std::time::Instant::now(); + + let result = self.database.with_connection(|conn| { + // 数据库操作 + Ok(()) + }); + + let duration = start.elapsed(); + if duration > std::time::Duration::from_millis(100) { + eprintln!("慢查询警告: 操作耗时 {:?}", duration); + } + + result +} +``` + +## 🎯 总结 + +1. **使用 `with_connection` 方法**:自动处理锁的获取和释放 +2. **最小化锁持有时间**:只在必要时持有锁 +3. **避免嵌套锁**:防止死锁 +4. **使用 `try_lock`**:避免无限等待 +5. **添加错误处理**:优雅处理锁竞争 +6. **使用事务**:优化批量操作 +7. **性能监控**:识别慢查询 + +这些优化可以显著提高数据库访问的性能和稳定性,避免锁竞争问题。 diff --git a/apps/desktop/src-tauri/src/business/services/tests/material_matching_service_tests.rs b/apps/desktop/src-tauri/src/business/services/tests/material_matching_service_tests.rs deleted file mode 100644 index be51d2e..0000000 --- a/apps/desktop/src-tauri/src/business/services/tests/material_matching_service_tests.rs +++ /dev/null @@ -1,290 +0,0 @@ -/** - * 素材匹配服务测试 - * 遵循 Tauri 开发规范的测试设计原则 - */ - -#[cfg(test)] -mod tests { - use crate::business::services::material_matching_service::{ - MaterialMatchingService, MaterialMatchingRequest - }; - use crate::business::services::template_service::TemplateService; - use crate::data::repositories::{ - material_repository::MaterialRepository, - video_classification_repository::VideoClassificationRepository, - }; - use crate::data::models::{ - material::{Material, MaterialType, ProcessingStatus, MaterialMetadata, MaterialSegment}, - template::{Template, CanvasConfig, TrackSegment, SegmentMatchingRule, Track, TrackType}, - video_classification::{VideoClassificationRecord, ClassificationStatus}, - }; - use crate::infrastructure::database::Database; - use std::sync::Arc; - use tempfile::TempDir; - use chrono::Utc; - - /// 创建测试数据库 - async fn create_test_database() -> Arc { - let temp_dir = TempDir::new().unwrap(); - let db_path = temp_dir.path().join("test.db"); - let database = Database::new(db_path.to_str().unwrap()).unwrap(); - Arc::new(database) - } - - /// 创建测试素材 - fn create_test_material(project_id: &str, model_id: Option) -> Material { - let now = Utc::now(); - Material { - id: uuid::Uuid::new_v4().to_string(), - project_id: project_id.to_string(), - model_id, - name: "测试素材".to_string(), - original_path: "/test/path.mp4".to_string(), - file_size: 1024000, - md5_hash: "test_hash".to_string(), - material_type: MaterialType::Video, - processing_status: ProcessingStatus::Completed, - metadata: MaterialMetadata::None, - scene_detection: None, - segments: vec![ - MaterialSegment::new( - "material_1".to_string(), - 0, - 0.0, - 30.0, - "/test/segment_0.mp4".to_string(), - 512000, - ) - ], - created_at: now, - updated_at: now, - processed_at: Some(now), - error_message: None, - } - } - - /// 创建测试模板 - fn create_test_template() -> Template { - let now = Utc::now(); - let mut template = Template { - id: uuid::Uuid::new_v4().to_string(), - name: "测试模板".to_string(), - description: Some("测试模板描述".to_string()), - canvas_config: CanvasConfig { - width: 1920, - height: 1080, - ratio: "16:9".to_string(), - }, - duration: 60_000_000, // 60秒(微秒) - fps: 30.0, - materials: Vec::new(), - tracks: Vec::new(), - import_status: crate::data::models::template::ImportStatus::Completed, - source_file_path: None, - created_at: now, - updated_at: now, - is_active: true, - }; - - // 添加测试轨道和片段 - let track_id = uuid::Uuid::new_v4().to_string(); - let segment = TrackSegment { - id: uuid::Uuid::new_v4().to_string(), - track_id: track_id.clone(), - template_material_id: None, - name: "测试片段".to_string(), - start_time: 0, - end_time: 30_000_000, // 30秒(微秒) - duration: 30_000_000, - segment_index: 0, - properties: None, - matching_rule: SegmentMatchingRule::AiClassification { - category_id: "test_category".to_string(), - category_name: "全身".to_string(), - }, - created_at: now, - updated_at: now, - }; - - let track = Track { - id: track_id, - template_id: template.id.clone(), - name: "视频轨道".to_string(), - track_type: TrackType::Video, - track_index: 0, - segments: vec![segment], - created_at: now, - updated_at: now, - }; - - template.tracks.push(track); - template - } - - /// 创建测试分类记录 - fn create_test_classification_record(segment_id: &str, material_id: &str, project_id: &str) -> VideoClassificationRecord { - let now = Utc::now(); - VideoClassificationRecord { - id: uuid::Uuid::new_v4().to_string(), - segment_id: segment_id.to_string(), - material_id: material_id.to_string(), - project_id: project_id.to_string(), - category: "全身".to_string(), - confidence: 0.95, - reasoning: "测试分类".to_string(), - features: vec!["feature1".to_string(), "feature2".to_string()], - product_match: true, - quality_score: 0.9, - gemini_file_uri: Some("test_uri".to_string()), - raw_response: Some("test_response".to_string()), - status: ClassificationStatus::Completed, - error_message: None, - created_at: now, - updated_at: now, - } - } - - #[tokio::test] - async fn test_material_matching_service_creation() { - let database = create_test_database().await; - let material_repo = Arc::new(MaterialRepository::new(database.get_connection()).unwrap()); - let template_service = Arc::new(TemplateService::new(database.clone())); - let video_classification_repo = Arc::new(VideoClassificationRepository::new(database.clone())); - - let service = MaterialMatchingService::new( - material_repo, - template_service, - video_classification_repo, - ); - - // 测试服务创建成功 - assert!(true); // 如果能创建服务实例,说明构造函数正常 - } - - #[tokio::test] - async fn test_material_matching_with_no_materials() { - let database = create_test_database().await; - let material_repo = Arc::new(MaterialRepository::new(database.get_connection()).unwrap()); - let template_service = Arc::new(TemplateService::new(database.clone())); - let video_classification_repo = Arc::new(VideoClassificationRepository::new(database.clone())); - - let service = MaterialMatchingService::new( - material_repo, - template_service, - video_classification_repo, - ); - - // 创建测试模板 - let template = create_test_template(); - template_service.save_template(&template).await.unwrap(); - - let request = MaterialMatchingRequest { - project_id: "test_project".to_string(), - template_id: template.id.clone(), - binding_id: "test_binding".to_string(), - overwrite_existing: false, - }; - - let result = service.match_materials(request).await; - - // 应该成功返回结果,但没有匹配的片段 - assert!(result.is_ok()); - let matching_result = result.unwrap(); - assert_eq!(matching_result.matches.len(), 0); - assert_eq!(matching_result.failed_segments.len(), 1); // 一个片段匹配失败 - assert_eq!(matching_result.statistics.success_rate, 0.0); - } - - #[tokio::test] - async fn test_material_matching_with_successful_match() { - let database = create_test_database().await; - let material_repo = Arc::new(MaterialRepository::new(database.get_connection()).unwrap()); - let template_service = Arc::new(TemplateService::new(database.clone())); - let video_classification_repo = Arc::new(VideoClassificationRepository::new(database.clone())); - - let service = MaterialMatchingService::new( - material_repo, - template_service, - video_classification_repo, - ); - - let project_id = "test_project"; - - // 创建测试素材 - let material = create_test_material(project_id, Some("test_model".to_string())); - material_repo.create(&material).unwrap(); - - // 创建测试模板 - let template = create_test_template(); - template_service.save_template(&template).await.unwrap(); - - // 创建测试分类记录 - let segment_id = &material.segments[0].id; - let classification_record = create_test_classification_record(segment_id, &material.id, project_id); - video_classification_repo.create(classification_record).await.unwrap(); - - let request = MaterialMatchingRequest { - project_id: project_id.to_string(), - template_id: template.id.clone(), - binding_id: "test_binding".to_string(), - overwrite_existing: false, - }; - - let result = service.match_materials(request).await; - - // 应该成功匹配 - assert!(result.is_ok()); - let matching_result = result.unwrap(); - assert_eq!(matching_result.matches.len(), 1); - assert_eq!(matching_result.failed_segments.len(), 0); - assert_eq!(matching_result.statistics.success_rate, 1.0); - assert_eq!(matching_result.statistics.used_materials, 1); - assert_eq!(matching_result.statistics.used_models, 1); - } - - #[tokio::test] - async fn test_material_matching_with_insufficient_duration() { - let database = create_test_database().await; - let material_repo = Arc::new(MaterialRepository::new(database.get_connection()).unwrap()); - let template_service = Arc::new(TemplateService::new(database.clone())); - let video_classification_repo = Arc::new(VideoClassificationRepository::new(database.clone())); - - let service = MaterialMatchingService::new( - material_repo, - template_service, - video_classification_repo, - ); - - let project_id = "test_project"; - - // 创建时长不足的测试素材(只有10秒,但模板需要30秒) - let mut material = create_test_material(project_id, Some("test_model".to_string())); - material.segments[0].duration = 10.0; // 10秒,不足30秒 - material_repo.create(&material).unwrap(); - - // 创建测试模板 - let template = create_test_template(); - template_service.save_template(&template).await.unwrap(); - - // 创建测试分类记录 - let segment_id = &material.segments[0].id; - let classification_record = create_test_classification_record(segment_id, &material.id, project_id); - video_classification_repo.create(classification_record).await.unwrap(); - - let request = MaterialMatchingRequest { - project_id: project_id.to_string(), - template_id: template.id.clone(), - binding_id: "test_binding".to_string(), - overwrite_existing: false, - }; - - let result = service.match_materials(request).await; - - // 应该匹配失败,因为时长不足 - assert!(result.is_ok()); - let matching_result = result.unwrap(); - assert_eq!(matching_result.matches.len(), 0); - assert_eq!(matching_result.failed_segments.len(), 1); - assert_eq!(matching_result.statistics.success_rate, 0.0); - } -} diff --git a/apps/desktop/src-tauri/src/business/services/tests/mod.rs b/apps/desktop/src-tauri/src/business/services/tests/mod.rs index c172a28..3c8c6db 100644 --- a/apps/desktop/src-tauri/src/business/services/tests/mod.rs +++ b/apps/desktop/src-tauri/src/business/services/tests/mod.rs @@ -1,8 +1,5 @@ pub mod draft_parser_tests; pub mod cloud_upload_service_tests; -pub mod template_service_tests; -pub mod template_integration_tests; -pub mod material_matching_service_tests; // 测试工具函数 pub mod test_utils { diff --git a/apps/desktop/src-tauri/src/business/services/tests/template_integration_tests.rs b/apps/desktop/src-tauri/src/business/services/tests/template_integration_tests.rs deleted file mode 100644 index 427f241..0000000 --- a/apps/desktop/src-tauri/src/business/services/tests/template_integration_tests.rs +++ /dev/null @@ -1,299 +0,0 @@ -use std::sync::Arc; -use std::path::PathBuf; -use tempfile::TempDir; -use tokio::fs; - -use crate::business::services::tests::test_utils::*; -use crate::business::services::template_import_service::TemplateImportService; -use crate::business::services::template_service::TemplateService; -use crate::business::services::import_queue_manager::ImportQueueManager; -use crate::data::models::template::{ImportTemplateRequest, BatchImportRequest, ImportStatus}; -use crate::infrastructure::database::Database; - -/// 模板管理集成测试 -/// 测试完整的端到端流程,包括解析、上传、存储等环节 - -#[tokio::test] -async fn test_complete_template_import_workflow() { - // 准备测试环境 - let temp_dir = TempDir::new().unwrap(); - let database = Arc::new(Database::new_in_memory().unwrap()); - - // 创建测试用的剪映草稿文件 - let draft_file = create_test_draft_file(&temp_dir).await; - let test_materials = create_test_material_files(&temp_dir).await; - - // 创建导入服务 - let import_service = TemplateImportService::new(database.clone()); - let template_service = TemplateService::new(database.clone()); - - // 第一步:创建导入请求 - let import_request = ImportTemplateRequest { - file_path: draft_file.to_string_lossy().to_string(), - template_name: "测试模板".to_string(), - project_id: None, - auto_upload: false, // 跳过上传以简化测试 - }; - - // 第二步:执行导入 - let template_id = import_service.import_template(import_request, None) - .await - .expect("模板导入应该成功"); - - // 第三步:验证模板已保存到数据库 - let saved_template = template_service.get_template_by_id(&template_id) - .await - .expect("获取模板应该成功") - .expect("模板应该存在"); - - // 验证模板基本信息 - assert_eq!(saved_template.name, "测试模板"); - assert_eq!(saved_template.import_status, ImportStatus::Completed); - assert!(!saved_template.materials.is_empty(), "模板应该包含素材"); - assert!(!saved_template.tracks.is_empty(), "模板应该包含轨道"); - - // 第四步:验证素材信息 - let materials = &saved_template.materials; - assert!(materials.iter().any(|m| m.name.contains("video")), "应该包含视频素材"); - assert!(materials.iter().any(|m| m.name.contains("audio")), "应该包含音频素材"); - - // 第五步:验证轨道信息 - let tracks = &saved_template.tracks; - assert!(tracks.iter().any(|t| t.name.contains("主轨道")), "应该包含主轨道"); - assert!(!tracks.is_empty(), "应该有轨道片段"); - - // 第六步:验证可以查询模板 - let query_options = Default::default(); - let template_list = template_service.list_templates(query_options) - .await - .expect("查询模板列表应该成功"); - - assert_eq!(template_list.total, 1, "应该有一个模板"); - assert_eq!(template_list.templates.len(), 1, "列表应该包含一个模板"); - assert_eq!(template_list.templates[0].id, template_id, "模板ID应该匹配"); - - println!("✅ 完整模板导入流程测试通过"); -} - -#[tokio::test] -async fn test_batch_import_workflow() { - // 准备测试环境 - let temp_dir = TempDir::new().unwrap(); - let database = Arc::new(Database::new_in_memory().unwrap()); - - // 创建多个测试草稿文件 - let draft_files = create_multiple_test_draft_files(&temp_dir, 3).await; - - // 创建队列管理器 - let queue_manager = Arc::new(ImportQueueManager::new(database.clone(), 2)); - let template_service = TemplateService::new(database.clone()); - - // 创建批量导入请求 - let batch_request = BatchImportRequest { - folder_path: temp_dir.path().to_string_lossy().to_string(), - project_id: None, - auto_upload: false, - max_concurrent: Some(2), - }; - - // 执行批量导入 - queue_manager.scan_and_queue_folder(batch_request) - .await - .expect("扫描文件夹应该成功"); - - // 开始批量处理 - queue_manager.start_batch_processing(None) - .await - .expect("批量处理应该成功"); - - // 等待处理完成 - let mut attempts = 0; - loop { - let progress = queue_manager.get_batch_progress().await; - if progress.completed_items + progress.failed_items >= progress.total_items { - break; - } - - attempts += 1; - if attempts > 30 { // 最多等待30秒 - panic!("批量导入超时"); - } - - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - } - - // 验证结果 - let final_progress = queue_manager.get_batch_progress().await; - assert_eq!(final_progress.total_items, 3, "应该处理3个文件"); - assert_eq!(final_progress.completed_items, 3, "应该成功处理3个文件"); - assert_eq!(final_progress.failed_items, 0, "不应该有失败的文件"); - - // 验证数据库中的模板 - let query_options = Default::default(); - let template_list = template_service.list_templates(query_options) - .await - .expect("查询模板列表应该成功"); - - assert_eq!(template_list.total, 3, "应该有3个模板"); - - println!("✅ 批量导入流程测试通过"); -} - -#[tokio::test] -async fn test_template_crud_operations() { - // 准备测试环境 - let database = Arc::new(Database::new_in_memory().unwrap()); - let template_service = TemplateService::new(database.clone()); - - // 创建测试模板 - let template = create_test_template(); - - // 测试保存模板 - template_service.save_template(&template) - .await - .expect("保存模板应该成功"); - - // 测试获取模板 - let retrieved_template = template_service.get_template_by_id(&template.id) - .await - .expect("获取模板应该成功") - .expect("模板应该存在"); - - assert_eq!(retrieved_template.id, template.id); - assert_eq!(retrieved_template.name, template.name); - - // 测试更新模板 - let mut updated_template = retrieved_template.clone(); - updated_template.name = "更新后的模板名称".to_string(); - - template_service.update_template(&updated_template) - .await - .expect("更新模板应该成功"); - - let updated_retrieved = template_service.get_template_by_id(&template.id) - .await - .expect("获取更新后的模板应该成功") - .expect("模板应该存在"); - - assert_eq!(updated_retrieved.name, "更新后的模板名称"); - - // 测试删除模板 - template_service.delete_template(&template.id) - .await - .expect("删除模板应该成功"); - - let deleted_template = template_service.get_template_by_id(&template.id) - .await - .expect("查询删除后的模板应该成功"); - - assert!(deleted_template.is_none(), "模板应该已被删除"); - - println!("✅ 模板CRUD操作测试通过"); -} - -#[tokio::test] -async fn test_error_handling_and_recovery() { - // 准备测试环境 - let temp_dir = TempDir::new().unwrap(); - let database = Arc::new(Database::new_in_memory().unwrap()); - let import_service = TemplateImportService::new(database.clone()); - - // 测试无效文件路径 - let invalid_request = ImportTemplateRequest { - file_path: "/不存在的路径/draft_content.json".to_string(), - template_name: "测试模板".to_string(), - project_id: None, - auto_upload: false, - }; - - let result = import_service.import_template(invalid_request, None).await; - assert!(result.is_err(), "导入不存在的文件应该失败"); - - // 测试无效的JSON文件 - let invalid_json_file = temp_dir.path().join("invalid.json"); - fs::write(&invalid_json_file, "{ invalid json }").await.unwrap(); - - let invalid_json_request = ImportTemplateRequest { - file_path: invalid_json_file.to_string_lossy().to_string(), - template_name: "测试模板".to_string(), - project_id: None, - auto_upload: false, - }; - - let result = import_service.import_template(invalid_json_request, None).await; - assert!(result.is_err(), "导入无效JSON应该失败"); - - // 测试缺少必要字段的JSON - let incomplete_json = serde_json::json!({ - "materials": [], - "tracks": [] - // 缺少其他必要字段 - }); - - let incomplete_file = temp_dir.path().join("incomplete.json"); - fs::write(&incomplete_file, incomplete_json.to_string()).await.unwrap(); - - let incomplete_request = ImportTemplateRequest { - file_path: incomplete_file.to_string_lossy().to_string(), - template_name: "测试模板".to_string(), - project_id: None, - auto_upload: false, - }; - - let result = import_service.import_template(incomplete_request, None).await; - assert!(result.is_err(), "导入不完整的JSON应该失败"); - - println!("✅ 错误处理和恢复测试通过"); -} - -// 辅助函数:创建测试用的草稿文件 -async fn create_test_draft_file(temp_dir: &TempDir) -> PathBuf { - let draft_content = create_test_draft_content(); - let draft_file = temp_dir.path().join("draft_content.json"); - fs::write(&draft_file, serde_json::to_string_pretty(&draft_content).unwrap()) - .await - .unwrap(); - draft_file -} - -// 辅助函数:创建多个测试草稿文件 -async fn create_multiple_test_draft_files(temp_dir: &TempDir, count: usize) -> Vec { - let mut files = Vec::new(); - - for i in 0..count { - let sub_dir = temp_dir.path().join(format!("template_{}", i)); - fs::create_dir_all(&sub_dir).await.unwrap(); - - let draft_content = create_test_draft_content_with_name(&format!("测试模板_{}", i)); - let draft_file = sub_dir.join("draft_content.json"); - fs::write(&draft_file, serde_json::to_string_pretty(&draft_content).unwrap()) - .await - .unwrap(); - - files.push(draft_file); - } - - files -} - -// 辅助函数:创建测试素材文件 -async fn create_test_material_files(temp_dir: &TempDir) -> Vec { - let mut files = Vec::new(); - - // 创建测试视频文件 - let video_file = temp_dir.path().join("test_video.mp4"); - fs::write(&video_file, b"fake video content").await.unwrap(); - files.push(video_file); - - // 创建测试音频文件 - let audio_file = temp_dir.path().join("test_audio.mp3"); - fs::write(&audio_file, b"fake audio content").await.unwrap(); - files.push(audio_file); - - // 创建测试图片文件 - let image_file = temp_dir.path().join("test_image.jpg"); - fs::write(&image_file, b"fake image content").await.unwrap(); - files.push(image_file); - - files -} diff --git a/apps/desktop/src-tauri/src/business/services/tests/template_service_tests.rs b/apps/desktop/src-tauri/src/business/services/tests/template_service_tests.rs deleted file mode 100644 index ace6cc7..0000000 --- a/apps/desktop/src-tauri/src/business/services/tests/template_service_tests.rs +++ /dev/null @@ -1,431 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::business::services::template_service::{TemplateService, TemplateQueryOptions}; - use crate::data::models::template::{ - Template, TemplateMaterial, Track, TrackSegment, CanvasConfig, - TemplateMaterialType, TrackType, ImportStatus, - CreateTemplateRequest - }; - use crate::infrastructure::database::Database; - use std::sync::Arc; - use tempfile::TempDir; - - async fn create_test_database() -> Arc { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let db_path = temp_dir.path().join("test.db"); - let database = Database::new_with_path(db_path.to_str().unwrap()) - .expect("Failed to create test database"); - - // 确保temp_dir不被释放 - std::mem::forget(temp_dir); - - Arc::new(database) - } - - fn create_test_template() -> Template { - let canvas_config = CanvasConfig { - width: 1920, - height: 1080, - ratio: "16:9".to_string(), - }; - - let mut template = Template::new( - "测试模板".to_string(), - canvas_config, - 30000000, // 30秒 - 30.0, - ); - - // 添加测试素材 - let material = TemplateMaterial::new( - template.id.clone(), - "test-material-id".to_string(), - "测试视频.mp4".to_string(), - TemplateMaterialType::Video, - "/path/to/test_video.mp4".to_string(), - ); - template.add_material(material); - - // 添加测试轨道 - let mut track = Track::new( - template.id.clone(), - "视频轨道".to_string(), - TrackType::Video, - 0, - ); - - // 添加轨道片段 - let segment = TrackSegment::new( - track.id.clone(), - "片段1".to_string(), - 0, - 15000000, // 15秒 - 0, - ); - track.add_segment(segment); - template.add_track(track); - - template - } - - #[tokio::test] - async fn test_create_template() { - let database = create_test_database().await; - let service = TemplateService::new(database); - - let request = CreateTemplateRequest { - name: "新模板".to_string(), - description: Some("测试描述".to_string()), - project_id: Some("test-project-id".to_string()), - source_file_path: "/path/to/draft.json".to_string(), - }; - - let result = service.create_template(request).await; - assert!(result.is_ok()); - - let template_id = result.unwrap(); - assert!(!template_id.is_empty()); - - // 验证模板是否被正确保存 - let saved_template = service.get_template_by_id(&template_id).await; - assert!(saved_template.is_ok()); - assert!(saved_template.unwrap().is_some()); - } - - #[tokio::test] - async fn test_save_and_get_template() { - let database = create_test_database().await; - let service = TemplateService::new(database); - - let template = create_test_template(); - let template_id = template.id.clone(); - - // 保存模板 - let save_result = service.save_template(&template).await; - assert!(save_result.is_ok()); - - // 获取模板 - let get_result = service.get_template_by_id(&template_id).await; - assert!(get_result.is_ok()); - - let saved_template = get_result.unwrap(); - assert!(saved_template.is_some()); - - let saved_template = saved_template.unwrap(); - assert_eq!(saved_template.id, template.id); - assert_eq!(saved_template.name, template.name); - assert_eq!(saved_template.canvas_config.width, template.canvas_config.width); - assert_eq!(saved_template.canvas_config.height, template.canvas_config.height); - assert_eq!(saved_template.duration, template.duration); - assert_eq!(saved_template.fps, template.fps); - - // 验证素材 - assert_eq!(saved_template.materials.len(), 1); - let saved_material = &saved_template.materials[0]; - assert_eq!(saved_material.name, "测试视频.mp4"); - assert!(matches!(saved_material.material_type, TemplateMaterialType::Video)); - - // 验证轨道 - assert_eq!(saved_template.tracks.len(), 1); - let saved_track = &saved_template.tracks[0]; - assert_eq!(saved_track.name, "视频轨道"); - assert!(matches!(saved_track.track_type, TrackType::Video)); - - // 验证轨道片段 - assert_eq!(saved_track.segments.len(), 1); - let saved_segment = &saved_track.segments[0]; - assert_eq!(saved_segment.name, "片段1"); - assert_eq!(saved_segment.start_time, 0); - assert_eq!(saved_segment.duration, 15000000); - } - - #[tokio::test] - async fn test_get_nonexistent_template() { - let database = create_test_database().await; - let service = TemplateService::new(database); - - let result = service.get_template_by_id("nonexistent-id").await; - assert!(result.is_ok()); - assert!(result.unwrap().is_none()); - } - - #[tokio::test] - async fn test_list_templates_empty() { - let database = create_test_database().await; - let service = TemplateService::new(database); - - let options = TemplateQueryOptions::default(); - let result = service.list_templates(options).await; - - assert!(result.is_ok()); - let response = result.unwrap(); - assert_eq!(response.templates.len(), 0); - assert_eq!(response.total, 0); - } - - #[tokio::test] - async fn test_list_templates_with_data() { - let database = create_test_database().await; - let service = TemplateService::new(database); - - // 保存几个测试模板 - let template1 = create_test_template(); - let mut template2 = create_test_template(); - template2.name = "第二个模板".to_string(); - template2.project_id = Some("project-2".to_string()); - - service.save_template(&template1).await.unwrap(); - service.save_template(&template2).await.unwrap(); - - // 查询所有模板 - let options = TemplateQueryOptions::default(); - let result = service.list_templates(options).await; - - assert!(result.is_ok()); - let response = result.unwrap(); - assert_eq!(response.templates.len(), 2); - assert_eq!(response.total, 2); - - // 验证模板名称 - let names: Vec<&String> = response.templates.iter().map(|t| &t.name).collect(); - assert!(names.contains(&&"测试模板".to_string())); - assert!(names.contains(&&"第二个模板".to_string())); - } - - #[tokio::test] - async fn test_list_templates_with_project_filter() { - let database = create_test_database().await; - let service = TemplateService::new(database); - - // 保存不同项目的模板 - let mut template1 = create_test_template(); - template1.project_id = Some("project-1".to_string()); - - let mut template2 = create_test_template(); - template2.project_id = Some("project-2".to_string()); - - let mut template3 = create_test_template(); - template3.project_id = None; // 无项目关联 - - service.save_template(&template1).await.unwrap(); - service.save_template(&template2).await.unwrap(); - service.save_template(&template3).await.unwrap(); - - // 查询特定项目的模板 - let options = TemplateQueryOptions { - project_id: Some("project-1".to_string()), - ..Default::default() - }; - let result = service.list_templates(options).await; - - assert!(result.is_ok()); - let response = result.unwrap(); - assert_eq!(response.templates.len(), 1); - assert_eq!(response.total, 1); - assert_eq!(response.templates[0].project_id, Some("project-1".to_string())); - } - - #[tokio::test] - async fn test_list_templates_with_status_filter() { - let database = create_test_database().await; - let service = TemplateService::new(database); - - // 保存不同状态的模板 - let mut template1 = create_test_template(); - template1.import_status = ImportStatus::Completed; - - let mut template2 = create_test_template(); - template2.import_status = ImportStatus::Failed; - - service.save_template(&template1).await.unwrap(); - service.save_template(&template2).await.unwrap(); - - // 查询已完成的模板 - let options = TemplateQueryOptions { - import_status: Some(ImportStatus::Completed), - ..Default::default() - }; - let result = service.list_templates(options).await; - - assert!(result.is_ok()); - let response = result.unwrap(); - assert_eq!(response.templates.len(), 1); - assert_eq!(response.total, 1); - assert!(matches!(response.templates[0].import_status, ImportStatus::Completed)); - } - - #[tokio::test] - async fn test_list_templates_with_search() { - let database = create_test_database().await; - let service = TemplateService::new(database); - - // 保存不同名称的模板 - let mut template1 = create_test_template(); - template1.name = "视频模板".to_string(); - - let mut template2 = create_test_template(); - template2.name = "音频模板".to_string(); - - let mut template3 = create_test_template(); - template3.name = "图片模板".to_string(); - template3.description = Some("包含视频内容".to_string()); - - service.save_template(&template1).await.unwrap(); - service.save_template(&template2).await.unwrap(); - service.save_template(&template3).await.unwrap(); - - // 搜索包含"视频"的模板 - let options = TemplateQueryOptions { - search_keyword: Some("视频".to_string()), - ..Default::default() - }; - let result = service.list_templates(options).await; - - assert!(result.is_ok()); - let response = result.unwrap(); - assert_eq!(response.templates.len(), 2); // "视频模板" 和 "图片模板"(描述中包含"视频") - assert_eq!(response.total, 2); - } - - #[tokio::test] - async fn test_list_templates_with_pagination() { - let database = create_test_database().await; - let service = TemplateService::new(database); - - // 保存多个模板 - for i in 0..5 { - let mut template = create_test_template(); - template.name = format!("模板{}", i); - service.save_template(&template).await.unwrap(); - } - - // 测试分页 - let options = TemplateQueryOptions { - limit: Some(2), - offset: Some(1), - ..Default::default() - }; - let result = service.list_templates(options).await; - - assert!(result.is_ok()); - let response = result.unwrap(); - assert_eq!(response.templates.len(), 2); // 限制2个 - assert_eq!(response.total, 5); // 总共5个 - } - - #[tokio::test] - async fn test_update_template() { - let database = create_test_database().await; - let service = TemplateService::new(database); - - let mut template = create_test_template(); - let template_id = template.id.clone(); - - // 保存原始模板 - service.save_template(&template).await.unwrap(); - - // 修改模板 - template.name = "更新后的模板".to_string(); - template.description = Some("更新后的描述".to_string()); - - // 更新模板 - let update_result = service.update_template(&template).await; - assert!(update_result.is_ok()); - - // 验证更新 - let updated_template = service.get_template_by_id(&template_id).await.unwrap().unwrap(); - assert_eq!(updated_template.name, "更新后的模板"); - assert_eq!(updated_template.description, Some("更新后的描述".to_string())); - } - - #[tokio::test] - async fn test_delete_template() { - let database = create_test_database().await; - let service = TemplateService::new(database); - - let template = create_test_template(); - let template_id = template.id.clone(); - - // 保存模板 - service.save_template(&template).await.unwrap(); - - // 验证模板存在 - let template_before = service.get_template_by_id(&template_id).await.unwrap(); - assert!(template_before.is_some()); - assert!(template_before.unwrap().is_active); - - // 删除模板 - let delete_result = service.delete_template(&template_id).await; - assert!(delete_result.is_ok()); - - // 验证模板被标记为不活跃(软删除) - let template_after = service.get_template_by_id(&template_id).await.unwrap(); - assert!(template_after.is_some()); - assert!(!template_after.unwrap().is_active); - - // 验证在列表中不再显示 - let list_result = service.list_templates(TemplateQueryOptions::default()).await.unwrap(); - assert_eq!(list_result.templates.len(), 0); - assert_eq!(list_result.total, 0); - } - - #[tokio::test] - async fn test_complex_template_with_multiple_materials_and_tracks() { - let database = create_test_database().await; - let service = TemplateService::new(database); - - let mut template = create_test_template(); - - // 添加更多素材 - let audio_material = TemplateMaterial::new( - template.id.clone(), - "audio-material-id".to_string(), - "背景音乐.mp3".to_string(), - TemplateMaterialType::Audio, - "/path/to/audio.mp3".to_string(), - ); - template.add_material(audio_material); - - let image_material = TemplateMaterial::new( - template.id.clone(), - "image-material-id".to_string(), - "封面图片.jpg".to_string(), - TemplateMaterialType::Image, - "/path/to/image.jpg".to_string(), - ); - template.add_material(image_material); - - // 添加音频轨道 - let audio_track = Track::new( - template.id.clone(), - "音频轨道".to_string(), - TrackType::Audio, - 1, - ); - template.add_track(audio_track); - - // 保存复杂模板 - let save_result = service.save_template(&template).await; - assert!(save_result.is_ok()); - - // 获取并验证 - let saved_template = service.get_template_by_id(&template.id).await.unwrap().unwrap(); - assert_eq!(saved_template.materials.len(), 3); // 1 video + 1 audio + 1 image - assert_eq!(saved_template.tracks.len(), 2); // 1 video track + 1 audio track - - // 验证素材类型 - let material_types: Vec<_> = saved_template.materials.iter() - .map(|m| &m.material_type) - .collect(); - assert!(material_types.iter().any(|t| matches!(t, TemplateMaterialType::Video))); - assert!(material_types.iter().any(|t| matches!(t, TemplateMaterialType::Audio))); - assert!(material_types.iter().any(|t| matches!(t, TemplateMaterialType::Image))); - - // 验证轨道类型 - let track_types: Vec<_> = saved_template.tracks.iter() - .map(|t| &t.track_type) - .collect(); - assert!(track_types.iter().any(|t| matches!(t, TrackType::Video))); - assert!(track_types.iter().any(|t| matches!(t, TrackType::Audio))); - } -} diff --git a/apps/desktop/src-tauri/src/data/repositories/material_repository.rs b/apps/desktop/src-tauri/src/data/repositories/material_repository.rs index 456ce8d..06ed8df 100644 --- a/apps/desktop/src-tauri/src/data/repositories/material_repository.rs +++ b/apps/desktop/src-tauri/src/data/repositories/material_repository.rs @@ -21,8 +21,13 @@ impl MaterialRepository { /// 创建素材 pub fn create(&self, material: &Material) -> Result<()> { - let conn = self.database.get_connection(); - let conn = conn.lock().unwrap(); + // 预处理 JSON 序列化,避免在持有锁时进行 + let metadata_json = serde_json::to_string(&material.metadata)?; + let scene_detection_json = material.scene_detection.as_ref() + .map(|sd| serde_json::to_string(sd)) + .transpose()?; + + self.database.with_connection(|conn| { // 添加调试日志 println!("Creating material with model_id: {:?}", material.model_id); diff --git a/apps/desktop/src-tauri/src/data/repositories/model_repository.rs b/apps/desktop/src-tauri/src/data/repositories/model_repository.rs index 7cd1527..5d2bfc8 100644 --- a/apps/desktop/src-tauri/src/data/repositories/model_repository.rs +++ b/apps/desktop/src-tauri/src/data/repositories/model_repository.rs @@ -1,4 +1,4 @@ -use rusqlite::{ Result, Row}; +use rusqlite::{Connection, Result, Row}; use std::sync::Arc; use crate::data::models::model::{ Model, ModelPhoto, Gender, ModelStatus, PhotoType, @@ -373,12 +373,97 @@ impl ModelRepository { } /// 获取模特照片 + /// 使用带超时的数据库访问模式,避免无限等待 pub fn get_photos(&self, model_id: &str) -> Result> { println!("get_photos 开始执行,model_id: {}", model_id); - let conn = self.database.get_connection(); - let conn = conn.lock().unwrap(); - println!("get_photos 获取数据库连接成功"); + // 直接使用简化的实现,避免复杂的超时逻辑 + let result = self.get_photos_simple(model_id); + + match &result { + Ok(photos) => println!("get_photos 执行完成,返回 {} 张照片", photos.len()), + Err(e) => eprintln!("get_photos 执行失败: {}", e), + } + + result + } + + /// 简化的获取照片实现 + fn get_photos_simple(&self, model_id: &str) -> Result> { + println!("get_photos_simple 开始获取数据库连接"); + + // 直接获取连接,但添加详细日志 + let conn_arc = self.database.get_connection(); + println!("get_photos_simple 获取连接引用成功"); + + // 使用阻塞方式获取锁,但添加超时检测 + let conn = match conn_arc.lock() { + Ok(conn) => { + println!("get_photos_simple 成功获取数据库连接锁"); + conn + }, + Err(e) => { + eprintln!("get_photos_simple 获取数据库连接锁失败: {}", e); + return Err(rusqlite::Error::SqliteFailure( + rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY), + Some("数据库连接锁获取失败".to_string()), + )); + } + }; + + println!("get_photos_simple 开始执行查询"); + self.execute_photo_query(&conn, model_id) + } + + /// 带超时的获取照片实现 + fn get_photos_with_timeout(&self, model_id: &str) -> Result> { + use std::time::{Duration, Instant}; + + let start_time = Instant::now(); + let timeout = Duration::from_secs(10); // 10秒超时 + let mut retry_count = 0; + + // 获取连接引用,避免生命周期问题 + let conn_arc = self.database.get_connection(); + + loop { + println!("get_photos 尝试获取数据库连接,重试次数: {}", retry_count); + + // 尝试获取锁 + match conn_arc.try_lock() { + Ok(conn) => { + println!("get_photos 成功获取数据库连接"); + + // 成功获取锁,执行查询 + let result = self.execute_photo_query(&conn, model_id); + + // 返回结果 + return result; + }, + Err(_) => { + println!("get_photos 数据库连接被占用,重试中..."); + + // 检查是否超时 + if start_time.elapsed() >= timeout { + eprintln!("get_photos 获取数据库连接超时,已等待 {:?}", start_time.elapsed()); + return Err(rusqlite::Error::SqliteFailure( + rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY), + Some(format!("数据库连接获取超时,已重试 {} 次", retry_count)), + )); + } + + // 等待一段时间后重试 + retry_count += 1; + let wait_time = std::cmp::min(50 * retry_count, 500); // 递增等待时间,最大500ms + std::thread::sleep(Duration::from_millis(wait_time)); + } + } + } + } + + /// 执行照片查询的具体实现 + fn execute_photo_query(&self, conn: &Connection, model_id: &str) -> Result> { + println!("get_photos 开始准备SQL语句"); let mut stmt = conn.prepare( "SELECT id, model_id, file_path, file_name, file_size, photo_type, @@ -388,51 +473,62 @@ impl ModelRepository { println!("get_photos SQL 语句准备成功"); let photo_iter = stmt.query_map([model_id], |row| { - println!("get_photos 开始解析照片行数据"); - let result = self.row_to_photo(row); - println!("get_photos 照片行数据解析结果: {:?}", result.is_ok()); - result + self.row_to_photo(row) })?; println!("get_photos 查询执行成功"); let mut photos = Vec::new(); - for (index, photo) in photo_iter.enumerate() { - println!("get_photos 处理第 {} 张照片", index + 1); - photos.push(photo?); + for (index, photo_result) in photo_iter.enumerate() { + match photo_result { + Ok(photo) => { + photos.push(photo); + if index % 10 == 0 && index > 0 { + println!("get_photos 已处理 {} 张照片", index); + } + }, + Err(e) => { + eprintln!("get_photos 处理第 {} 张照片时出错: {}", index, e); + return Err(e); + } + } } - println!("get_photos 执行完成,返回 {} 张照片", photos.len()); + println!("get_photos 查询处理完成,共 {} 张照片", photos.len()); Ok(photos) } /// 添加模特照片 pub fn add_photo(&self, photo: &ModelPhoto) -> Result<()> { - let conn = self.database.get_connection(); - let conn = conn.lock().unwrap(); - + // 在函数外部处理 JSON 序列化,避免在持有锁时进行 let tags_json = serde_json::to_string(&photo.tags) .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - conn.execute( - "INSERT INTO model_photos ( - id, model_id, file_path, file_name, file_size, photo_type, - description, tags, is_cover, created_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", - ( - &photo.id, - &photo.model_id, - &photo.file_path, - &photo.file_name, - photo.file_size, - serde_json::to_string(&photo.photo_type).unwrap(), - &photo.description, - tags_json, - photo.is_cover, - photo.created_at.to_rfc3339(), - ), - )?; + let photo_type_json = serde_json::to_string(&photo.photo_type) + .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?; - Ok(()) + // 使用优化的数据库访问模式 + self.database.execute(|conn| { + conn.execute( + "INSERT INTO model_photos ( + id, model_id, file_path, file_name, file_size, photo_type, + description, tags, is_cover, created_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + ( + &photo.id, + &photo.model_id, + &photo.file_path, + &photo.file_name, + photo.file_size, + &photo_type_json, + &photo.description, + &tags_json, + photo.is_cover, + photo.created_at.to_rfc3339(), + ), + )?; + + Ok(()) + }) } /// 删除模特照片 diff --git a/apps/desktop/src-tauri/src/infrastructure/database.rs b/apps/desktop/src-tauri/src/infrastructure/database.rs index ce669f3..36793e3 100644 --- a/apps/desktop/src-tauri/src/infrastructure/database.rs +++ b/apps/desktop/src-tauri/src/infrastructure/database.rs @@ -75,6 +75,61 @@ impl Database { Arc::clone(&self.connection) } + /// 检查数据库连接状态 + pub fn check_connection_status(&self) -> String { + match self.connection.try_lock() { + Ok(_) => "连接可用".to_string(), + Err(std::sync::TryLockError::Poisoned(_)) => "连接已损坏".to_string(), + Err(std::sync::TryLockError::WouldBlock) => "连接被占用".to_string(), + } + } + + /// 执行数据库操作的辅助方法,自动处理锁的获取和释放 + /// 这是推荐的数据库访问方式,可以避免锁竞争问题 + pub fn with_connection(&self, operation: F) -> Result + where + F: FnOnce(&Connection) -> Result, + { + // 直接使用 connection 字段,避免额外的 Arc::clone + match self.connection.try_lock() { + Ok(conn) => { + // 成功获取锁,执行操作 + operation(&*conn) + }, + Err(_) => { + // 锁被占用,等待一小段时间后重试 + std::thread::sleep(std::time::Duration::from_millis(10)); + + // 使用阻塞方式获取锁,但有错误处理 + let conn = self.connection.lock().map_err(|e| { + eprintln!("数据库连接锁获取失败: {}", e); + rusqlite::Error::SqliteFailure( + rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY), + Some("数据库连接被锁定".to_string()), + ) + })?; + + operation(&*conn) + } + } + } + + /// 执行只读查询的辅助方法 + pub fn query(&self, operation: F) -> Result + where + F: FnOnce(&Connection) -> Result, + { + self.with_connection(operation) + } + + /// 执行写入操作的辅助方法 + pub fn execute(&self, operation: F) -> Result + where + F: FnOnce(&Connection) -> Result, + { + self.with_connection(operation) + } + /// 初始化数据库表结构 /// 遵循模块化设计原则,清晰的表结构定义 fn initialize_tables(&self) -> Result<()> { @@ -924,70 +979,6 @@ impl Database { Ok(()) } - /// 清理无效的项目记录 - /// 删除路径不存在的项目记录,避免 UNIQUE 约束冲突 - fn cleanup_invalid_projects(&self) -> Result<()> { - println!("Starting cleanup of invalid projects..."); - - let conn = self.connection.lock().unwrap(); - - // 先检查是否有项目记录 - let project_count: i64 = conn.query_row( - "SELECT COUNT(*) FROM projects", - [], - |row| row.get(0) - )?; - - println!("Found {} total projects in database", project_count); - - if project_count == 0 { - println!("No projects to clean up"); - return Ok(()); - } - - // 获取所有项目路径(限制查询以避免潜在的性能问题) - let mut stmt = conn.prepare("SELECT id, path FROM projects LIMIT 100")?; - let project_iter = stmt.query_map([], |row| { - let id: String = row.get(0)?; - let path: String = row.get(1)?; - Ok((id, path)) - })?; - - let mut invalid_project_ids = Vec::new(); - - for project_result in project_iter { - match project_result { - Ok((id, path)) => { - // 检查路径是否存在 - if !std::path::Path::new(&path).exists() { - invalid_project_ids.push(id.clone()); - println!("Found invalid project path: {} (ID: {})", path, id); - } - } - Err(e) => { - println!("Error reading project record: {}", e); - continue; - } - } - } - - // 删除无效的项目记录 - if !invalid_project_ids.is_empty() { - println!("Cleaning up {} invalid project records", invalid_project_ids.len()); - for project_id in &invalid_project_ids { - match conn.execute("DELETE FROM projects WHERE id = ?1", [project_id]) { - Ok(_) => println!("Deleted project record: {}", project_id), - Err(e) => println!("Failed to delete project record {}: {}", project_id, e), - } - } - println!("Cleanup completed"); - } else { - println!("No invalid project records found"); - } - - Ok(()) - } - /// 获取数据库文件路径 /// 遵循安全存储原则,将数据库存储在应用数据目录 fn get_database_path() -> PathBuf { diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 1969eb2..28a80a9 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -46,6 +46,9 @@ pub fn run() { commands::system_commands::cleanup_performance_data, commands::system_commands::record_performance_metric, commands::system_commands::cleanup_invalid_projects, + commands::database_commands::initialize_database, + commands::database_commands::check_database_connection, + commands::database_commands::force_release_database_connection, commands::material_commands::import_materials, commands::material_commands::import_materials_async, commands::material_commands::select_material_folders, diff --git a/apps/desktop/src-tauri/src/presentation/commands/database_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/database_commands.rs new file mode 100644 index 0000000..6143eb1 --- /dev/null +++ b/apps/desktop/src-tauri/src/presentation/commands/database_commands.rs @@ -0,0 +1,32 @@ +use crate::app_state::AppState; +use tauri::State; + +/// 初始化数据库 +/// 遵循 Tauri 开发规范的命令设计模式 +#[tauri::command] +pub fn initialize_database(state: State) -> Result<(), String> { + state.initialize_database().map_err(|e| e.to_string()) +} + +/// 检查数据库连接状态 +/// 用于诊断数据库连接问题 +#[tauri::command] +pub fn check_database_connection(state: State) -> Result { + let database_guard = state.database.lock().map_err(|e| format!("获取数据库失败: {}", e))?; + let database = database_guard.as_ref().ok_or("数据库未初始化")?; + + Ok(database.check_connection_status()) +} + +/// 强制释放数据库连接 +/// 紧急情况下使用,可能导致数据不一致 +#[tauri::command] +pub fn force_release_database_connection(state: State) -> Result { + println!("警告:强制释放数据库连接"); + + // 重新初始化数据库连接 + match state.initialize_database() { + Ok(_) => Ok("数据库连接已重新初始化".to_string()), + Err(e) => Err(format!("重新初始化数据库失败: {}", e)) + } +} diff --git a/apps/desktop/src-tauri/src/presentation/commands/mod.rs b/apps/desktop/src-tauri/src/presentation/commands/mod.rs index 4cddd7e..b68bce9 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/mod.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/mod.rs @@ -1,6 +1,7 @@ pub mod project_commands; pub mod system_commands; pub mod material_commands; +pub mod database_commands; pub mod material_segment_view_commands; pub mod model_commands; pub mod ai_classification_commands; diff --git a/apps/desktop/src/components/DatabaseDebugPanel.tsx b/apps/desktop/src/components/DatabaseDebugPanel.tsx new file mode 100644 index 0000000..cb46131 --- /dev/null +++ b/apps/desktop/src/components/DatabaseDebugPanel.tsx @@ -0,0 +1,154 @@ +import React, { useState } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { Database, RefreshCw, AlertTriangle, CheckCircle, XCircle } from 'lucide-react'; + +/** + * 数据库调试面板组件 + * 用于诊断和解决数据库连接问题 + */ +export const DatabaseDebugPanel: React.FC = () => { + const [connectionStatus, setConnectionStatus] = useState(''); + const [isChecking, setIsChecking] = useState(false); + const [isReleasing, setIsReleasing] = useState(false); + const [lastCheckTime, setLastCheckTime] = useState(''); + + // 检查数据库连接状态 + const checkConnection = async () => { + setIsChecking(true); + try { + const status = await invoke('check_database_connection'); + setConnectionStatus(status); + setLastCheckTime(new Date().toLocaleTimeString()); + } catch (error) { + setConnectionStatus(`检查失败: ${error}`); + } finally { + setIsChecking(false); + } + }; + + // 强制释放数据库连接 + const forceReleaseConnection = async () => { + setIsReleasing(true); + try { + const result = await invoke('force_release_database_connection'); + setConnectionStatus(result); + setLastCheckTime(new Date().toLocaleTimeString()); + } catch (error) { + setConnectionStatus(`释放失败: ${error}`); + } finally { + setIsReleasing(false); + } + }; + + // 获取状态图标 + const getStatusIcon = () => { + if (connectionStatus.includes('可用')) { + return ; + } else if (connectionStatus.includes('被占用')) { + return ; + } else if (connectionStatus.includes('损坏') || connectionStatus.includes('失败')) { + return ; + } + return ; + }; + + // 获取状态颜色 + const getStatusColor = () => { + if (connectionStatus.includes('可用')) { + return 'text-green-700 bg-green-50 border-green-200'; + } else if (connectionStatus.includes('被占用')) { + return 'text-yellow-700 bg-yellow-50 border-yellow-200'; + } else if (connectionStatus.includes('损坏') || connectionStatus.includes('失败')) { + return 'text-red-700 bg-red-50 border-red-200'; + } + return 'text-gray-700 bg-gray-50 border-gray-200'; + }; + + return ( +
+
+ +

数据库连接诊断

+
+ + {/* 连接状态显示 */} +
+
+ 连接状态 + {lastCheckTime && ( + + 最后检查: {lastCheckTime} + + )} +
+ + {connectionStatus ? ( +
+ {getStatusIcon()} + {connectionStatus} +
+ ) : ( +
+ + 点击检查按钮获取连接状态 +
+ )} +
+ + {/* 操作按钮 */} +
+ + + +
+ + {/* 使用说明 */} +
+

使用说明

+
    +
  • 连接可用: 数据库连接正常,可以执行操作
  • +
  • 连接被占用: 其他操作正在使用连接,请稍等
  • +
  • 连接已损坏: 连接出现问题,需要强制释放
  • +
  • 强制释放: 紧急情况下使用,可能导致数据不一致
  • +
+
+ + {/* 故障排除建议 */} + {connectionStatus.includes('被占用') && ( +
+

故障排除建议

+
    +
  • 1. 等待当前操作完成(通常几秒钟)
  • +
  • 2. 检查是否有长时间运行的导入或分析任务
  • +
  • 3. 如果问题持续,可以尝试强制释放连接
  • +
+
+ )} + + {connectionStatus.includes('损坏') && ( +
+

紧急处理

+
    +
  • 1. 立即点击"强制释放连接"按钮
  • +
  • 2. 重新启动应用程序
  • +
  • 3. 如果问题持续,请联系技术支持
  • +
+
+ )} +
+ ); +};