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 6e684fd..a6c248f 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 @@ -10,6 +10,7 @@ use crate::data::models::{ }; use crate::data::repositories::{ material_repository::MaterialRepository, + material_usage_repository::MaterialUsageRepository, video_classification_repository::VideoClassificationRepository, template_matching_result_repository::TemplateMatchingResultRepository, }; @@ -23,6 +24,7 @@ use std::sync::Arc; /// 素材匹配服务 pub struct MaterialMatchingService { material_repo: Arc, + material_usage_repo: Arc, template_service: Arc, video_classification_repo: Arc, matching_result_service: Option>, @@ -85,11 +87,13 @@ impl MaterialMatchingService { /// 创建新的素材匹配服务实例 pub fn new( material_repo: Arc, + material_usage_repo: Arc, template_service: Arc, video_classification_repo: Arc, ) -> Self { Self { material_repo, + material_usage_repo, template_service, video_classification_repo, matching_result_service: None, @@ -99,12 +103,14 @@ impl MaterialMatchingService { /// 创建新的素材匹配服务实例(带结果保存功能) pub fn new_with_result_service( material_repo: Arc, + material_usage_repo: Arc, template_service: Arc, video_classification_repo: Arc, matching_result_service: Arc, ) -> Self { Self { material_repo, + material_usage_repo, template_service, video_classification_repo, matching_result_service: Some(matching_result_service), @@ -128,8 +134,8 @@ impl MaterialMatchingService { classification_records.insert(material.id.clone(), records); } - // 获取所有可用的素材片段(已分类的) - let available_segments = self.get_classified_segments(&project_materials, &classification_records).await?; + // 获取所有可用的素材片段(已分类的,排除已使用的) + let available_segments = self.get_classified_segments(&project_materials, &classification_records, &request.project_id).await?; @@ -247,14 +253,29 @@ impl MaterialMatchingService { Ok((matching_result, None)) } - /// 获取已分类的素材片段 + /// 获取已分类的素材片段(排除已使用的片段) async fn get_classified_segments( &self, materials: &[Material], classification_records: &HashMap>, + project_id: &str, ) -> Result> { let mut classified_segments = Vec::new(); + // 获取项目中已使用的素材片段ID列表 + let used_segment_ids = match self.material_usage_repo.get_usage_records_by_project(project_id) { + Ok(usage_records) => { + usage_records.into_iter() + .map(|record| record.material_segment_id) + .collect::>() + } + Err(e) => { + // 如果获取使用记录失败,记录警告但不影响匹配流程 + eprintln!("警告:获取素材使用记录失败: {},将继续进行匹配", e); + HashSet::new() + } + }; + for material in materials { // 只处理有分类记录的素材 @@ -270,6 +291,11 @@ impl MaterialMatchingService { if let Some(duration) = material.get_duration() { // 为每个分类记录创建一个虚拟片段 for record in records { + // 检查该片段是否已被使用 + if used_segment_ids.contains(&record.segment_id) { + continue; // 跳过已使用的片段 + } + // 创建虚拟片段,使用分类记录中的segment_id let virtual_segment = MaterialSegment { id: record.segment_id.clone(), // 使用分类记录中的segment_id @@ -281,6 +307,9 @@ impl MaterialMatchingService { file_path: material.original_path.clone(), // 使用原始文件路径 file_size: material.file_size, thumbnail_path: material.thumbnail_path.clone(), + usage_count: 0, + is_used: false, + last_used_at: None, created_at: chrono::Utc::now(), }; @@ -290,6 +319,11 @@ impl MaterialMatchingService { } else { // 为每个素材片段查找对应的分类记录 for segment in &material.segments { + // 检查该片段是否已被使用 + if used_segment_ids.contains(&segment.id) { + continue; // 跳过已使用的片段 + } + // 查找该片段的分类记录 if let Some(record) = records.iter().find(|r| r.segment_id == segment.id) { classified_segments.push((segment.clone(), record.category.clone())); diff --git a/apps/desktop/src-tauri/src/data/models/material.rs b/apps/desktop/src-tauri/src/data/models/material.rs index 1391dd9..5ae0eff 100644 --- a/apps/desktop/src-tauri/src/data/models/material.rs +++ b/apps/desktop/src-tauri/src/data/models/material.rs @@ -133,6 +133,9 @@ pub struct MaterialSegment { pub file_path: String, pub file_size: u64, pub thumbnail_path: Option, // 缩略图路径 + pub usage_count: u32, // 使用次数 + pub is_used: bool, // 是否已使用 + pub last_used_at: Option>, // 最后使用时间 pub created_at: DateTime, } @@ -367,6 +370,9 @@ impl MaterialSegment { file_path, file_size, thumbnail_path: None, + usage_count: 0, + is_used: false, + last_used_at: None, created_at: Utc::now(), } } diff --git a/apps/desktop/src-tauri/src/data/models/material_usage.rs b/apps/desktop/src-tauri/src/data/models/material_usage.rs new file mode 100644 index 0000000..4f546ae --- /dev/null +++ b/apps/desktop/src-tauri/src/data/models/material_usage.rs @@ -0,0 +1,244 @@ +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; + +/// 素材使用类型枚举 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum MaterialUsageType { + /// 模板匹配使用 + TemplateMatching, + /// 手动编辑使用 + ManualEdit, + /// 其他用途 + Other, +} + +impl Default for MaterialUsageType { + fn default() -> Self { + Self::TemplateMatching + } +} + +/// 素材使用记录实体模型 +/// 记录素材片段在各种场景下的使用情况 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MaterialUsageRecord { + pub id: String, + pub material_segment_id: String, + pub material_id: String, + pub project_id: String, + pub template_matching_result_id: String, + pub template_id: String, + pub binding_id: String, + pub track_segment_id: String, + pub usage_type: MaterialUsageType, + pub usage_context: Option, // JSON格式的使用上下文信息 + pub created_at: DateTime, +} + +impl MaterialUsageRecord { + /// 创建新的素材使用记录实例 + pub fn new( + material_segment_id: String, + material_id: String, + project_id: String, + template_matching_result_id: String, + template_id: String, + binding_id: String, + track_segment_id: String, + usage_type: MaterialUsageType, + usage_context: Option, + ) -> Self { + Self { + id: uuid::Uuid::new_v4().to_string(), + material_segment_id, + material_id, + project_id, + template_matching_result_id, + template_id, + binding_id, + track_segment_id, + usage_type, + usage_context, + created_at: Utc::now(), + } + } + + /// 创建模板匹配使用记录 + pub fn new_template_matching( + material_segment_id: String, + material_id: String, + project_id: String, + template_matching_result_id: String, + template_id: String, + binding_id: String, + track_segment_id: String, + match_score: f64, + match_reason: String, + ) -> Self { + let usage_context = serde_json::json!({ + "match_score": match_score, + "match_reason": match_reason, + "usage_timestamp": Utc::now().to_rfc3339() + }).to_string(); + + Self::new( + material_segment_id, + material_id, + project_id, + template_matching_result_id, + template_id, + binding_id, + track_segment_id, + MaterialUsageType::TemplateMatching, + Some(usage_context), + ) + } + + /// 验证记录的有效性 + pub fn validate(&self) -> Result<(), String> { + if self.material_segment_id.is_empty() { + return Err("素材片段ID不能为空".to_string()); + } + if self.material_id.is_empty() { + return Err("素材ID不能为空".to_string()); + } + if self.project_id.is_empty() { + return Err("项目ID不能为空".to_string()); + } + if self.template_matching_result_id.is_empty() { + return Err("模板匹配结果ID不能为空".to_string()); + } + Ok(()) + } +} + +/// 素材使用统计信息 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MaterialUsageStats { + pub material_id: String, + pub material_name: String, + pub total_segments: u32, + pub used_segments: u32, + pub unused_segments: u32, + pub total_usage_count: u32, + pub last_used_at: Option>, + pub usage_rate: f64, // 使用率 (used_segments / total_segments) +} + +impl MaterialUsageStats { + /// 创建新的素材使用统计实例 + pub fn new( + material_id: String, + material_name: String, + total_segments: u32, + used_segments: u32, + total_usage_count: u32, + last_used_at: Option>, + ) -> Self { + let unused_segments = total_segments.saturating_sub(used_segments); + let usage_rate = if total_segments > 0 { + used_segments as f64 / total_segments as f64 + } else { + 0.0 + }; + + Self { + material_id, + material_name, + total_segments, + used_segments, + unused_segments, + total_usage_count, + last_used_at, + usage_rate, + } + } +} + +/// 项目素材使用概览 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectMaterialUsageOverview { + pub project_id: String, + pub total_materials: u32, + pub total_segments: u32, + pub used_segments: u32, + pub unused_segments: u32, + pub total_usage_count: u32, + pub overall_usage_rate: f64, + pub materials_stats: Vec, +} + +impl ProjectMaterialUsageOverview { + /// 创建新的项目素材使用概览实例 + pub fn new( + project_id: String, + materials_stats: Vec, + ) -> Self { + let total_materials = materials_stats.len() as u32; + let total_segments: u32 = materials_stats.iter().map(|s| s.total_segments).sum(); + let used_segments: u32 = materials_stats.iter().map(|s| s.used_segments).sum(); + let unused_segments = total_segments.saturating_sub(used_segments); + let total_usage_count: u32 = materials_stats.iter().map(|s| s.total_usage_count).sum(); + let overall_usage_rate = if total_segments > 0 { + used_segments as f64 / total_segments as f64 + } else { + 0.0 + }; + + Self { + project_id, + total_materials, + total_segments, + used_segments, + unused_segments, + total_usage_count, + overall_usage_rate, + materials_stats, + } + } +} + +/// 创建素材使用记录请求 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateMaterialUsageRecordRequest { + pub material_segment_id: String, + pub material_id: String, + pub project_id: String, + pub template_matching_result_id: String, + pub template_id: String, + pub binding_id: String, + pub track_segment_id: String, + pub usage_type: MaterialUsageType, + pub usage_context: Option, +} + +impl CreateMaterialUsageRecordRequest { + /// 验证请求的有效性 + pub fn validate(&self) -> Result<(), String> { + if self.material_segment_id.is_empty() { + return Err("素材片段ID不能为空".to_string()); + } + if self.material_id.is_empty() { + return Err("素材ID不能为空".to_string()); + } + if self.project_id.is_empty() { + return Err("项目ID不能为空".to_string()); + } + Ok(()) + } + + /// 转换为素材使用记录实体 + pub fn to_entity(self) -> MaterialUsageRecord { + MaterialUsageRecord::new( + self.material_segment_id, + self.material_id, + self.project_id, + self.template_matching_result_id, + self.template_id, + self.binding_id, + self.track_segment_id, + self.usage_type, + self.usage_context, + ) + } +} diff --git a/apps/desktop/src-tauri/src/data/models/mod.rs b/apps/desktop/src-tauri/src/data/models/mod.rs index 5739240..f5c0043 100644 --- a/apps/desktop/src-tauri/src/data/models/mod.rs +++ b/apps/desktop/src-tauri/src/data/models/mod.rs @@ -1,6 +1,7 @@ pub mod project; pub mod material; pub mod material_segment_view; +pub mod material_usage; pub mod model; pub mod ai_classification; pub mod video_classification; 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 f9e0c9c..4c4cd79 100644 --- a/apps/desktop/src-tauri/src/data/repositories/material_repository.rs +++ b/apps/desktop/src-tauri/src/data/repositories/material_repository.rs @@ -434,6 +434,11 @@ impl MaterialRepository { file_path: row.get("file_path")?, file_size: row.get::<_, i64>("file_size")? as u64, thumbnail_path: row.get("thumbnail_path")?, + usage_count: row.get::<_, Option>("usage_count")?.unwrap_or(0), + is_used: row.get::<_, Option>("is_used")?.unwrap_or(false), + last_used_at: row.get::<_, Option>("last_used_at")? + .and_then(|s| chrono::DateTime::parse_from_rfc3339(&s).ok()) + .map(|dt| dt.with_timezone(&chrono::Utc)), created_at: { let created_at_str = row.get::<_, String>("created_at")?; // 尝试解析SQLite DATETIME格式 (YYYY-MM-DD HH:MM:SS) @@ -615,7 +620,7 @@ impl MaterialRepository { let mut stmt = conn.prepare( "SELECT id, material_id, segment_index, start_time, end_time, duration, - file_path, file_size, thumbnail_path, created_at + file_path, file_size, thumbnail_path, usage_count, is_used, last_used_at, created_at FROM material_segments WHERE id = ?1" )?; @@ -630,8 +635,13 @@ impl MaterialRepository { file_path: row.get(6)?, file_size: row.get(7)?, thumbnail_path: row.get(8)?, + usage_count: row.get::<_, Option>(9)?.unwrap_or(0), + is_used: row.get::<_, Option>(10)?.unwrap_or(false), + last_used_at: row.get::<_, Option>(11)? + .and_then(|s| chrono::DateTime::parse_from_rfc3339(&s).ok()) + .map(|dt| dt.with_timezone(&chrono::Utc)), created_at: { - let created_at_str = row.get::<_, String>(9)?; + let created_at_str = row.get::<_, String>(12)?; // 尝试解析SQLite DATETIME格式 (YYYY-MM-DD HH:MM:SS) if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&created_at_str, "%Y-%m-%d %H:%M:%S") { dt.and_utc() @@ -658,7 +668,7 @@ impl MaterialRepository { let mut stmt = conn.prepare( "SELECT id, material_id, segment_index, start_time, end_time, duration, - file_path, file_size, thumbnail_path, created_at + file_path, file_size, thumbnail_path, usage_count, is_used, last_used_at, created_at FROM material_segments WHERE id = ?1" )?; @@ -673,8 +683,13 @@ impl MaterialRepository { file_path: row.get(6)?, file_size: row.get(7)?, thumbnail_path: row.get(8)?, + usage_count: row.get::<_, Option>(9)?.unwrap_or(0), + is_used: row.get::<_, Option>(10)?.unwrap_or(false), + last_used_at: row.get::<_, Option>(11)? + .and_then(|s| chrono::DateTime::parse_from_rfc3339(&s).ok()) + .map(|dt| dt.with_timezone(&chrono::Utc)), created_at: { - let created_at_str = row.get::<_, String>(9)?; + let created_at_str = row.get::<_, String>(12)?; // 尝试解析SQLite DATETIME格式 (YYYY-MM-DD HH:MM:SS) if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&created_at_str, "%Y-%m-%d %H:%M:%S") { dt.and_utc() diff --git a/apps/desktop/src-tauri/src/data/repositories/material_usage_repository.rs b/apps/desktop/src-tauri/src/data/repositories/material_usage_repository.rs new file mode 100644 index 0000000..65fc018 --- /dev/null +++ b/apps/desktop/src-tauri/src/data/repositories/material_usage_repository.rs @@ -0,0 +1,283 @@ +use std::sync::Arc; +use rusqlite::{Connection, Row, Result}; +use chrono::{DateTime, Utc}; + +use crate::data::models::material_usage::{ + MaterialUsageRecord, MaterialUsageStats, ProjectMaterialUsageOverview, + CreateMaterialUsageRecordRequest, MaterialUsageType +}; +use crate::infrastructure::database::Database; + +/// 素材使用记录仓库 +/// 负责管理素材使用记录的数据库操作 +pub struct MaterialUsageRepository { + database: Arc, +} + +impl MaterialUsageRepository { + /// 创建新的素材使用记录仓库实例 + pub fn new(database: Arc) -> Self { + Self { database } + } + + /// 创建素材使用记录 + pub fn create_usage_record(&self, request: CreateMaterialUsageRecordRequest) -> Result { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + let record = request.to_entity(); + + conn.execute( + "INSERT INTO material_usage_records ( + id, material_segment_id, material_id, project_id, + template_matching_result_id, template_id, binding_id, + track_segment_id, usage_type, usage_context, created_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + rusqlite::params![ + &record.id, + &record.material_segment_id, + &record.material_id, + &record.project_id, + &record.template_matching_result_id, + &record.template_id, + &record.binding_id, + &record.track_segment_id, + &serde_json::to_string(&record.usage_type).unwrap(), + &record.usage_context, + &record.created_at.to_rfc3339(), + ], + )?; + + // 更新素材片段的使用状态 + self.update_segment_usage_status(&conn, &record.material_segment_id)?; + + Ok(record) + } + + /// 批量创建素材使用记录 + pub fn create_usage_records_batch(&self, requests: Vec) -> Result> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + let mut records = Vec::new(); + + // 开始事务 + let tx = conn.unchecked_transaction()?; + + for request in requests { + let record = request.to_entity(); + + tx.execute( + "INSERT INTO material_usage_records ( + id, material_segment_id, material_id, project_id, + template_matching_result_id, template_id, binding_id, + track_segment_id, usage_type, usage_context, created_at + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + rusqlite::params![ + &record.id, + &record.material_segment_id, + &record.material_id, + &record.project_id, + &record.template_matching_result_id, + &record.template_id, + &record.binding_id, + &record.track_segment_id, + &serde_json::to_string(&record.usage_type).unwrap(), + &record.usage_context, + &record.created_at.to_rfc3339(), + ], + )?; + + // 更新素材片段的使用状态 + self.update_segment_usage_status_in_tx(&tx, &record.material_segment_id)?; + + records.push(record); + } + + // 提交事务 + tx.commit()?; + + Ok(records) + } + + /// 更新素材片段的使用状态 + fn update_segment_usage_status(&self, conn: &Connection, segment_id: &str) -> Result<()> { + // 获取当前使用次数 + let usage_count: u32 = conn.query_row( + "SELECT COUNT(*) FROM material_usage_records WHERE material_segment_id = ?1", + [segment_id], + |row| row.get(0) + )?; + + // 更新素材片段的使用状态 + conn.execute( + "UPDATE material_segments SET + usage_count = ?1, + is_used = ?2, + last_used_at = ?3 + WHERE id = ?4", + rusqlite::params![ + usage_count, + usage_count > 0, + Utc::now().to_rfc3339(), + segment_id + ], + )?; + + Ok(()) + } + + /// 在事务中更新素材片段的使用状态 + fn update_segment_usage_status_in_tx(&self, tx: &rusqlite::Transaction, segment_id: &str) -> Result<()> { + // 获取当前使用次数 + let usage_count: u32 = tx.query_row( + "SELECT COUNT(*) FROM material_usage_records WHERE material_segment_id = ?1", + [segment_id], + |row| row.get(0) + )?; + + // 更新素材片段的使用状态 + tx.execute( + "UPDATE material_segments SET + usage_count = ?1, + is_used = ?2, + last_used_at = ?3 + WHERE id = ?4", + rusqlite::params![ + usage_count, + usage_count > 0, + Utc::now().to_rfc3339(), + segment_id + ], + )?; + + Ok(()) + } + + /// 获取素材使用记录列表 + pub fn get_usage_records_by_project(&self, project_id: &str) -> Result> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + let mut stmt = conn.prepare( + "SELECT id, material_segment_id, material_id, project_id, + template_matching_result_id, template_id, binding_id, + track_segment_id, usage_type, usage_context, created_at + FROM material_usage_records + WHERE project_id = ?1 + ORDER BY created_at DESC" + )?; + + let rows = stmt.query_map([project_id], |row| { + self.row_to_usage_record(row) + })?; + + let mut records = Vec::new(); + for row in rows { + records.push(row?); + } + + Ok(records) + } + + /// 获取素材使用记录(按模板匹配结果ID) + pub fn get_usage_records_by_matching_result(&self, matching_result_id: &str) -> Result> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + let mut stmt = conn.prepare( + "SELECT id, material_segment_id, material_id, project_id, + template_matching_result_id, template_id, binding_id, + track_segment_id, usage_type, usage_context, created_at + FROM material_usage_records + WHERE template_matching_result_id = ?1 + ORDER BY created_at DESC" + )?; + + let rows = stmt.query_map([matching_result_id], |row| { + self.row_to_usage_record(row) + })?; + + let mut records = Vec::new(); + for row in rows { + records.push(row?); + } + + Ok(records) + } + + /// 获取素材使用统计信息 + pub fn get_material_usage_stats(&self, project_id: &str) -> Result> { + let conn = self.database.get_connection(); + let conn = conn.lock().unwrap(); + let mut stmt = conn.prepare( + "SELECT + m.id as material_id, + m.name as material_name, + COUNT(ms.id) as total_segments, + COUNT(CASE WHEN ms.is_used = 1 THEN 1 END) as used_segments, + COALESCE(SUM(ms.usage_count), 0) as total_usage_count, + MAX(ms.last_used_at) as last_used_at + FROM materials m + LEFT JOIN material_segments ms ON m.id = ms.material_id + WHERE m.project_id = ?1 + GROUP BY m.id, m.name + ORDER BY m.name" + )?; + + let rows = stmt.query_map([project_id], |row| { + let material_id: String = row.get("material_id")?; + let material_name: String = row.get("material_name")?; + let total_segments: u32 = row.get::<_, i64>("total_segments")? as u32; + let used_segments: u32 = row.get::<_, i64>("used_segments")? as u32; + let total_usage_count: u32 = row.get::<_, i64>("total_usage_count")? as u32; + let last_used_at: Option> = row.get::<_, Option>("last_used_at")? + .and_then(|s| DateTime::parse_from_rfc3339(&s).ok()) + .map(|dt| dt.with_timezone(&Utc)); + + Ok(MaterialUsageStats::new( + material_id, + material_name, + total_segments, + used_segments, + total_usage_count, + last_used_at, + )) + })?; + + let mut stats = Vec::new(); + for row in rows { + stats.push(row?); + } + + Ok(stats) + } + + /// 获取项目素材使用概览 + pub fn get_project_usage_overview(&self, project_id: &str) -> Result { + let materials_stats = self.get_material_usage_stats(project_id)?; + Ok(ProjectMaterialUsageOverview::new(project_id.to_string(), materials_stats)) + } + + /// 将数据库行转换为素材使用记录对象 + fn row_to_usage_record(&self, row: &Row) -> Result { + let usage_type_str: String = row.get("usage_type")?; + let usage_type: MaterialUsageType = serde_json::from_str(&usage_type_str) + .unwrap_or(MaterialUsageType::TemplateMatching); + + let created_at_str: String = row.get("created_at")?; + let created_at = DateTime::parse_from_rfc3339(&created_at_str) + .map_err(|_| rusqlite::Error::InvalidColumnType(10, "created_at".to_string(), rusqlite::types::Type::Text))? + .with_timezone(&Utc); + + Ok(MaterialUsageRecord { + id: row.get("id")?, + material_segment_id: row.get("material_segment_id")?, + material_id: row.get("material_id")?, + project_id: row.get("project_id")?, + template_matching_result_id: row.get("template_matching_result_id")?, + template_id: row.get("template_id")?, + binding_id: row.get("binding_id")?, + track_segment_id: row.get("track_segment_id")?, + usage_type, + usage_context: row.get("usage_context")?, + created_at, + }) + } +} diff --git a/apps/desktop/src-tauri/src/data/repositories/mod.rs b/apps/desktop/src-tauri/src/data/repositories/mod.rs index 3d57589..78351bb 100644 --- a/apps/desktop/src-tauri/src/data/repositories/mod.rs +++ b/apps/desktop/src-tauri/src/data/repositories/mod.rs @@ -1,5 +1,6 @@ pub mod project_repository; pub mod material_repository; +pub mod material_usage_repository; pub mod model_repository; pub mod ai_classification_repository; pub mod video_classification_repository; diff --git a/apps/desktop/src-tauri/src/infrastructure/database.rs b/apps/desktop/src-tauri/src/infrastructure/database.rs index c1f2260..4cd0647 100644 --- a/apps/desktop/src-tauri/src/infrastructure/database.rs +++ b/apps/desktop/src-tauri/src/infrastructure/database.rs @@ -749,6 +749,28 @@ impl Database { [], )?; + // 创建素材使用记录表 + conn.execute( + "CREATE TABLE IF NOT EXISTS material_usage_records ( + id TEXT PRIMARY KEY, + material_segment_id TEXT NOT NULL, + material_id TEXT NOT NULL, + project_id TEXT NOT NULL, + template_matching_result_id TEXT NOT NULL, + template_id TEXT NOT NULL, + binding_id TEXT NOT NULL, + track_segment_id TEXT NOT NULL, + usage_type TEXT NOT NULL DEFAULT 'TemplateMatching', + usage_context TEXT, + created_at DATETIME NOT NULL, + FOREIGN KEY (material_segment_id) REFERENCES material_segments (id) ON DELETE CASCADE, + FOREIGN KEY (material_id) REFERENCES materials (id) ON DELETE CASCADE, + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE, + FOREIGN KEY (template_matching_result_id) REFERENCES template_matching_results (id) ON DELETE CASCADE + )", + [], + )?; + // 创建性能监控表 conn.execute( "CREATE TABLE IF NOT EXISTS performance_metrics ( @@ -1369,6 +1391,25 @@ impl Database { println!("Added thumbnail_path column to materials table"); } + // 添加素材使用状态字段到素材片段表 + let has_usage_count_column = conn.prepare("SELECT usage_count FROM material_segments LIMIT 1").is_ok(); + if !has_usage_count_column { + println!("Adding usage tracking columns to material_segments table"); + conn.execute( + "ALTER TABLE material_segments ADD COLUMN usage_count INTEGER DEFAULT 0", + [], + )?; + conn.execute( + "ALTER TABLE material_segments ADD COLUMN is_used BOOLEAN DEFAULT 0", + [], + )?; + conn.execute( + "ALTER TABLE material_segments ADD COLUMN last_used_at DATETIME", + [], + )?; + println!("Added usage tracking columns to material_segments table"); + } + // 暂时禁用自动清理,避免启动时卡住 // self.cleanup_invalid_projects()?; diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 30bc12c..a1a1268 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -194,6 +194,16 @@ pub fn run() { 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::material_usage_commands::create_material_usage_record, + commands::material_usage_commands::create_material_usage_records_batch, + commands::material_usage_commands::get_project_material_usage_records, + commands::material_usage_commands::get_matching_result_usage_records, + commands::material_usage_commands::get_project_material_usage_stats, + commands::material_usage_commands::get_project_material_usage_overview, + commands::material_usage_commands::create_usage_records_from_matching_result, + commands::material_usage_commands::reset_material_segment_usage, + commands::material_usage_commands::reset_project_material_usage, // 模板匹配结果命令 commands::template_matching_result_commands::save_matching_result, commands::template_matching_result_commands::get_matching_result_detail, 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 3463b18..9887fc9 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 @@ -37,8 +37,13 @@ pub async fn execute_material_matching( VideoClassificationRepository::new(database.inner().clone()) ); + let material_usage_repo = Arc::new( + crate::data::repositories::material_usage_repository::MaterialUsageRepository::new(database.inner().clone()) + ); + let matching_service = MaterialMatchingService::new( material_repo, + material_usage_repo, template_service, video_classification_repo, ); @@ -70,11 +75,15 @@ pub async fn execute_material_matching_with_save( ); // 创建匹配结果服务 + let material_usage_repo = Arc::new( + crate::data::repositories::material_usage_repository::MaterialUsageRepository::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, + material_usage_repo, template_service, video_classification_repo, matching_result_service, diff --git a/apps/desktop/src-tauri/src/presentation/commands/material_usage_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/material_usage_commands.rs new file mode 100644 index 0000000..287e4ca --- /dev/null +++ b/apps/desktop/src-tauri/src/presentation/commands/material_usage_commands.rs @@ -0,0 +1,240 @@ +use tauri::State; +use std::sync::Arc; + +use crate::data::models::material_usage::{ + CreateMaterialUsageRecordRequest, MaterialUsageRecord, MaterialUsageStats, + ProjectMaterialUsageOverview, MaterialUsageType +}; +use crate::data::repositories::material_usage_repository::MaterialUsageRepository; +use crate::app_state::AppState; + +/// 创建素材使用记录 +#[tauri::command] +pub async fn create_material_usage_record( + state: State<'_, AppState>, + request: CreateMaterialUsageRecordRequest, +) -> Result { + let database = state.get_database(); + let repo = MaterialUsageRepository::new(database); + + // 验证请求 + request.validate().map_err(|e| format!("请求验证失败: {}", e))?; + + repo.create_usage_record(request) + .map_err(|e| format!("创建素材使用记录失败: {}", e)) +} + +/// 批量创建素材使用记录 +#[tauri::command] +pub async fn create_material_usage_records_batch( + state: State<'_, AppState>, + requests: Vec, +) -> Result, String> { + let database = state.get_database(); + let repo = MaterialUsageRepository::new(database); + + // 验证所有请求 + for request in &requests { + request.validate().map_err(|e| format!("请求验证失败: {}", e))?; + } + + repo.create_usage_records_batch(requests) + .map_err(|e| format!("批量创建素材使用记录失败: {}", e)) +} + +/// 获取项目的素材使用记录 +#[tauri::command] +pub async fn get_project_material_usage_records( + state: State<'_, AppState>, + project_id: String, +) -> Result, String> { + let database = state.get_database(); + let repo = MaterialUsageRepository::new(database); + + repo.get_usage_records_by_project(&project_id) + .map_err(|e| format!("获取项目素材使用记录失败: {}", e)) +} + +/// 获取模板匹配结果的素材使用记录 +#[tauri::command] +pub async fn get_matching_result_usage_records( + state: State<'_, AppState>, + matching_result_id: String, +) -> Result, String> { + let database = state.get_database(); + let repo = MaterialUsageRepository::new(database); + + repo.get_usage_records_by_matching_result(&matching_result_id) + .map_err(|e| format!("获取匹配结果使用记录失败: {}", e)) +} + +/// 获取项目的素材使用统计信息 +#[tauri::command] +pub async fn get_project_material_usage_stats( + state: State<'_, AppState>, + project_id: String, +) -> Result, String> { + let database = state.get_database(); + let repo = MaterialUsageRepository::new(database); + + repo.get_material_usage_stats(&project_id) + .map_err(|e| format!("获取素材使用统计失败: {}", e)) +} + +/// 获取项目的素材使用概览 +#[tauri::command] +pub async fn get_project_material_usage_overview( + state: State<'_, AppState>, + project_id: String, +) -> Result { + let database = state.get_database(); + let repo = MaterialUsageRepository::new(database); + + repo.get_project_usage_overview(&project_id) + .map_err(|e| format!("获取项目素材使用概览失败: {}", e)) +} + +/// 从匹配结果创建素材使用记录 +/// 这是一个便捷方法,用于在应用匹配结果时自动创建使用记录 +#[tauri::command] +pub async fn create_usage_records_from_matching_result( + state: State<'_, AppState>, + project_id: String, + template_id: String, + binding_id: String, + template_matching_result_id: String, + matches: Vec, // 匹配结果的JSON格式 +) -> Result, String> { + let database = state.get_database(); + let repo = MaterialUsageRepository::new(database); + + let mut requests = Vec::new(); + + // 解析匹配结果并创建使用记录请求 + for match_value in matches { + // 尝试解析匹配结果 + let segment_result = match_value.get("segment_result"); + let track_segment = match_value.get("track_segment"); + + if let (Some(segment_result), Some(track_segment)) = (segment_result, track_segment) { + // 提取必要的字段 + let material_segment_id = segment_result.get("material_segment_id") + .and_then(|v| v.as_str()) + .ok_or("缺少material_segment_id字段")?; + + let material_id = segment_result.get("material_id") + .and_then(|v| v.as_str()) + .ok_or("缺少material_id字段")?; + + let track_segment_id = track_segment.get("id") + .and_then(|v| v.as_str()) + .ok_or("缺少track_segment_id字段")?; + + let match_score = segment_result.get("match_score") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); + + let match_reason = segment_result.get("match_reason") + .and_then(|v| v.as_str()) + .unwrap_or("模板匹配"); + + // 创建使用上下文 + let usage_context = serde_json::json!({ + "match_score": match_score, + "match_reason": match_reason, + "usage_timestamp": chrono::Utc::now().to_rfc3339() + }).to_string(); + + // 创建使用记录请求 + let request = CreateMaterialUsageRecordRequest { + material_segment_id: material_segment_id.to_string(), + material_id: material_id.to_string(), + project_id: project_id.clone(), + template_matching_result_id: template_matching_result_id.clone(), + template_id: template_id.clone(), + binding_id: binding_id.clone(), + track_segment_id: track_segment_id.to_string(), + usage_type: MaterialUsageType::TemplateMatching, + usage_context: Some(usage_context), + }; + + requests.push(request); + } + } + + if requests.is_empty() { + return Ok(Vec::new()); + } + + // 批量创建使用记录 + repo.create_usage_records_batch(requests) + .map_err(|e| format!("从匹配结果创建使用记录失败: {}", e)) +} + +/// 重置素材片段的使用状态 +#[tauri::command] +pub async fn reset_material_segment_usage( + state: State<'_, AppState>, + segment_ids: Vec, +) -> Result { + let database = state.get_database(); + let conn = database.get_connection(); + let conn = conn.lock().unwrap(); + + // 开始事务 + let tx = conn.unchecked_transaction() + .map_err(|e| format!("开始事务失败: {}", e))?; + + // 删除使用记录 + for segment_id in &segment_ids { + tx.execute( + "DELETE FROM material_usage_records WHERE material_segment_id = ?1", + [segment_id], + ).map_err(|e| format!("删除使用记录失败: {}", e))?; + + // 重置素材片段状态 + tx.execute( + "UPDATE material_segments SET usage_count = 0, is_used = 0, last_used_at = NULL WHERE id = ?1", + [segment_id], + ).map_err(|e| format!("重置片段状态失败: {}", e))?; + } + + // 提交事务 + tx.commit().map_err(|e| format!("提交事务失败: {}", e))?; + + Ok(format!("成功重置 {} 个素材片段的使用状态", segment_ids.len())) +} + +/// 重置项目所有素材的使用状态 +#[tauri::command] +pub async fn reset_project_material_usage( + state: State<'_, AppState>, + project_id: String, +) -> Result { + let database = state.get_database(); + let conn = database.get_connection(); + let conn = conn.lock().unwrap(); + + // 开始事务 + let tx = conn.unchecked_transaction() + .map_err(|e| format!("开始事务失败: {}", e))?; + + // 删除项目的所有使用记录 + let deleted_records = tx.execute( + "DELETE FROM material_usage_records WHERE project_id = ?1", + [&project_id], + ).map_err(|e| format!("删除使用记录失败: {}", e))?; + + // 重置项目所有素材片段状态 + let updated_segments = tx.execute( + "UPDATE material_segments SET usage_count = 0, is_used = 0, last_used_at = NULL + WHERE material_id IN (SELECT id FROM materials WHERE project_id = ?1)", + [&project_id], + ).map_err(|e| format!("重置片段状态失败: {}", e))?; + + // 提交事务 + tx.commit().map_err(|e| format!("提交事务失败: {}", e))?; + + Ok(format!("成功重置项目素材使用状态:删除 {} 条使用记录,重置 {} 个素材片段", + deleted_records, updated_segments)) +} diff --git a/apps/desktop/src-tauri/src/presentation/commands/mod.rs b/apps/desktop/src-tauri/src/presentation/commands/mod.rs index 82cf8e8..b76170a 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 material_usage_commands; pub mod database_commands; pub mod material_segment_view_commands; pub mod model_commands; diff --git a/apps/desktop/src/pages/ProjectDetails.tsx b/apps/desktop/src/pages/ProjectDetails.tsx index fe9503b..2221cce 100644 --- a/apps/desktop/src/pages/ProjectDetails.tsx +++ b/apps/desktop/src/pages/ProjectDetails.tsx @@ -373,7 +373,7 @@ export const ProjectDetails: React.FC = () => { const description = `模板 ${currentMatchingBinding.template_name} 的匹配结果,成功匹配 ${result.statistics.matched_segments} 个片段`; // 调用后端API保存匹配结果 - await invoke('save_matching_result', { + const savedResult = await invoke('save_matching_result', { serviceResult: { project_id: project?.id, template_id: currentMatchingBinding.binding.template_id, @@ -388,6 +388,23 @@ export const ProjectDetails: React.FC = () => { matchingDurationMs: 0 // 使用默认值 }); + // 创建素材使用记录 + if (savedResult && typeof savedResult === 'object' && 'id' in savedResult) { + try { + await invoke('create_usage_records_from_matching_result', { + project_id: project?.id, + template_id: currentMatchingBinding.binding.template_id, + binding_id: currentMatchingBinding.binding.id, + template_matching_result_id: (savedResult as any).id, + matches: result.matches + }); + console.log('素材使用记录创建成功'); + } catch (usageError) { + console.warn('创建素材使用记录失败,但匹配结果已保存:', usageError); + // 不阻断主流程,只记录警告 + } + } + // 关闭对话框 setShowMatchingResultDialog(false); setMatchingResult(null);