diff --git a/apps/desktop/src-tauri/src/business/services/material_matching_service.rs b/apps/desktop/src-tauri/src/business/services/material_matching_service.rs index 20df000..6e684fd 100644 --- a/apps/desktop/src-tauri/src/business/services/material_matching_service.rs +++ b/apps/desktop/src-tauri/src/business/services/material_matching_service.rs @@ -11,8 +11,10 @@ use crate::data::models::{ use crate::data::repositories::{ material_repository::MaterialRepository, video_classification_repository::VideoClassificationRepository, + template_matching_result_repository::TemplateMatchingResultRepository, }; use crate::business::services::template_service::TemplateService; +use crate::business::services::template_matching_result_service::TemplateMatchingResultService; use anyhow::{Result, anyhow}; use serde::{Serialize, Deserialize}; use std::collections::{HashMap, HashSet}; @@ -23,6 +25,7 @@ pub struct MaterialMatchingService { material_repo: Arc, template_service: Arc, video_classification_repo: Arc, + matching_result_service: Option>, } /// 素材匹配请求 @@ -89,6 +92,22 @@ impl MaterialMatchingService { material_repo, template_service, video_classification_repo, + matching_result_service: None, + } + } + + /// 创建新的素材匹配服务实例(带结果保存功能) + pub fn new_with_result_service( + material_repo: Arc, + template_service: Arc, + video_classification_repo: Arc, + matching_result_service: Arc, + ) -> Self { + Self { + material_repo, + template_service, + video_classification_repo, + matching_result_service: Some(matching_result_service), } } @@ -193,6 +212,41 @@ impl MaterialMatchingService { }) } + /// 执行素材匹配并自动保存结果到数据库 + pub async fn match_materials_and_save( + &self, + request: MaterialMatchingRequest, + result_name: String, + description: Option, + ) -> Result<(MaterialMatchingResult, Option)> { + let start_time = std::time::Instant::now(); + + // 执行匹配 + let matching_result = self.match_materials(request).await?; + + let matching_duration_ms = start_time.elapsed().as_millis() as u64; + + // 如果配置了结果保存服务,则自动保存结果 + if let Some(result_service) = &self.matching_result_service { + match result_service.save_matching_result( + &matching_result, + result_name, + description, + matching_duration_ms, + ).await { + Ok(saved_result) => { + return Ok((matching_result, Some(saved_result))); + } + Err(e) => { + // 保存失败时记录错误但不影响匹配结果的返回 + eprintln!("保存匹配结果失败: {}", e); + } + } + } + + Ok((matching_result, None)) + } + /// 获取已分类的素材片段 async fn get_classified_segments( &self, diff --git a/apps/desktop/src-tauri/src/business/services/mod.rs b/apps/desktop/src-tauri/src/business/services/mod.rs index f155113..49db08c 100644 --- a/apps/desktop/src-tauri/src/business/services/mod.rs +++ b/apps/desktop/src-tauri/src/business/services/mod.rs @@ -18,6 +18,7 @@ pub mod performance_monitor; pub mod enhanced_template_import_service; pub mod project_template_binding_service; pub mod material_matching_service; +pub mod template_matching_result_service; #[cfg(test)] pub mod tests; diff --git a/apps/desktop/src-tauri/src/business/services/template_matching_result_service.rs b/apps/desktop/src-tauri/src/business/services/template_matching_result_service.rs new file mode 100644 index 0000000..c454e4d --- /dev/null +++ b/apps/desktop/src-tauri/src/business/services/template_matching_result_service.rs @@ -0,0 +1,284 @@ +use anyhow::{Result, anyhow}; +use std::sync::Arc; +use chrono::Utc; + +use crate::data::models::template_matching_result::{ + TemplateMatchingResult, MatchingSegmentResult, MatchingFailedSegmentResult, + CreateTemplateMatchingResultRequest, TemplateMatchingResultQueryOptions, + MatchingResultStatus, +}; +use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository; +use crate::business::services::material_matching_service::{ + MaterialMatchingResult as ServiceMatchingResult, SegmentMatch, FailedSegmentMatch, +}; + +/// 模板匹配结果服务 +/// 遵循 Tauri 开发规范的业务逻辑设计原则 +pub struct TemplateMatchingResultService { + repository: Arc, +} + +impl TemplateMatchingResultService { + /// 创建新的模板匹配结果服务实例 + pub fn new(repository: Arc) -> Self { + Self { repository } + } + + /// 保存匹配结果到数据库 + /// 这是一个完整的事务操作,保存主结果和所有相关的片段信息 + pub async fn save_matching_result( + &self, + service_result: &ServiceMatchingResult, + result_name: String, + description: Option, + matching_duration_ms: u64, + ) -> Result { + // 创建主匹配结果记录 + let create_request = CreateTemplateMatchingResultRequest { + project_id: service_result.project_id.clone(), + template_id: service_result.template_id.clone(), + binding_id: service_result.binding_id.clone(), + result_name, + description, + }; + + let mut matching_result = self.repository.create(create_request)?; + + // 更新统计信息 + let total_segments = service_result.matches.len() + service_result.failed_segments.len(); + let matched_segments = service_result.matches.len(); + let failed_segments = service_result.failed_segments.len(); + + // 计算使用的素材和模特数量 + let mut used_material_ids = std::collections::HashSet::new(); + let mut used_model_names = std::collections::HashSet::new(); + + for segment_match in &service_result.matches { + used_material_ids.insert(segment_match.material_segment.material_id.clone()); + if let Some(model_name) = &segment_match.model_name { + used_model_names.insert(model_name.clone()); + } + } + + matching_result.update_statistics( + total_segments as u32, + matched_segments as u32, + failed_segments as u32, + used_material_ids.len() as u32, + used_model_names.len() as u32, + matching_duration_ms, + ); + + // 更新主记录 + self.repository.update(&matching_result)?; + + // 保存成功匹配的片段 + let segment_results: Vec = service_result.matches + .iter() + .map(|segment_match| { + let mut segment_result = MatchingSegmentResult::new( + uuid::Uuid::new_v4().to_string(), + matching_result.id.clone(), + segment_match.track_segment_id.clone(), + segment_match.track_segment_name.clone(), + segment_match.material_segment_id.clone(), + segment_match.material_segment.material_id.clone(), + segment_match.material_name.clone(), + segment_match.match_score, + segment_match.match_reason.clone(), + (segment_match.material_segment.duration * 1_000_000.0) as u64, + (segment_match.material_segment.start_time * 1_000_000.0) as u64, + (segment_match.material_segment.end_time * 1_000_000.0) as u64, + ); + + // 设置模特信息 + if let Some(model_name) = &segment_match.model_name { + // 这里可以通过模特名称查找模特ID,暂时使用名称作为ID + segment_result.set_model_info(model_name.clone(), model_name.clone()); + } + + segment_result + }) + .collect(); + + if !segment_results.is_empty() { + self.repository.create_segment_results(&segment_results)?; + } + + // 保存失败的片段 + let failed_segment_results: Vec = service_result.failed_segments + .iter() + .map(|failed_segment| { + let mut failed_result = MatchingFailedSegmentResult::new( + uuid::Uuid::new_v4().to_string(), + matching_result.id.clone(), + failed_segment.track_segment_id.clone(), + failed_segment.track_segment_name.clone(), + failed_segment.matching_rule.display_name(), + failed_segment.failure_reason.clone(), + 0, // 这里需要从模板中获取片段时长 + 0, // 这里需要从模板中获取开始时间 + 0, // 这里需要从模板中获取结束时间 + ); + + // 设置匹配规则数据 + if let Ok(rule_json) = serde_json::to_string(&failed_segment.matching_rule) { + failed_result.set_matching_rule_data(rule_json); + } + + failed_result + }) + .collect(); + + if !failed_segment_results.is_empty() { + self.repository.create_failed_segment_results(&failed_segment_results)?; + } + + Ok(matching_result) + } + + /// 获取匹配结果详情(包含所有片段信息) + pub async fn get_matching_result_detail(&self, result_id: &str) -> Result> { + let matching_result = match self.repository.get_by_id(result_id)? { + Some(result) => result, + None => return Ok(None), + }; + + let segment_results = self.repository.get_segment_results_by_matching_result_id(result_id)?; + let failed_segment_results = self.repository.get_failed_segment_results_by_matching_result_id(result_id)?; + + Ok(Some(TemplateMatchingResultDetail { + matching_result, + segment_results, + failed_segment_results, + })) + } + + /// 获取项目的匹配结果列表 + pub async fn get_project_matching_results(&self, project_id: &str) -> Result> { + Ok(self.repository.get_by_project_id(project_id)?) + } + + /// 获取模板的匹配结果列表 + pub async fn get_template_matching_results(&self, template_id: &str) -> Result> { + Ok(self.repository.get_by_template_id(template_id)?) + } + + /// 获取绑定的匹配结果列表 + pub async fn get_binding_matching_results(&self, binding_id: &str) -> Result> { + Ok(self.repository.get_by_binding_id(binding_id)?) + } + + /// 查询匹配结果列表 + pub async fn list_matching_results(&self, options: TemplateMatchingResultQueryOptions) -> Result> { + Ok(self.repository.list(options)?) + } + + /// 删除匹配结果 + pub async fn delete_matching_result(&self, result_id: &str) -> Result { + Ok(self.repository.delete(result_id)?) + } + + /// 软删除匹配结果 + pub async fn soft_delete_matching_result(&self, result_id: &str) -> Result { + Ok(self.repository.soft_delete(result_id)?) + } + + /// 更新匹配结果名称和描述 + pub async fn update_matching_result_info( + &self, + result_id: &str, + result_name: Option, + description: Option, + ) -> Result> { + let mut matching_result = match self.repository.get_by_id(result_id)? { + Some(result) => result, + None => return Ok(None), + }; + + let mut updated = false; + + if let Some(name) = result_name { + matching_result.result_name = name; + updated = true; + } + + if let Some(desc) = description { + matching_result.set_description(desc); + updated = true; + } + + if updated { + matching_result.updated_at = Utc::now(); + self.repository.update(&matching_result)?; + } + + Ok(Some(matching_result)) + } + + /// 设置匹配结果质量评分 + pub async fn set_quality_score(&self, result_id: &str, quality_score: f64) -> Result> { + let mut matching_result = match self.repository.get_by_id(result_id)? { + Some(result) => result, + None => return Ok(None), + }; + + matching_result.set_quality_score(quality_score); + self.repository.update(&matching_result)?; + + Ok(Some(matching_result)) + } + + /// 获取匹配结果统计信息 + pub async fn get_matching_statistics(&self, project_id: Option<&str>) -> Result { + let options = TemplateMatchingResultQueryOptions { + project_id: project_id.map(|id| id.to_string()), + ..Default::default() + }; + + let results = self.repository.list(options)?; + + let total_results = results.len(); + let successful_results = results.iter().filter(|r| r.is_successful()).count(); + let total_segments: u32 = results.iter().map(|r| r.total_segments).sum(); + let matched_segments: u32 = results.iter().map(|r| r.matched_segments).sum(); + let total_materials: u32 = results.iter().map(|r| r.used_materials).sum(); + let total_models: u32 = results.iter().map(|r| r.used_models).sum(); + + let average_success_rate = if total_results > 0 { + results.iter().map(|r| r.success_rate).sum::() / total_results as f64 + } else { + 0.0 + }; + + Ok(MatchingStatistics { + total_results, + successful_results, + total_segments, + matched_segments, + total_materials, + total_models, + average_success_rate, + }) + } +} + +/// 模板匹配结果详情 +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct TemplateMatchingResultDetail { + pub matching_result: TemplateMatchingResult, + pub segment_results: Vec, + pub failed_segment_results: Vec, +} + +/// 匹配统计信息 +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct MatchingStatistics { + pub total_results: usize, + pub successful_results: usize, + pub total_segments: u32, + pub matched_segments: u32, + pub total_materials: u32, + pub total_models: u32, + pub average_success_rate: f64, +} 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 3c8c6db..0783847 100644 --- a/apps/desktop/src-tauri/src/business/services/tests/mod.rs +++ b/apps/desktop/src-tauri/src/business/services/tests/mod.rs @@ -1,5 +1,6 @@ pub mod draft_parser_tests; pub mod cloud_upload_service_tests; +pub mod template_matching_result_service_tests; // 测试工具函数 pub mod test_utils { diff --git a/apps/desktop/src-tauri/src/business/services/tests/template_matching_result_service_tests.rs b/apps/desktop/src-tauri/src/business/services/tests/template_matching_result_service_tests.rs new file mode 100644 index 0000000..5196727 --- /dev/null +++ b/apps/desktop/src-tauri/src/business/services/tests/template_matching_result_service_tests.rs @@ -0,0 +1,413 @@ +#[cfg(test)] +mod tests { + use super::super::super::template_matching_result_service::{ + TemplateMatchingResultService, TemplateMatchingResultDetail, MatchingStatistics, + }; + use crate::data::models::template_matching_result::{ + TemplateMatchingResult, MatchingSegmentResult, MatchingFailedSegmentResult, + CreateTemplateMatchingResultRequest, TemplateMatchingResultQueryOptions, + MatchingResultStatus, + }; + use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository; + use crate::business::services::material_matching_service::{ + MaterialMatchingResult as ServiceMatchingResult, SegmentMatch, FailedSegmentMatch, + MatchingStatistics as ServiceMatchingStatistics, + }; + use crate::data::models::template::SegmentMatchingRule; + use crate::data::models::material::MaterialSegment; + use crate::infrastructure::database::Database; + use std::sync::Arc; + use tempfile::TempDir; + use chrono::Utc; + + /// 创建测试数据库 + async fn create_test_database() -> Arc { + let database = Database::new().expect("Failed to create database"); + Arc::new(database) + } + + /// 创建测试用的素材匹配结果 + fn create_test_service_matching_result() -> ServiceMatchingResult { + let material_segment = MaterialSegment { + id: "segment-1".to_string(), + material_id: "material-1".to_string(), + segment_index: 0, + start_time: 0.0, + end_time: 10.0, + duration: 10.0, + file_path: "/test/video.mp4".to_string(), + file_size: 1024, + thumbnail_path: None, + created_at: Utc::now(), + }; + + let segment_match = SegmentMatch { + track_segment_id: "track-segment-1".to_string(), + track_segment_name: "测试轨道片段".to_string(), + material_segment_id: "segment-1".to_string(), + material_segment, + material_name: "测试素材".to_string(), + model_name: Some("测试模特".to_string()), + match_score: 0.95, + match_reason: "AI分类匹配".to_string(), + }; + + let failed_segment = FailedSegmentMatch { + track_segment_id: "track-segment-2".to_string(), + track_segment_name: "失败轨道片段".to_string(), + matching_rule: SegmentMatchingRule::AiClassification { + category_id: "category-1".to_string(), + category_name: "全身".to_string(), + }, + failure_reason: "没有找到匹配的素材".to_string(), + }; + + let statistics = ServiceMatchingStatistics { + total_segments: 2, + matched_segments: 1, + failed_segments: 1, + success_rate: 50.0, + used_materials: 1, + used_models: 1, + }; + + ServiceMatchingResult { + binding_id: "binding-1".to_string(), + template_id: "template-1".to_string(), + project_id: "project-1".to_string(), + matches: vec![segment_match], + statistics, + failed_segments: vec![failed_segment], + } + } + + #[tokio::test] + async fn test_save_matching_result() { + let database = create_test_database().await; + + // 先创建必要的依赖数据 + let conn = database.get_connection(); + let conn = conn.lock().unwrap(); + + // 创建项目 + let unique_path = format!("/test/path/{}", uuid::Uuid::new_v4()); + conn.execute( + "INSERT INTO projects (id, name, path, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)", + ["project-1", "测试项目", &unique_path, "2024-01-01T00:00:00Z", "2024-01-01T00:00:00Z"], + ).unwrap(); + + // 创建模板 + conn.execute( + "INSERT INTO templates (id, name, description, canvas_width, canvas_height, canvas_ratio, duration, fps, import_status, source_file_path, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", + ["template-1", "测试模板", "测试模板描述", "1920", "1080", "16:9", "10000000", "30.0", "Completed", "/test/template.json", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00Z"], + ).unwrap(); + + // 创建绑定 + conn.execute( + "INSERT INTO project_template_bindings (id, project_id, template_id, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)", + ["binding-1", "project-1", "template-1", "2024-01-01T00:00:00Z", "2024-01-01T00:00:00Z"], + ).unwrap(); + + drop(conn); + + let repository = Arc::new(TemplateMatchingResultRepository::new(database)); + let service = TemplateMatchingResultService::new(repository); + + let service_result = create_test_service_matching_result(); + let result_name = "测试匹配结果".to_string(); + let description = Some("这是一个测试匹配结果".to_string()); + let matching_duration_ms = 1500; + + let result = service.save_matching_result( + &service_result, + result_name.clone(), + description.clone(), + matching_duration_ms, + ).await; + + if let Err(e) = &result { + println!("Error saving matching result: {}", e); + } + assert!(result.is_ok()); + let saved_result = result.unwrap(); + + assert_eq!(saved_result.result_name, result_name); + assert_eq!(saved_result.description, description); + assert_eq!(saved_result.project_id, service_result.project_id); + assert_eq!(saved_result.template_id, service_result.template_id); + assert_eq!(saved_result.binding_id, service_result.binding_id); + assert_eq!(saved_result.total_segments, 2); + assert_eq!(saved_result.matched_segments, 1); + assert_eq!(saved_result.failed_segments, 1); + assert_eq!(saved_result.success_rate, 50.0); + assert_eq!(saved_result.used_materials, 1); + assert_eq!(saved_result.used_models, 1); + assert_eq!(saved_result.matching_duration_ms, matching_duration_ms); + assert_eq!(saved_result.status, MatchingResultStatus::PartialSuccess); + } + + #[tokio::test] + async fn test_get_matching_result_detail() { + let database = create_test_database().await; + let repository = Arc::new(TemplateMatchingResultRepository::new(database)); + let service = TemplateMatchingResultService::new(repository); + + // 先保存一个匹配结果 + let service_result = create_test_service_matching_result(); + let saved_result = service.save_matching_result( + &service_result, + "测试匹配结果".to_string(), + Some("测试描述".to_string()), + 1000, + ).await.unwrap(); + + // 获取详情 + let detail_result = service.get_matching_result_detail(&saved_result.id).await; + assert!(detail_result.is_ok()); + + let detail = detail_result.unwrap(); + assert!(detail.is_some()); + + let detail = detail.unwrap(); + assert_eq!(detail.matching_result.id, saved_result.id); + assert_eq!(detail.segment_results.len(), 1); + assert_eq!(detail.failed_segment_results.len(), 1); + + // 验证成功片段 + let segment_result = &detail.segment_results[0]; + assert_eq!(segment_result.track_segment_name, "测试轨道片段"); + assert_eq!(segment_result.material_name, "测试素材"); + assert_eq!(segment_result.model_name, Some("测试模特".to_string())); + assert_eq!(segment_result.match_score, 0.95); + + // 验证失败片段 + let failed_segment = &detail.failed_segment_results[0]; + assert_eq!(failed_segment.track_segment_name, "失败轨道片段"); + assert_eq!(failed_segment.failure_reason, "没有找到匹配的素材"); + } + + #[tokio::test] + async fn test_get_project_matching_results() { + let database = create_test_database().await; + let repository = Arc::new(TemplateMatchingResultRepository::new(database)); + let service = TemplateMatchingResultService::new(repository); + + let project_id = "test-project-1"; + + // 保存两个匹配结果 + let mut service_result1 = create_test_service_matching_result(); + service_result1.project_id = project_id.to_string(); + + let mut service_result2 = create_test_service_matching_result(); + service_result2.project_id = project_id.to_string(); + service_result2.template_id = "template-2".to_string(); + + service.save_matching_result(&service_result1, "结果1".to_string(), None, 1000).await.unwrap(); + service.save_matching_result(&service_result2, "结果2".to_string(), None, 1500).await.unwrap(); + + // 获取项目的匹配结果 + let results = service.get_project_matching_results(project_id).await; + assert!(results.is_ok()); + + let results = results.unwrap(); + assert_eq!(results.len(), 2); + assert!(results.iter().all(|r| r.project_id == project_id)); + } + + #[tokio::test] + async fn test_delete_matching_result() { + let database = create_test_database().await; + let repository = Arc::new(TemplateMatchingResultRepository::new(database)); + let service = TemplateMatchingResultService::new(repository); + + // 保存一个匹配结果 + let service_result = create_test_service_matching_result(); + let saved_result = service.save_matching_result( + &service_result, + "待删除结果".to_string(), + None, + 1000, + ).await.unwrap(); + + // 删除结果 + let delete_result = service.delete_matching_result(&saved_result.id).await; + assert!(delete_result.is_ok()); + assert!(delete_result.unwrap()); + + // 验证已删除 + let detail_result = service.get_matching_result_detail(&saved_result.id).await; + assert!(detail_result.is_ok()); + assert!(detail_result.unwrap().is_none()); + } + + #[tokio::test] + async fn test_soft_delete_matching_result() { + let database = create_test_database().await; + let repository = Arc::new(TemplateMatchingResultRepository::new(database)); + let service = TemplateMatchingResultService::new(repository); + + // 保存一个匹配结果 + let service_result = create_test_service_matching_result(); + let saved_result = service.save_matching_result( + &service_result, + "待软删除结果".to_string(), + None, + 1000, + ).await.unwrap(); + + // 软删除结果 + let delete_result = service.soft_delete_matching_result(&saved_result.id).await; + assert!(delete_result.is_ok()); + assert!(delete_result.unwrap()); + + // 验证在列表中不可见(软删除) + let list_result = service.list_matching_results(TemplateMatchingResultQueryOptions::default()).await; + assert!(list_result.is_ok()); + let results = list_result.unwrap(); + assert!(!results.iter().any(|r| r.id == saved_result.id)); + } + + #[tokio::test] + async fn test_update_matching_result_info() { + let database = create_test_database().await; + let repository = Arc::new(TemplateMatchingResultRepository::new(database)); + let service = TemplateMatchingResultService::new(repository); + + // 保存一个匹配结果 + let service_result = create_test_service_matching_result(); + let saved_result = service.save_matching_result( + &service_result, + "原始名称".to_string(), + Some("原始描述".to_string()), + 1000, + ).await.unwrap(); + + // 更新信息 + let new_name = "更新后的名称".to_string(); + let new_description = "更新后的描述".to_string(); + + let update_result = service.update_matching_result_info( + &saved_result.id, + Some(new_name.clone()), + Some(new_description.clone()), + ).await; + + assert!(update_result.is_ok()); + let updated_result = update_result.unwrap(); + assert!(updated_result.is_some()); + + let updated_result = updated_result.unwrap(); + assert_eq!(updated_result.result_name, new_name); + assert_eq!(updated_result.description, Some(new_description)); + } + + #[tokio::test] + async fn test_set_quality_score() { + let database = create_test_database().await; + let repository = Arc::new(TemplateMatchingResultRepository::new(database)); + let service = TemplateMatchingResultService::new(repository); + + // 保存一个匹配结果 + let service_result = create_test_service_matching_result(); + let saved_result = service.save_matching_result( + &service_result, + "质量评分测试".to_string(), + None, + 1000, + ).await.unwrap(); + + // 设置质量评分 + let quality_score = 4.5; + let score_result = service.set_quality_score(&saved_result.id, quality_score).await; + + assert!(score_result.is_ok()); + let updated_result = score_result.unwrap(); + assert!(updated_result.is_some()); + + let updated_result = updated_result.unwrap(); + assert_eq!(updated_result.quality_score, Some(quality_score)); + } + + #[tokio::test] + async fn test_get_matching_statistics() { + let database = create_test_database().await; + let repository = Arc::new(TemplateMatchingResultRepository::new(database)); + let service = TemplateMatchingResultService::new(repository); + + let project_id = "stats-test-project"; + + // 保存多个匹配结果 + for i in 0..3 { + let mut service_result = create_test_service_matching_result(); + service_result.project_id = project_id.to_string(); + service_result.template_id = format!("template-{}", i); + + service.save_matching_result( + &service_result, + format!("结果{}", i), + None, + 1000 + i * 100, + ).await.unwrap(); + } + + // 获取统计信息 + let stats_result = service.get_matching_statistics(Some(project_id)).await; + assert!(stats_result.is_ok()); + + let stats = stats_result.unwrap(); + assert_eq!(stats.total_results, 3); + assert_eq!(stats.successful_results, 3); // 所有结果都是部分成功 + assert_eq!(stats.total_segments, 6); // 每个结果2个片段 + assert_eq!(stats.matched_segments, 3); // 每个结果1个匹配片段 + assert_eq!(stats.total_materials, 3); // 每个结果1个素材 + assert_eq!(stats.total_models, 3); // 每个结果1个模特 + assert_eq!(stats.average_success_rate, 50.0); // 平均成功率50% + } + + #[tokio::test] + async fn test_list_matching_results_with_filters() { + let database = create_test_database().await; + let repository = Arc::new(TemplateMatchingResultRepository::new(database)); + let service = TemplateMatchingResultService::new(repository); + + // 保存不同状态的匹配结果 + let mut service_result1 = create_test_service_matching_result(); + service_result1.statistics.success_rate = 100.0; // 成功 + service_result1.statistics.matched_segments = 2; + service_result1.statistics.failed_segments = 0; + + let service_result2 = create_test_service_matching_result(); // 部分成功 + + service.save_matching_result(&service_result1, "成功结果".to_string(), None, 1000).await.unwrap(); + service.save_matching_result(&service_result2, "部分成功结果".to_string(), None, 1500).await.unwrap(); + + // 测试状态过滤 + let options = TemplateMatchingResultQueryOptions { + status: Some(MatchingResultStatus::Success), + ..Default::default() + }; + + let results = service.list_matching_results(options).await.unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].status, MatchingResultStatus::Success); + + // 测试搜索关键词 + let options = TemplateMatchingResultQueryOptions { + search_keyword: Some("成功".to_string()), + ..Default::default() + }; + + let results = service.list_matching_results(options).await.unwrap(); + assert_eq!(results.len(), 2); // 两个结果都包含"成功" + + // 测试分页 + let options = TemplateMatchingResultQueryOptions { + limit: Some(1), + offset: Some(0), + ..Default::default() + }; + + let results = service.list_matching_results(options).await.unwrap(); + assert_eq!(results.len(), 1); + } +} diff --git a/apps/desktop/src-tauri/src/data/models/mod.rs b/apps/desktop/src-tauri/src/data/models/mod.rs index 3e26336..5739240 100644 --- a/apps/desktop/src-tauri/src/data/models/mod.rs +++ b/apps/desktop/src-tauri/src/data/models/mod.rs @@ -6,3 +6,4 @@ pub mod ai_classification; pub mod video_classification; pub mod template; pub mod project_template_binding; +pub mod template_matching_result; diff --git a/apps/desktop/src-tauri/src/data/models/template_matching_result.rs b/apps/desktop/src-tauri/src/data/models/template_matching_result.rs new file mode 100644 index 0000000..bd7f82e --- /dev/null +++ b/apps/desktop/src-tauri/src/data/models/template_matching_result.rs @@ -0,0 +1,309 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; + +/// 模板匹配结果实体模型 +/// 遵循 Tauri 开发规范的数据模型设计原则 +/// 保存一次完整的模板匹配结果,包含所有匹配的片段信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TemplateMatchingResult { + pub id: String, + pub project_id: String, + pub template_id: String, + pub binding_id: String, + pub result_name: String, // 用户自定义的结果名称 + pub description: Option, + pub total_segments: u32, + pub matched_segments: u32, + pub failed_segments: u32, + pub success_rate: f64, + pub used_materials: u32, + pub used_models: u32, + pub matching_duration_ms: u64, // 匹配耗时(毫秒) + pub quality_score: Option, // 匹配质量评分 + pub status: MatchingResultStatus, + pub metadata: Option, // JSON格式的额外元数据 + pub created_at: DateTime, + pub updated_at: DateTime, + pub is_active: bool, +} + +/// 匹配结果状态 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum MatchingResultStatus { + /// 匹配成功 + Success, + /// 部分匹配成功 + PartialSuccess, + /// 匹配失败 + Failed, + /// 匹配被取消 + Cancelled, +} + +impl Default for MatchingResultStatus { + fn default() -> Self { + Self::Success + } +} + +/// 匹配片段结果实体模型 +/// 保存每个成功匹配的片段详细信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MatchingSegmentResult { + pub id: String, + pub matching_result_id: String, // 关联的匹配结果ID + pub track_segment_id: String, // 模板轨道片段ID + pub track_segment_name: String, + pub material_segment_id: String, // 匹配到的素材片段ID + pub material_id: String, // 素材ID + pub material_name: String, + pub model_id: Option, // 模特ID + pub model_name: Option, + pub match_score: f64, // 匹配评分 + pub match_reason: String, // 匹配原因 + pub segment_duration: u64, // 片段时长(微秒) + pub start_time: u64, // 在模板中的开始时间(微秒) + pub end_time: u64, // 在模板中的结束时间(微秒) + pub properties: Option, // JSON格式的片段属性 + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// 匹配失败片段结果实体模型 +/// 保存匹配失败的片段信息,用于分析和改进 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MatchingFailedSegmentResult { + pub id: String, + pub matching_result_id: String, // 关联的匹配结果ID + pub track_segment_id: String, // 模板轨道片段ID + pub track_segment_name: String, + pub matching_rule_type: String, // 匹配规则类型 + pub matching_rule_data: Option, // 匹配规则数据(JSON格式) + pub failure_reason: String, // 失败原因 + pub failure_details: Option, // 失败详情(JSON格式) + pub segment_duration: u64, // 片段时长(微秒) + pub start_time: u64, // 在模板中的开始时间(微秒) + pub end_time: u64, // 在模板中的结束时间(微秒) + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl TemplateMatchingResult { + /// 创建新的模板匹配结果实例 + pub fn new( + id: String, + project_id: String, + template_id: String, + binding_id: String, + result_name: String, + ) -> Self { + let now = Utc::now(); + Self { + id, + project_id, + template_id, + binding_id, + result_name, + description: None, + total_segments: 0, + matched_segments: 0, + failed_segments: 0, + success_rate: 0.0, + used_materials: 0, + used_models: 0, + matching_duration_ms: 0, + quality_score: None, + status: MatchingResultStatus::default(), + metadata: None, + created_at: now, + updated_at: now, + is_active: true, + } + } + + /// 更新匹配统计信息 + pub fn update_statistics( + &mut self, + total_segments: u32, + matched_segments: u32, + failed_segments: u32, + used_materials: u32, + used_models: u32, + matching_duration_ms: u64, + ) { + self.total_segments = total_segments; + self.matched_segments = matched_segments; + self.failed_segments = failed_segments; + self.success_rate = if total_segments > 0 { + (matched_segments as f64) / (total_segments as f64) * 100.0 + } else { + 0.0 + }; + self.used_materials = used_materials; + self.used_models = used_models; + self.matching_duration_ms = matching_duration_ms; + self.updated_at = Utc::now(); + + // 根据成功率设置状态 + self.status = if self.success_rate >= 100.0 { + MatchingResultStatus::Success + } else if self.success_rate > 0.0 { + MatchingResultStatus::PartialSuccess + } else { + MatchingResultStatus::Failed + }; + } + + /// 设置质量评分 + pub fn set_quality_score(&mut self, score: f64) { + self.quality_score = Some(score); + self.updated_at = Utc::now(); + } + + /// 设置描述 + pub fn set_description(&mut self, description: String) { + self.description = Some(description); + self.updated_at = Utc::now(); + } + + /// 设置元数据 + pub fn set_metadata(&mut self, metadata: String) { + self.metadata = Some(metadata); + self.updated_at = Utc::now(); + } + + /// 检查是否为成功的匹配结果 + pub fn is_successful(&self) -> bool { + matches!(self.status, MatchingResultStatus::Success | MatchingResultStatus::PartialSuccess) + } + + /// 获取状态显示名称 + pub fn status_display_name(&self) -> String { + match self.status { + MatchingResultStatus::Success => "匹配成功".to_string(), + MatchingResultStatus::PartialSuccess => "部分成功".to_string(), + MatchingResultStatus::Failed => "匹配失败".to_string(), + MatchingResultStatus::Cancelled => "已取消".to_string(), + } + } +} + +impl MatchingSegmentResult { + /// 创建新的匹配片段结果实例 + pub fn new( + id: String, + matching_result_id: String, + track_segment_id: String, + track_segment_name: String, + material_segment_id: String, + material_id: String, + material_name: String, + match_score: f64, + match_reason: String, + segment_duration: u64, + start_time: u64, + end_time: u64, + ) -> Self { + let now = Utc::now(); + Self { + id, + matching_result_id, + track_segment_id, + track_segment_name, + material_segment_id, + material_id, + material_name, + model_id: None, + model_name: None, + match_score, + match_reason, + segment_duration, + start_time, + end_time, + properties: None, + created_at: now, + updated_at: now, + } + } + + /// 设置模特信息 + pub fn set_model_info(&mut self, model_id: String, model_name: String) { + self.model_id = Some(model_id); + self.model_name = Some(model_name); + self.updated_at = Utc::now(); + } + + /// 设置片段属性 + pub fn set_properties(&mut self, properties: String) { + self.properties = Some(properties); + self.updated_at = Utc::now(); + } +} + +impl MatchingFailedSegmentResult { + /// 创建新的匹配失败片段结果实例 + pub fn new( + id: String, + matching_result_id: String, + track_segment_id: String, + track_segment_name: String, + matching_rule_type: String, + failure_reason: String, + segment_duration: u64, + start_time: u64, + end_time: u64, + ) -> Self { + let now = Utc::now(); + Self { + id, + matching_result_id, + track_segment_id, + track_segment_name, + matching_rule_type, + matching_rule_data: None, + failure_reason, + failure_details: None, + segment_duration, + start_time, + end_time, + created_at: now, + updated_at: now, + } + } + + /// 设置匹配规则数据 + pub fn set_matching_rule_data(&mut self, rule_data: String) { + self.matching_rule_data = Some(rule_data); + self.updated_at = Utc::now(); + } + + /// 设置失败详情 + pub fn set_failure_details(&mut self, details: String) { + self.failure_details = Some(details); + self.updated_at = Utc::now(); + } +} + +/// 创建模板匹配结果请求 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateTemplateMatchingResultRequest { + pub project_id: String, + pub template_id: String, + pub binding_id: String, + pub result_name: String, + pub description: Option, +} + +/// 模板匹配结果查询选项 +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct TemplateMatchingResultQueryOptions { + pub project_id: Option, + pub template_id: Option, + pub binding_id: Option, + pub status: Option, + pub limit: Option, + pub offset: Option, + pub search_keyword: Option, + pub sort_by: Option, // "created_at", "success_rate", "result_name" + pub sort_order: Option, // "asc", "desc" +} diff --git a/apps/desktop/src-tauri/src/data/repositories/mod.rs b/apps/desktop/src-tauri/src/data/repositories/mod.rs index 77aea09..3d57589 100644 --- a/apps/desktop/src-tauri/src/data/repositories/mod.rs +++ b/apps/desktop/src-tauri/src/data/repositories/mod.rs @@ -4,3 +4,4 @@ pub mod model_repository; pub mod ai_classification_repository; pub mod video_classification_repository; pub mod project_template_binding_repository; +pub mod template_matching_result_repository; diff --git a/apps/desktop/src-tauri/src/data/repositories/template_matching_result_repository.rs b/apps/desktop/src-tauri/src/data/repositories/template_matching_result_repository.rs new file mode 100644 index 0000000..1f5cc26 --- /dev/null +++ b/apps/desktop/src-tauri/src/data/repositories/template_matching_result_repository.rs @@ -0,0 +1,558 @@ +use rusqlite::{Result, Row}; +use std::sync::Arc; +use chrono::{DateTime, Utc}; +use crate::infrastructure::database::Database; + +use crate::data::models::template_matching_result::{ + TemplateMatchingResult, MatchingSegmentResult, MatchingFailedSegmentResult, + MatchingResultStatus, CreateTemplateMatchingResultRequest, + TemplateMatchingResultQueryOptions, +}; + +/// 模板匹配结果数据库操作仓储 +/// 遵循 Tauri 开发规范的数据访问层设计原则 +pub struct TemplateMatchingResultRepository { + database: Arc, +} + +impl TemplateMatchingResultRepository { + /// 创建新的模板匹配结果仓储实例 + pub fn new(database: Arc) -> Self { + Self { database } + } + + /// 创建模板匹配结果 + pub fn create(&self, request: CreateTemplateMatchingResultRequest) -> Result { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + let result = TemplateMatchingResult::new( + uuid::Uuid::new_v4().to_string(), + request.project_id, + request.template_id, + request.binding_id, + request.result_name, + ); + + let mut final_result = result; + if let Some(desc) = request.description { + final_result.set_description(desc); + } + + conn.execute( + "INSERT INTO template_matching_results ( + id, project_id, template_id, binding_id, result_name, description, + total_segments, matched_segments, failed_segments, success_rate, + used_materials, used_models, matching_duration_ms, quality_score, + status, metadata, created_at, updated_at, is_active + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19)", + rusqlite::params![ + &final_result.id, + &final_result.project_id, + &final_result.template_id, + &final_result.binding_id, + &final_result.result_name, + &final_result.description, + &final_result.total_segments.to_string(), + &final_result.matched_segments.to_string(), + &final_result.failed_segments.to_string(), + &final_result.success_rate.to_string(), + &final_result.used_materials.to_string(), + &final_result.used_models.to_string(), + &final_result.matching_duration_ms.to_string(), + &final_result.quality_score.map(|s| s.to_string()), + &serde_json::to_string(&final_result.status).unwrap(), + &final_result.metadata, + &final_result.created_at.to_rfc3339(), + &final_result.updated_at.to_rfc3339(), + &(final_result.is_active as i32).to_string(), + ], + )?; + + Ok(final_result) + } + + /// 根据ID获取模板匹配结果 + pub fn get_by_id(&self, id: &str) -> Result> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + let mut stmt = conn.prepare( + "SELECT id, project_id, template_id, binding_id, result_name, description, + total_segments, matched_segments, failed_segments, success_rate, + used_materials, used_models, matching_duration_ms, quality_score, + status, metadata, created_at, updated_at, is_active + FROM template_matching_results WHERE id = ?1" + )?; + + let result_iter = stmt.query_map([id], |row| { + self.row_to_matching_result(row) + })?; + + for result in result_iter { + return Ok(Some(result?)); + } + + Ok(None) + } + + /// 更新模板匹配结果 + pub fn update(&self, result: &TemplateMatchingResult) -> Result<()> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + conn.execute( + "UPDATE template_matching_results SET + result_name = ?1, description = ?2, total_segments = ?3, + matched_segments = ?4, failed_segments = ?5, success_rate = ?6, + used_materials = ?7, used_models = ?8, matching_duration_ms = ?9, + quality_score = ?10, status = ?11, metadata = ?12, updated_at = ?13 + WHERE id = ?14", + rusqlite::params![ + &result.result_name, + &result.description, + &result.total_segments.to_string(), + &result.matched_segments.to_string(), + &result.failed_segments.to_string(), + &result.success_rate.to_string(), + &result.used_materials.to_string(), + &result.used_models.to_string(), + &result.matching_duration_ms.to_string(), + &result.quality_score.map(|s| s.to_string()), + &serde_json::to_string(&result.status).unwrap(), + &result.metadata, + &result.updated_at.to_rfc3339(), + &result.id, + ], + )?; + + Ok(()) + } + + /// 删除模板匹配结果 + pub fn delete(&self, id: &str) -> Result { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + let rows_affected = conn.execute( + "DELETE FROM template_matching_results WHERE id = ?1", + [id], + )?; + + Ok(rows_affected > 0) + } + + /// 软删除模板匹配结果 + pub fn soft_delete(&self, id: &str) -> Result { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + let rows_affected = conn.execute( + "UPDATE template_matching_results SET is_active = 0, updated_at = ?1 WHERE id = ?2", + [&Utc::now().to_rfc3339(), id], + )?; + + Ok(rows_affected > 0) + } + + /// 查询模板匹配结果列表 + pub fn list(&self, options: TemplateMatchingResultQueryOptions) -> Result> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + let mut query = "SELECT id, project_id, template_id, binding_id, result_name, description, + total_segments, matched_segments, failed_segments, success_rate, + used_materials, used_models, matching_duration_ms, quality_score, + status, metadata, created_at, updated_at, is_active + FROM template_matching_results WHERE is_active = 1".to_string(); + let mut params: Vec = Vec::new(); + + // 添加查询条件 + if let Some(project_id) = &options.project_id { + query.push_str(" AND project_id = ?"); + params.push(project_id.clone()); + } + + if let Some(template_id) = &options.template_id { + query.push_str(" AND template_id = ?"); + params.push(template_id.clone()); + } + + if let Some(binding_id) = &options.binding_id { + query.push_str(" AND binding_id = ?"); + params.push(binding_id.clone()); + } + + if let Some(status) = &options.status { + query.push_str(" AND status = ?"); + params.push(serde_json::to_string(status).unwrap()); + } + + if let Some(keyword) = &options.search_keyword { + query.push_str(" AND (result_name LIKE ? OR description LIKE ?)"); + let search_pattern = format!("%{}%", keyword); + params.push(search_pattern.clone()); + params.push(search_pattern); + } + + // 添加排序 + let sort_by = options.sort_by.as_deref().unwrap_or("created_at"); + let sort_order = options.sort_order.as_deref().unwrap_or("desc"); + query.push_str(&format!(" ORDER BY {} {}", sort_by, sort_order)); + + // 添加分页 + if let Some(limit) = options.limit { + query.push_str(" LIMIT ?"); + params.push(limit.to_string()); + + if let Some(offset) = options.offset { + query.push_str(" OFFSET ?"); + params.push(offset.to_string()); + } + } + + let mut stmt = conn.prepare(&query)?; + let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p as &dyn rusqlite::ToSql).collect(); + + let result_iter = stmt.query_map(param_refs.as_slice(), |row| { + self.row_to_matching_result(row) + })?; + + let mut results = Vec::new(); + for result in result_iter { + results.push(result?); + } + + Ok(results) + } + + /// 根据项目ID获取匹配结果列表 + pub fn get_by_project_id(&self, project_id: &str) -> Result> { + let options = TemplateMatchingResultQueryOptions { + project_id: Some(project_id.to_string()), + ..Default::default() + }; + self.list(options) + } + + /// 根据模板ID获取匹配结果列表 + pub fn get_by_template_id(&self, template_id: &str) -> Result> { + let options = TemplateMatchingResultQueryOptions { + template_id: Some(template_id.to_string()), + ..Default::default() + }; + self.list(options) + } + + /// 根据绑定ID获取匹配结果列表 + pub fn get_by_binding_id(&self, binding_id: &str) -> Result> { + let options = TemplateMatchingResultQueryOptions { + binding_id: Some(binding_id.to_string()), + ..Default::default() + }; + self.list(options) + } + + // ===== 匹配片段结果管理 ===== + + /// 创建匹配片段结果 + pub fn create_segment_result(&self, segment_result: &MatchingSegmentResult) -> Result<()> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + conn.execute( + "INSERT INTO matching_segment_results ( + id, matching_result_id, track_segment_id, track_segment_name, + material_segment_id, material_id, material_name, model_id, model_name, + match_score, match_reason, segment_duration, start_time, end_time, + properties, created_at, updated_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)", + rusqlite::params![ + &segment_result.id, + &segment_result.matching_result_id, + &segment_result.track_segment_id, + &segment_result.track_segment_name, + &segment_result.material_segment_id, + &segment_result.material_id, + &segment_result.material_name, + &segment_result.model_id, + &segment_result.model_name, + &segment_result.match_score.to_string(), + &segment_result.match_reason, + &segment_result.segment_duration.to_string(), + &segment_result.start_time.to_string(), + &segment_result.end_time.to_string(), + &segment_result.properties, + &segment_result.created_at.to_rfc3339(), + &segment_result.updated_at.to_rfc3339(), + ], + )?; + + Ok(()) + } + + /// 批量创建匹配片段结果 + pub fn create_segment_results(&self, segment_results: &[MatchingSegmentResult]) -> Result<()> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + let tx = conn.unchecked_transaction()?; + + for segment_result in segment_results { + tx.execute( + "INSERT INTO matching_segment_results ( + id, matching_result_id, track_segment_id, track_segment_name, + material_segment_id, material_id, material_name, model_id, model_name, + match_score, match_reason, segment_duration, start_time, end_time, + properties, created_at, updated_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)", + rusqlite::params![ + &segment_result.id, + &segment_result.matching_result_id, + &segment_result.track_segment_id, + &segment_result.track_segment_name, + &segment_result.material_segment_id, + &segment_result.material_id, + &segment_result.material_name, + &segment_result.model_id, + &segment_result.model_name, + &segment_result.match_score.to_string(), + &segment_result.match_reason, + &segment_result.segment_duration.to_string(), + &segment_result.start_time.to_string(), + &segment_result.end_time.to_string(), + &segment_result.properties, + &segment_result.created_at.to_rfc3339(), + &segment_result.updated_at.to_rfc3339(), + ], + )?; + } + + tx.commit()?; + Ok(()) + } + + /// 获取匹配结果的所有成功片段 + pub fn get_segment_results_by_matching_result_id(&self, matching_result_id: &str) -> Result> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + let mut stmt = conn.prepare( + "SELECT id, matching_result_id, track_segment_id, track_segment_name, + material_segment_id, material_id, material_name, model_id, model_name, + match_score, match_reason, segment_duration, start_time, end_time, + properties, created_at, updated_at + FROM matching_segment_results WHERE matching_result_id = ?1 + ORDER BY start_time ASC" + )?; + + let segment_iter = stmt.query_map([matching_result_id], |row| { + self.row_to_segment_result(row) + })?; + + let mut segments = Vec::new(); + for segment in segment_iter { + segments.push(segment?); + } + + Ok(segments) + } + + // ===== 匹配失败片段结果管理 ===== + + /// 创建匹配失败片段结果 + pub fn create_failed_segment_result(&self, failed_segment: &MatchingFailedSegmentResult) -> Result<()> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + conn.execute( + "INSERT INTO matching_failed_segment_results ( + id, matching_result_id, track_segment_id, track_segment_name, + matching_rule_type, matching_rule_data, failure_reason, failure_details, + segment_duration, start_time, end_time, created_at, updated_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + rusqlite::params![ + &failed_segment.id, + &failed_segment.matching_result_id, + &failed_segment.track_segment_id, + &failed_segment.track_segment_name, + &failed_segment.matching_rule_type, + &failed_segment.matching_rule_data, + &failed_segment.failure_reason, + &failed_segment.failure_details, + &failed_segment.segment_duration.to_string(), + &failed_segment.start_time.to_string(), + &failed_segment.end_time.to_string(), + &failed_segment.created_at.to_rfc3339(), + &failed_segment.updated_at.to_rfc3339(), + ], + )?; + + Ok(()) + } + + /// 批量创建匹配失败片段结果 + pub fn create_failed_segment_results(&self, failed_segments: &[MatchingFailedSegmentResult]) -> Result<()> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + let tx = conn.unchecked_transaction()?; + + for failed_segment in failed_segments { + tx.execute( + "INSERT INTO matching_failed_segment_results ( + id, matching_result_id, track_segment_id, track_segment_name, + matching_rule_type, matching_rule_data, failure_reason, failure_details, + segment_duration, start_time, end_time, created_at, updated_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + rusqlite::params![ + &failed_segment.id, + &failed_segment.matching_result_id, + &failed_segment.track_segment_id, + &failed_segment.track_segment_name, + &failed_segment.matching_rule_type, + &failed_segment.matching_rule_data, + &failed_segment.failure_reason, + &failed_segment.failure_details, + &failed_segment.segment_duration.to_string(), + &failed_segment.start_time.to_string(), + &failed_segment.end_time.to_string(), + &failed_segment.created_at.to_rfc3339(), + &failed_segment.updated_at.to_rfc3339(), + ], + )?; + } + + tx.commit()?; + Ok(()) + } + + /// 获取匹配结果的所有失败片段 + pub fn get_failed_segment_results_by_matching_result_id(&self, matching_result_id: &str) -> Result> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + + let mut stmt = conn.prepare( + "SELECT id, matching_result_id, track_segment_id, track_segment_name, + matching_rule_type, matching_rule_data, failure_reason, failure_details, + segment_duration, start_time, end_time, created_at, updated_at + FROM matching_failed_segment_results WHERE matching_result_id = ?1 + ORDER BY start_time ASC" + )?; + + let failed_segment_iter = stmt.query_map([matching_result_id], |row| { + self.row_to_failed_segment_result(row) + })?; + + let mut failed_segments = Vec::new(); + for failed_segment in failed_segment_iter { + failed_segments.push(failed_segment?); + } + + Ok(failed_segments) + } + + /// 将数据库行转换为模板匹配结果实体 + fn row_to_matching_result(&self, row: &Row) -> Result { + let status_str: String = row.get("status")?; + let status: MatchingResultStatus = serde_json::from_str(&status_str) + .unwrap_or(MatchingResultStatus::Success); + + let quality_score_str: Option = row.get("quality_score")?; + let quality_score = quality_score_str.and_then(|s| s.parse::().ok()); + + let created_at_str: String = row.get("created_at")?; + let updated_at_str: String = row.get("updated_at")?; + let created_at = DateTime::parse_from_rfc3339(&created_at_str) + .unwrap_or_else(|_| Utc::now().into()) + .with_timezone(&Utc); + let updated_at = DateTime::parse_from_rfc3339(&updated_at_str) + .unwrap_or_else(|_| Utc::now().into()) + .with_timezone(&Utc); + + let is_active_str: String = row.get("is_active")?; + let is_active = is_active_str.parse::().unwrap_or(1) != 0; + + Ok(TemplateMatchingResult { + id: row.get("id")?, + project_id: row.get("project_id")?, + template_id: row.get("template_id")?, + binding_id: row.get("binding_id")?, + result_name: row.get("result_name")?, + description: row.get("description")?, + total_segments: row.get::<_, String>("total_segments")?.parse().unwrap_or(0), + matched_segments: row.get::<_, String>("matched_segments")?.parse().unwrap_or(0), + failed_segments: row.get::<_, String>("failed_segments")?.parse().unwrap_or(0), + success_rate: row.get::<_, String>("success_rate")?.parse().unwrap_or(0.0), + used_materials: row.get::<_, String>("used_materials")?.parse().unwrap_or(0), + used_models: row.get::<_, String>("used_models")?.parse().unwrap_or(0), + matching_duration_ms: row.get::<_, String>("matching_duration_ms")?.parse().unwrap_or(0), + quality_score, + status, + metadata: row.get("metadata")?, + created_at, + updated_at, + is_active, + }) + } + + /// 将数据库行转换为匹配片段结果实体 + fn row_to_segment_result(&self, row: &Row) -> Result { + let created_at_str: String = row.get("created_at")?; + let updated_at_str: String = row.get("updated_at")?; + let created_at = DateTime::parse_from_rfc3339(&created_at_str) + .unwrap_or_else(|_| Utc::now().into()) + .with_timezone(&Utc); + let updated_at = DateTime::parse_from_rfc3339(&updated_at_str) + .unwrap_or_else(|_| Utc::now().into()) + .with_timezone(&Utc); + + Ok(MatchingSegmentResult { + id: row.get("id")?, + matching_result_id: row.get("matching_result_id")?, + track_segment_id: row.get("track_segment_id")?, + track_segment_name: row.get("track_segment_name")?, + material_segment_id: row.get("material_segment_id")?, + material_id: row.get("material_id")?, + material_name: row.get("material_name")?, + model_id: row.get("model_id")?, + model_name: row.get("model_name")?, + match_score: row.get::<_, String>("match_score")?.parse().unwrap_or(0.0), + match_reason: row.get("match_reason")?, + segment_duration: row.get::<_, String>("segment_duration")?.parse().unwrap_or(0), + start_time: row.get::<_, String>("start_time")?.parse().unwrap_or(0), + end_time: row.get::<_, String>("end_time")?.parse().unwrap_or(0), + properties: row.get("properties")?, + created_at, + updated_at, + }) + } + + /// 将数据库行转换为匹配失败片段结果实体 + fn row_to_failed_segment_result(&self, row: &Row) -> Result { + let created_at_str: String = row.get("created_at")?; + let updated_at_str: String = row.get("updated_at")?; + let created_at = DateTime::parse_from_rfc3339(&created_at_str) + .unwrap_or_else(|_| Utc::now().into()) + .with_timezone(&Utc); + let updated_at = DateTime::parse_from_rfc3339(&updated_at_str) + .unwrap_or_else(|_| Utc::now().into()) + .with_timezone(&Utc); + + Ok(MatchingFailedSegmentResult { + id: row.get("id")?, + matching_result_id: row.get("matching_result_id")?, + track_segment_id: row.get("track_segment_id")?, + track_segment_name: row.get("track_segment_name")?, + matching_rule_type: row.get("matching_rule_type")?, + matching_rule_data: row.get("matching_rule_data")?, + failure_reason: row.get("failure_reason")?, + failure_details: row.get("failure_details")?, + segment_duration: row.get::<_, String>("segment_duration")?.parse().unwrap_or(0), + start_time: row.get::<_, String>("start_time")?.parse().unwrap_or(0), + end_time: row.get::<_, String>("end_time")?.parse().unwrap_or(0), + created_at, + updated_at, + }) + } +} diff --git a/apps/desktop/src-tauri/src/infrastructure/database.rs b/apps/desktop/src-tauri/src/infrastructure/database.rs index 2ac2409..c1f2260 100644 --- a/apps/desktop/src-tauri/src/infrastructure/database.rs +++ b/apps/desktop/src-tauri/src/infrastructure/database.rs @@ -671,7 +671,83 @@ impl Database { [], )?; + // 创建模板匹配结果表 + conn.execute( + "CREATE TABLE IF NOT EXISTS template_matching_results ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + template_id TEXT NOT NULL, + binding_id TEXT NOT NULL, + result_name TEXT NOT NULL, + description TEXT, + total_segments INTEGER NOT NULL DEFAULT 0, + matched_segments INTEGER NOT NULL DEFAULT 0, + failed_segments INTEGER NOT NULL DEFAULT 0, + success_rate REAL NOT NULL DEFAULT 0.0, + used_materials INTEGER NOT NULL DEFAULT 0, + used_models INTEGER NOT NULL DEFAULT 0, + matching_duration_ms INTEGER NOT NULL DEFAULT 0, + quality_score REAL, + status TEXT NOT NULL DEFAULT 'Success', + metadata TEXT, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + is_active INTEGER DEFAULT 1, + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (template_id) REFERENCES templates (id) ON DELETE CASCADE, + FOREIGN KEY (binding_id) REFERENCES project_template_bindings (id) ON DELETE CASCADE + )", + [], + )?; + // 创建匹配片段结果表 + conn.execute( + "CREATE TABLE IF NOT EXISTS matching_segment_results ( + id TEXT PRIMARY KEY, + matching_result_id TEXT NOT NULL, + track_segment_id TEXT NOT NULL, + track_segment_name TEXT NOT NULL, + material_segment_id TEXT NOT NULL, + material_id TEXT NOT NULL, + material_name TEXT NOT NULL, + model_id TEXT, + model_name TEXT, + match_score REAL NOT NULL, + match_reason TEXT NOT NULL, + segment_duration INTEGER NOT NULL, + start_time INTEGER NOT NULL, + end_time INTEGER NOT NULL, + properties TEXT, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + FOREIGN KEY (matching_result_id) REFERENCES template_matching_results (id) ON DELETE CASCADE, + FOREIGN KEY (material_segment_id) REFERENCES material_segments (id) ON DELETE CASCADE, + FOREIGN KEY (material_id) REFERENCES materials (id) ON DELETE CASCADE, + FOREIGN KEY (model_id) REFERENCES models (id) ON DELETE SET NULL + )", + [], + )?; + + // 创建匹配失败片段结果表 + conn.execute( + "CREATE TABLE IF NOT EXISTS matching_failed_segment_results ( + id TEXT PRIMARY KEY, + matching_result_id TEXT NOT NULL, + track_segment_id TEXT NOT NULL, + track_segment_name TEXT NOT NULL, + matching_rule_type TEXT NOT NULL, + matching_rule_data TEXT, + failure_reason TEXT NOT NULL, + failure_details TEXT, + segment_duration INTEGER NOT NULL, + start_time INTEGER NOT NULL, + end_time INTEGER NOT NULL, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + FOREIGN KEY (matching_result_id) REFERENCES template_matching_results (id) ON DELETE CASCADE + )", + [], + )?; // 创建性能监控表 conn.execute( @@ -748,7 +824,53 @@ impl Database { [], )?; + // 创建模板匹配结果表索引 + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_template_matching_results_project_id ON template_matching_results (project_id)", + [], + )?; + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_template_matching_results_template_id ON template_matching_results (template_id)", + [], + )?; + + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_template_matching_results_binding_id ON template_matching_results (binding_id)", + [], + )?; + + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_template_matching_results_status ON template_matching_results (status)", + [], + )?; + + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_template_matching_results_created_at ON template_matching_results (created_at)", + [], + )?; + + // 创建匹配片段结果表索引 + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_matching_segment_results_matching_result_id ON matching_segment_results (matching_result_id)", + [], + )?; + + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_matching_segment_results_material_id ON matching_segment_results (material_id)", + [], + )?; + + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_matching_segment_results_model_id ON matching_segment_results (model_id)", + [], + )?; + + // 创建匹配失败片段结果表索引 + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_matching_failed_segment_results_matching_result_id ON matching_failed_segment_results (matching_result_id)", + [], + )?; // 添加新字段(如果不存在)- 数据库迁移 let _ = conn.execute( diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index e81e224..30bc12c 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -191,8 +191,24 @@ pub fn run() { commands::project_template_binding_commands::check_project_template_binding_exists, // 素材匹配命令 commands::material_matching_commands::execute_material_matching, + commands::material_matching_commands::execute_material_matching_with_save, commands::material_matching_commands::get_project_material_stats_for_matching, commands::material_matching_commands::validate_template_binding_for_matching, + // 模板匹配结果命令 + commands::template_matching_result_commands::save_matching_result, + commands::template_matching_result_commands::get_matching_result_detail, + commands::template_matching_result_commands::get_project_matching_results, + commands::template_matching_result_commands::get_template_matching_results, + commands::template_matching_result_commands::get_binding_matching_results, + commands::template_matching_result_commands::list_matching_results, + commands::template_matching_result_commands::delete_matching_result, + commands::template_matching_result_commands::soft_delete_matching_result, + commands::template_matching_result_commands::update_matching_result_info, + commands::template_matching_result_commands::set_matching_result_quality_score, + commands::template_matching_result_commands::get_matching_statistics, + commands::template_matching_result_commands::create_matching_result, + commands::template_matching_result_commands::get_matching_result_by_id, + commands::template_matching_result_commands::get_matching_result_status_options, // MaterialSegment聚合视图命令 commands::material_segment_view_commands::get_project_segment_view, commands::material_segment_view_commands::get_project_segment_view_with_query, diff --git a/apps/desktop/src-tauri/src/presentation/commands/material_matching_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/material_matching_commands.rs index 77af50a..3463b18 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/material_matching_commands.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/material_matching_commands.rs @@ -10,10 +10,13 @@ use crate::business::services::material_matching_service::{ MaterialMatchingService, MaterialMatchingRequest, MaterialMatchingResult }; use crate::business::services::template_service::TemplateService; +use crate::business::services::template_matching_result_service::TemplateMatchingResultService; use crate::data::repositories::{ material_repository::MaterialRepository, video_classification_repository::VideoClassificationRepository, + template_matching_result_repository::TemplateMatchingResultRepository, }; +use crate::data::models::template_matching_result::TemplateMatchingResult; use crate::infrastructure::database::Database; /// 执行素材匹配 @@ -46,6 +49,43 @@ pub async fn execute_material_matching( .map_err(|e| e.to_string()) } +/// 执行素材匹配并自动保存结果 +#[command] +pub async fn execute_material_matching_with_save( + request: MaterialMatchingRequest, + result_name: String, + description: Option, + database: State<'_, Arc>, +) -> Result<(MaterialMatchingResult, Option), String> { + // 创建服务实例 + let material_repo = Arc::new( + MaterialRepository::new(database.inner().clone()) + .map_err(|e| format!("创建素材仓库失败: {}", e))? + ); + + let template_service = Arc::new(TemplateService::new(database.inner().clone())); + + let video_classification_repo = Arc::new( + VideoClassificationRepository::new(database.inner().clone()) + ); + + // 创建匹配结果服务 + let matching_result_repo = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone())); + let matching_result_service = Arc::new(TemplateMatchingResultService::new(matching_result_repo)); + + let matching_service = MaterialMatchingService::new_with_result_service( + material_repo, + template_service, + video_classification_repo, + matching_result_service, + ); + + // 执行匹配并保存结果 + matching_service.match_materials_and_save(request, result_name, description) + .await + .map_err(|e| e.to_string()) +} + /// 获取项目的可用素材统计信息 #[command] pub async fn get_project_material_stats_for_matching( diff --git a/apps/desktop/src-tauri/src/presentation/commands/mod.rs b/apps/desktop/src-tauri/src/presentation/commands/mod.rs index b68bce9..82cf8e8 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/mod.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/mod.rs @@ -12,3 +12,4 @@ pub mod test_commands; pub mod debug_commands; pub mod project_template_binding_commands; pub mod material_matching_commands; +pub mod template_matching_result_commands; diff --git a/apps/desktop/src-tauri/src/presentation/commands/template_matching_result_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/template_matching_result_commands.rs new file mode 100644 index 0000000..ef7dfba --- /dev/null +++ b/apps/desktop/src-tauri/src/presentation/commands/template_matching_result_commands.rs @@ -0,0 +1,215 @@ +/** + * 模板匹配结果相关的 Tauri 命令 + * 遵循 Tauri 开发规范的 API 设计原则 + */ + +use tauri::{command, State}; +use std::sync::Arc; + +use crate::business::services::template_matching_result_service::{ + TemplateMatchingResultService, TemplateMatchingResultDetail, MatchingStatistics, +}; +use crate::business::services::material_matching_service::MaterialMatchingResult as ServiceMatchingResult; +use crate::data::models::template_matching_result::{ + TemplateMatchingResult, CreateTemplateMatchingResultRequest, + TemplateMatchingResultQueryOptions, MatchingResultStatus, +}; +use crate::data::repositories::template_matching_result_repository::TemplateMatchingResultRepository; +use crate::infrastructure::database::Database; + +/// 保存匹配结果到数据库 +#[command] +pub async fn save_matching_result( + service_result: ServiceMatchingResult, + result_name: String, + description: Option, + matching_duration_ms: u64, + database: State<'_, Arc>, +) -> Result { + // 创建服务实例 + let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone())); + let service = TemplateMatchingResultService::new(repository); + + // 保存匹配结果 + service.save_matching_result(&service_result, result_name, description, matching_duration_ms) + .await + .map_err(|e| e.to_string()) +} + +/// 获取匹配结果详情 +#[command] +pub async fn get_matching_result_detail( + result_id: String, + database: State<'_, Arc>, +) -> Result, String> { + let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone())); + let service = TemplateMatchingResultService::new(repository); + + service.get_matching_result_detail(&result_id) + .await + .map_err(|e| e.to_string()) +} + +/// 获取项目的匹配结果列表 +#[command] +pub async fn get_project_matching_results( + project_id: String, + database: State<'_, Arc>, +) -> Result, String> { + let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone())); + let service = TemplateMatchingResultService::new(repository); + + service.get_project_matching_results(&project_id) + .await + .map_err(|e| e.to_string()) +} + +/// 获取模板的匹配结果列表 +#[command] +pub async fn get_template_matching_results( + template_id: String, + database: State<'_, Arc>, +) -> Result, String> { + let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone())); + let service = TemplateMatchingResultService::new(repository); + + service.get_template_matching_results(&template_id) + .await + .map_err(|e| e.to_string()) +} + +/// 获取绑定的匹配结果列表 +#[command] +pub async fn get_binding_matching_results( + binding_id: String, + database: State<'_, Arc>, +) -> Result, String> { + let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone())); + let service = TemplateMatchingResultService::new(repository); + + service.get_binding_matching_results(&binding_id) + .await + .map_err(|e| e.to_string()) +} + +/// 查询匹配结果列表 +#[command] +pub async fn list_matching_results( + options: TemplateMatchingResultQueryOptions, + database: State<'_, Arc>, +) -> Result, String> { + let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone())); + let service = TemplateMatchingResultService::new(repository); + + service.list_matching_results(options) + .await + .map_err(|e| e.to_string()) +} + +/// 删除匹配结果 +#[command] +pub async fn delete_matching_result( + result_id: String, + database: State<'_, Arc>, +) -> Result { + let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone())); + let service = TemplateMatchingResultService::new(repository); + + service.delete_matching_result(&result_id) + .await + .map_err(|e| e.to_string()) +} + +/// 软删除匹配结果 +#[command] +pub async fn soft_delete_matching_result( + result_id: String, + database: State<'_, Arc>, +) -> Result { + let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone())); + let service = TemplateMatchingResultService::new(repository); + + service.soft_delete_matching_result(&result_id) + .await + .map_err(|e| e.to_string()) +} + +/// 更新匹配结果信息 +#[command] +pub async fn update_matching_result_info( + result_id: String, + result_name: Option, + description: Option, + database: State<'_, Arc>, +) -> Result, String> { + let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone())); + let service = TemplateMatchingResultService::new(repository); + + service.update_matching_result_info(&result_id, result_name, description) + .await + .map_err(|e| e.to_string()) +} + +/// 设置匹配结果质量评分 +#[command] +pub async fn set_matching_result_quality_score( + result_id: String, + quality_score: f64, + database: State<'_, Arc>, +) -> Result, String> { + let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone())); + let service = TemplateMatchingResultService::new(repository); + + service.set_quality_score(&result_id, quality_score) + .await + .map_err(|e| e.to_string()) +} + +/// 获取匹配结果统计信息 +#[command] +pub async fn get_matching_statistics( + project_id: Option, + database: State<'_, Arc>, +) -> Result { + let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone())); + let service = TemplateMatchingResultService::new(repository); + + service.get_matching_statistics(project_id.as_deref()) + .await + .map_err(|e| e.to_string()) +} + +/// 创建匹配结果(手动创建,不通过匹配服务) +#[command] +pub async fn create_matching_result( + request: CreateTemplateMatchingResultRequest, + database: State<'_, Arc>, +) -> Result { + let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone())); + + repository.create(request) + .map_err(|e| e.to_string()) +} + +/// 根据ID获取匹配结果 +#[command] +pub async fn get_matching_result_by_id( + result_id: String, + database: State<'_, Arc>, +) -> Result, String> { + let repository = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone())); + + repository.get_by_id(&result_id) + .map_err(|e| e.to_string()) +} + +/// 获取匹配结果状态选项(用于前端下拉选择) +#[command] +pub async fn get_matching_result_status_options() -> Result, String> { + Ok(vec![ + ("Success".to_string(), "匹配成功".to_string()), + ("PartialSuccess".to_string(), "部分成功".to_string()), + ("Failed".to_string(), "匹配失败".to_string()), + ("Cancelled".to_string(), "已取消".to_string()), + ]) +} diff --git a/apps/desktop/src/components/DeleteConfirmDialog.tsx b/apps/desktop/src/components/DeleteConfirmDialog.tsx index 907158a..dc8e5d9 100644 --- a/apps/desktop/src/components/DeleteConfirmDialog.tsx +++ b/apps/desktop/src/components/DeleteConfirmDialog.tsx @@ -12,7 +12,7 @@ interface DeleteConfirmDialogProps { /** 要删除的项目名称 */ itemName?: string; /** 是否正在删除 */ - deleting: boolean; + deleting?: boolean; /** 确认删除回调 */ onConfirm: () => void; /** 取消回调 */ diff --git a/apps/desktop/src/components/TemplateMatchingResultCard.tsx b/apps/desktop/src/components/TemplateMatchingResultCard.tsx new file mode 100644 index 0000000..18c4cee --- /dev/null +++ b/apps/desktop/src/components/TemplateMatchingResultCard.tsx @@ -0,0 +1,198 @@ +import React from 'react'; +import { TemplateMatchingResult, MatchingResultStatus } from '../types/templateMatchingResult'; + +interface TemplateMatchingResultCardProps { + result: TemplateMatchingResult; + onViewDetail: () => void; + onDelete: () => void; + onEdit?: () => void; +} + +export const TemplateMatchingResultCard: React.FC = ({ + result, + onViewDetail, + onDelete, + onEdit, +}) => { + // 格式化时长显示 + const formatDuration = (ms: number): string => { + if (ms < 1000) return `${ms}ms`; + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; + return `${(ms / 60000).toFixed(1)}min`; + }; + + // 格式化日期显示 + const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); + }; + + // 获取状态样式 + const getStatusStyle = (status: MatchingResultStatus) => { + switch (status) { + case MatchingResultStatus.Success: + return 'bg-green-100 text-green-800 border-green-200'; + case MatchingResultStatus.PartialSuccess: + return 'bg-yellow-100 text-yellow-800 border-yellow-200'; + case MatchingResultStatus.Failed: + return 'bg-red-100 text-red-800 border-red-200'; + case MatchingResultStatus.Cancelled: + return 'bg-gray-100 text-gray-800 border-gray-200'; + default: + return 'bg-gray-100 text-gray-800 border-gray-200'; + } + }; + + // 获取状态显示文本 + const getStatusText = (status: MatchingResultStatus) => { + switch (status) { + case MatchingResultStatus.Success: + return '匹配成功'; + case MatchingResultStatus.PartialSuccess: + return '部分成功'; + case MatchingResultStatus.Failed: + return '匹配失败'; + case MatchingResultStatus.Cancelled: + return '已取消'; + default: + return '未知状态'; + } + }; + + // 获取成功率颜色 + const getSuccessRateColor = (rate: number) => { + if (rate >= 90) return 'text-green-600'; + if (rate >= 70) return 'text-yellow-600'; + if (rate >= 50) return 'text-orange-600'; + return 'text-red-600'; + }; + + return ( +
+ {/* 卡片头部 */} +
+
+
+

+ {result.result_name} +

+ {result.description && ( +

+ {result.description} +

+ )} +
+
+ {getStatusText(result.status)} +
+
+
+ + {/* 统计信息 */} +
+
+ {/* 成功率 */} +
+
+ {result.success_rate.toFixed(1)}% +
+
成功率
+
+ + {/* 片段统计 */} +
+
+ {result.matched_segments}/{result.total_segments} +
+
匹配片段
+
+
+ + {/* 详细统计 */} +
+
+ 失败片段: + {result.failed_segments} +
+
+ 使用素材: + {result.used_materials} +
+
+ 使用模特: + {result.used_models} +
+
+ 匹配耗时: + {formatDuration(result.matching_duration_ms)} +
+
+ + {/* 质量评分 */} + {result.quality_score && ( +
+
+ 质量评分: +
+
+ {[1, 2, 3, 4, 5].map((star) => ( + + + + ))} +
+ + {result.quality_score.toFixed(1)} + +
+
+
+ )} +
+ + {/* 时间信息 */} +
+
+ 创建: {formatDate(result.created_at)} + 更新: {formatDate(result.updated_at)} +
+
+ + {/* 操作按钮 */} +
+ + {onEdit && ( + + )} + +
+
+ ); +}; diff --git a/apps/desktop/src/components/TemplateMatchingResultDetailModal.tsx b/apps/desktop/src/components/TemplateMatchingResultDetailModal.tsx new file mode 100644 index 0000000..5307358 --- /dev/null +++ b/apps/desktop/src/components/TemplateMatchingResultDetailModal.tsx @@ -0,0 +1,452 @@ +import React, { useState, useEffect } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { + TemplateMatchingResultDetail, + MatchingResultStatus +} from '../types/templateMatchingResult'; +import { Modal } from './Modal'; +import { LoadingSpinner } from './LoadingSpinner'; +import { ErrorMessage } from './ErrorMessage'; +import { TabNavigation } from './TabNavigation'; + +interface TemplateMatchingResultDetailModalProps { + resultId: string; + onClose: () => void; + onUpdate?: () => void; +} + +export const TemplateMatchingResultDetailModal: React.FC = ({ + resultId, + onClose, + onUpdate, +}) => { + const [detail, setDetail] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [activeTab, setActiveTab] = useState('overview'); + const [editMode, setEditMode] = useState(false); + const [editForm, setEditForm] = useState({ + result_name: '', + description: '', + quality_score: '', + }); + + // 加载详情数据 + const loadDetail = async () => { + try { + setLoading(true); + setError(null); + + const resultDetail = await invoke('get_matching_result_detail', { + resultId, + }); + + if (resultDetail) { + setDetail(resultDetail); + setEditForm({ + result_name: resultDetail.matching_result.result_name, + description: resultDetail.matching_result.description || '', + quality_score: resultDetail.matching_result.quality_score?.toString() || '', + }); + } else { + setError('匹配结果不存在'); + } + } catch (err) { + setError(err as string); + } finally { + setLoading(false); + } + }; + + // 保存编辑 + const handleSave = async () => { + if (!detail) return; + + try { + // 更新基本信息 + await invoke('update_matching_result_info', { + resultId: detail.matching_result.id, + resultName: editForm.result_name || undefined, + description: editForm.description || undefined, + }); + + // 更新质量评分 + if (editForm.quality_score) { + const score = parseFloat(editForm.quality_score); + if (!isNaN(score) && score >= 0 && score <= 5) { + await invoke('set_matching_result_quality_score', { + resultId: detail.matching_result.id, + qualityScore: score, + }); + } + } + + setEditMode(false); + await loadDetail(); + onUpdate?.(); + } catch (err) { + setError(`保存失败: ${err}`); + } + }; + + // 格式化时长 + const formatDuration = (ms: number): string => { + if (ms < 1000) return `${ms}ms`; + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; + return `${(ms / 60000).toFixed(1)}min`; + }; + + // 格式化时间 + const formatTime = (microseconds: number): string => { + const seconds = microseconds / 1000000; + const minutes = Math.floor(seconds / 60); + const remainingSeconds = (seconds % 60).toFixed(1); + return minutes > 0 ? `${minutes}:${remainingSeconds.padStart(4, '0')}` : `${remainingSeconds}s`; + }; + + // 获取状态样式 + const getStatusStyle = (status: MatchingResultStatus) => { + switch (status) { + case MatchingResultStatus.Success: + return 'bg-green-100 text-green-800'; + case MatchingResultStatus.PartialSuccess: + return 'bg-yellow-100 text-yellow-800'; + case MatchingResultStatus.Failed: + return 'bg-red-100 text-red-800'; + case MatchingResultStatus.Cancelled: + return 'bg-gray-100 text-gray-800'; + default: + return 'bg-gray-100 text-gray-800'; + } + }; + + useEffect(() => { + loadDetail(); + }, [resultId]); + + const tabs = [ + { id: 'overview', label: '概览', count: undefined }, + { id: 'segments', label: '成功片段', count: detail?.segment_results.length }, + { id: 'failed', label: '失败片段', count: detail?.failed_segment_results.length }, + ]; + + if (loading) { + return ( + +
+ +
+
+ ); + } + + if (error || !detail) { + return ( + +
+ +
+
+ ); + } + + const { matching_result, segment_results, failed_segment_results } = detail; + + return ( + +
+ {/* 头部信息 */} +
+ {editMode ? ( +
+
+ + setEditForm(prev => ({ ...prev, result_name: e.target.value }))} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ +