feat: 实现素材使用状态管理系统
1. 数据库层面改进: - 新增material_usage_records表记录素材使用历史 - 为material_segments表添加usage_count、is_used、last_used_at字段 - 实现数据库迁移逻辑 2. 数据模型和仓库: - 创建MaterialUsageRecord、MaterialUsageStats等数据模型 - 实现MaterialUsageRepository处理使用记录的CRUD操作 - 支持批量创建使用记录和统计查询 3. 业务逻辑改进: - 修改MaterialMatchingService,在获取素材片段时排除已使用的片段 - 实现素材使用状态的自动更新机制 - 支持重置素材使用状态功能 4. 前端集成: - 修改handleApplyMatchingResult函数,应用匹配结果时自动创建使用记录 - 新增多个后端命令支持素材使用状态管理 - 实现从匹配结果自动创建使用记录的便捷方法 5. 核心功能: - 一个素材片段只能使用一次的限制机制 - 模板匹配时自动排除已使用的素材片段 - 完整的使用历史记录和统计功能 - 支持项目级别的使用状态重置 这个实现确保了素材的合理使用,避免重复使用同一素材片段, 同时提供了完整的使用追踪和管理功能。
This commit is contained in:
@@ -10,6 +10,7 @@ use crate::data::models::{
|
|||||||
};
|
};
|
||||||
use crate::data::repositories::{
|
use crate::data::repositories::{
|
||||||
material_repository::MaterialRepository,
|
material_repository::MaterialRepository,
|
||||||
|
material_usage_repository::MaterialUsageRepository,
|
||||||
video_classification_repository::VideoClassificationRepository,
|
video_classification_repository::VideoClassificationRepository,
|
||||||
template_matching_result_repository::TemplateMatchingResultRepository,
|
template_matching_result_repository::TemplateMatchingResultRepository,
|
||||||
};
|
};
|
||||||
@@ -23,6 +24,7 @@ use std::sync::Arc;
|
|||||||
/// 素材匹配服务
|
/// 素材匹配服务
|
||||||
pub struct MaterialMatchingService {
|
pub struct MaterialMatchingService {
|
||||||
material_repo: Arc<MaterialRepository>,
|
material_repo: Arc<MaterialRepository>,
|
||||||
|
material_usage_repo: Arc<MaterialUsageRepository>,
|
||||||
template_service: Arc<TemplateService>,
|
template_service: Arc<TemplateService>,
|
||||||
video_classification_repo: Arc<VideoClassificationRepository>,
|
video_classification_repo: Arc<VideoClassificationRepository>,
|
||||||
matching_result_service: Option<Arc<TemplateMatchingResultService>>,
|
matching_result_service: Option<Arc<TemplateMatchingResultService>>,
|
||||||
@@ -85,11 +87,13 @@ impl MaterialMatchingService {
|
|||||||
/// 创建新的素材匹配服务实例
|
/// 创建新的素材匹配服务实例
|
||||||
pub fn new(
|
pub fn new(
|
||||||
material_repo: Arc<MaterialRepository>,
|
material_repo: Arc<MaterialRepository>,
|
||||||
|
material_usage_repo: Arc<MaterialUsageRepository>,
|
||||||
template_service: Arc<TemplateService>,
|
template_service: Arc<TemplateService>,
|
||||||
video_classification_repo: Arc<VideoClassificationRepository>,
|
video_classification_repo: Arc<VideoClassificationRepository>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
material_repo,
|
material_repo,
|
||||||
|
material_usage_repo,
|
||||||
template_service,
|
template_service,
|
||||||
video_classification_repo,
|
video_classification_repo,
|
||||||
matching_result_service: None,
|
matching_result_service: None,
|
||||||
@@ -99,12 +103,14 @@ impl MaterialMatchingService {
|
|||||||
/// 创建新的素材匹配服务实例(带结果保存功能)
|
/// 创建新的素材匹配服务实例(带结果保存功能)
|
||||||
pub fn new_with_result_service(
|
pub fn new_with_result_service(
|
||||||
material_repo: Arc<MaterialRepository>,
|
material_repo: Arc<MaterialRepository>,
|
||||||
|
material_usage_repo: Arc<MaterialUsageRepository>,
|
||||||
template_service: Arc<TemplateService>,
|
template_service: Arc<TemplateService>,
|
||||||
video_classification_repo: Arc<VideoClassificationRepository>,
|
video_classification_repo: Arc<VideoClassificationRepository>,
|
||||||
matching_result_service: Arc<TemplateMatchingResultService>,
|
matching_result_service: Arc<TemplateMatchingResultService>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
material_repo,
|
material_repo,
|
||||||
|
material_usage_repo,
|
||||||
template_service,
|
template_service,
|
||||||
video_classification_repo,
|
video_classification_repo,
|
||||||
matching_result_service: Some(matching_result_service),
|
matching_result_service: Some(matching_result_service),
|
||||||
@@ -128,8 +134,8 @@ impl MaterialMatchingService {
|
|||||||
classification_records.insert(material.id.clone(), records);
|
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))
|
Ok((matching_result, None))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取已分类的素材片段
|
/// 获取已分类的素材片段(排除已使用的片段)
|
||||||
async fn get_classified_segments(
|
async fn get_classified_segments(
|
||||||
&self,
|
&self,
|
||||||
materials: &[Material],
|
materials: &[Material],
|
||||||
classification_records: &HashMap<String, Vec<VideoClassificationRecord>>,
|
classification_records: &HashMap<String, Vec<VideoClassificationRecord>>,
|
||||||
|
project_id: &str,
|
||||||
) -> Result<Vec<(MaterialSegment, String)>> {
|
) -> Result<Vec<(MaterialSegment, String)>> {
|
||||||
let mut classified_segments = Vec::new();
|
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::<HashSet<String>>()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// 如果获取使用记录失败,记录警告但不影响匹配流程
|
||||||
|
eprintln!("警告:获取素材使用记录失败: {},将继续进行匹配", e);
|
||||||
|
HashSet::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for material in materials {
|
for material in materials {
|
||||||
|
|
||||||
// 只处理有分类记录的素材
|
// 只处理有分类记录的素材
|
||||||
@@ -270,6 +291,11 @@ impl MaterialMatchingService {
|
|||||||
if let Some(duration) = material.get_duration() {
|
if let Some(duration) = material.get_duration() {
|
||||||
// 为每个分类记录创建一个虚拟片段
|
// 为每个分类记录创建一个虚拟片段
|
||||||
for record in records {
|
for record in records {
|
||||||
|
// 检查该片段是否已被使用
|
||||||
|
if used_segment_ids.contains(&record.segment_id) {
|
||||||
|
continue; // 跳过已使用的片段
|
||||||
|
}
|
||||||
|
|
||||||
// 创建虚拟片段,使用分类记录中的segment_id
|
// 创建虚拟片段,使用分类记录中的segment_id
|
||||||
let virtual_segment = MaterialSegment {
|
let virtual_segment = MaterialSegment {
|
||||||
id: record.segment_id.clone(), // 使用分类记录中的segment_id
|
id: record.segment_id.clone(), // 使用分类记录中的segment_id
|
||||||
@@ -281,6 +307,9 @@ impl MaterialMatchingService {
|
|||||||
file_path: material.original_path.clone(), // 使用原始文件路径
|
file_path: material.original_path.clone(), // 使用原始文件路径
|
||||||
file_size: material.file_size,
|
file_size: material.file_size,
|
||||||
thumbnail_path: material.thumbnail_path.clone(),
|
thumbnail_path: material.thumbnail_path.clone(),
|
||||||
|
usage_count: 0,
|
||||||
|
is_used: false,
|
||||||
|
last_used_at: None,
|
||||||
created_at: chrono::Utc::now(),
|
created_at: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -290,6 +319,11 @@ impl MaterialMatchingService {
|
|||||||
} else {
|
} else {
|
||||||
// 为每个素材片段查找对应的分类记录
|
// 为每个素材片段查找对应的分类记录
|
||||||
for segment in &material.segments {
|
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) {
|
if let Some(record) = records.iter().find(|r| r.segment_id == segment.id) {
|
||||||
classified_segments.push((segment.clone(), record.category.clone()));
|
classified_segments.push((segment.clone(), record.category.clone()));
|
||||||
|
|||||||
@@ -133,6 +133,9 @@ pub struct MaterialSegment {
|
|||||||
pub file_path: String,
|
pub file_path: String,
|
||||||
pub file_size: u64,
|
pub file_size: u64,
|
||||||
pub thumbnail_path: Option<String>, // 缩略图路径
|
pub thumbnail_path: Option<String>, // 缩略图路径
|
||||||
|
pub usage_count: u32, // 使用次数
|
||||||
|
pub is_used: bool, // 是否已使用
|
||||||
|
pub last_used_at: Option<DateTime<Utc>>, // 最后使用时间
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,6 +370,9 @@ impl MaterialSegment {
|
|||||||
file_path,
|
file_path,
|
||||||
file_size,
|
file_size,
|
||||||
thumbnail_path: None,
|
thumbnail_path: None,
|
||||||
|
usage_count: 0,
|
||||||
|
is_used: false,
|
||||||
|
last_used_at: None,
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
244
apps/desktop/src-tauri/src/data/models/material_usage.rs
Normal file
244
apps/desktop/src-tauri/src/data/models/material_usage.rs
Normal file
@@ -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<String>, // JSON格式的使用上下文信息
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String>,
|
||||||
|
) -> 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<DateTime<Utc>>,
|
||||||
|
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<DateTime<Utc>>,
|
||||||
|
) -> 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<MaterialUsageStats>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectMaterialUsageOverview {
|
||||||
|
/// 创建新的项目素材使用概览实例
|
||||||
|
pub fn new(
|
||||||
|
project_id: String,
|
||||||
|
materials_stats: Vec<MaterialUsageStats>,
|
||||||
|
) -> 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod project;
|
pub mod project;
|
||||||
pub mod material;
|
pub mod material;
|
||||||
pub mod material_segment_view;
|
pub mod material_segment_view;
|
||||||
|
pub mod material_usage;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod ai_classification;
|
pub mod ai_classification;
|
||||||
pub mod video_classification;
|
pub mod video_classification;
|
||||||
|
|||||||
@@ -434,6 +434,11 @@ impl MaterialRepository {
|
|||||||
file_path: row.get("file_path")?,
|
file_path: row.get("file_path")?,
|
||||||
file_size: row.get::<_, i64>("file_size")? as u64,
|
file_size: row.get::<_, i64>("file_size")? as u64,
|
||||||
thumbnail_path: row.get("thumbnail_path")?,
|
thumbnail_path: row.get("thumbnail_path")?,
|
||||||
|
usage_count: row.get::<_, Option<u32>>("usage_count")?.unwrap_or(0),
|
||||||
|
is_used: row.get::<_, Option<bool>>("is_used")?.unwrap_or(false),
|
||||||
|
last_used_at: row.get::<_, Option<String>>("last_used_at")?
|
||||||
|
.and_then(|s| chrono::DateTime::parse_from_rfc3339(&s).ok())
|
||||||
|
.map(|dt| dt.with_timezone(&chrono::Utc)),
|
||||||
created_at: {
|
created_at: {
|
||||||
let created_at_str = row.get::<_, String>("created_at")?;
|
let created_at_str = row.get::<_, String>("created_at")?;
|
||||||
// 尝试解析SQLite DATETIME格式 (YYYY-MM-DD HH:MM:SS)
|
// 尝试解析SQLite DATETIME格式 (YYYY-MM-DD HH:MM:SS)
|
||||||
@@ -615,7 +620,7 @@ impl MaterialRepository {
|
|||||||
|
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT id, material_id, segment_index, start_time, end_time, duration,
|
"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"
|
FROM material_segments WHERE id = ?1"
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -630,8 +635,13 @@ impl MaterialRepository {
|
|||||||
file_path: row.get(6)?,
|
file_path: row.get(6)?,
|
||||||
file_size: row.get(7)?,
|
file_size: row.get(7)?,
|
||||||
thumbnail_path: row.get(8)?,
|
thumbnail_path: row.get(8)?,
|
||||||
|
usage_count: row.get::<_, Option<u32>>(9)?.unwrap_or(0),
|
||||||
|
is_used: row.get::<_, Option<bool>>(10)?.unwrap_or(false),
|
||||||
|
last_used_at: row.get::<_, Option<String>>(11)?
|
||||||
|
.and_then(|s| chrono::DateTime::parse_from_rfc3339(&s).ok())
|
||||||
|
.map(|dt| dt.with_timezone(&chrono::Utc)),
|
||||||
created_at: {
|
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)
|
// 尝试解析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") {
|
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&created_at_str, "%Y-%m-%d %H:%M:%S") {
|
||||||
dt.and_utc()
|
dt.and_utc()
|
||||||
@@ -658,7 +668,7 @@ impl MaterialRepository {
|
|||||||
|
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT id, material_id, segment_index, start_time, end_time, duration,
|
"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"
|
FROM material_segments WHERE id = ?1"
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -673,8 +683,13 @@ impl MaterialRepository {
|
|||||||
file_path: row.get(6)?,
|
file_path: row.get(6)?,
|
||||||
file_size: row.get(7)?,
|
file_size: row.get(7)?,
|
||||||
thumbnail_path: row.get(8)?,
|
thumbnail_path: row.get(8)?,
|
||||||
|
usage_count: row.get::<_, Option<u32>>(9)?.unwrap_or(0),
|
||||||
|
is_used: row.get::<_, Option<bool>>(10)?.unwrap_or(false),
|
||||||
|
last_used_at: row.get::<_, Option<String>>(11)?
|
||||||
|
.and_then(|s| chrono::DateTime::parse_from_rfc3339(&s).ok())
|
||||||
|
.map(|dt| dt.with_timezone(&chrono::Utc)),
|
||||||
created_at: {
|
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)
|
// 尝试解析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") {
|
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&created_at_str, "%Y-%m-%d %H:%M:%S") {
|
||||||
dt.and_utc()
|
dt.and_utc()
|
||||||
|
|||||||
@@ -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<Database>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MaterialUsageRepository {
|
||||||
|
/// 创建新的素材使用记录仓库实例
|
||||||
|
pub fn new(database: Arc<Database>) -> Self {
|
||||||
|
Self { database }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 创建素材使用记录
|
||||||
|
pub fn create_usage_record(&self, request: CreateMaterialUsageRecordRequest) -> Result<MaterialUsageRecord> {
|
||||||
|
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<CreateMaterialUsageRecordRequest>) -> Result<Vec<MaterialUsageRecord>> {
|
||||||
|
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<Vec<MaterialUsageRecord>> {
|
||||||
|
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<Vec<MaterialUsageRecord>> {
|
||||||
|
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<Vec<MaterialUsageStats>> {
|
||||||
|
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<DateTime<Utc>> = row.get::<_, Option<String>>("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<ProjectMaterialUsageOverview> {
|
||||||
|
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<MaterialUsageRecord> {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod project_repository;
|
pub mod project_repository;
|
||||||
pub mod material_repository;
|
pub mod material_repository;
|
||||||
|
pub mod material_usage_repository;
|
||||||
pub mod model_repository;
|
pub mod model_repository;
|
||||||
pub mod ai_classification_repository;
|
pub mod ai_classification_repository;
|
||||||
pub mod video_classification_repository;
|
pub mod video_classification_repository;
|
||||||
|
|||||||
@@ -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(
|
conn.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS performance_metrics (
|
"CREATE TABLE IF NOT EXISTS performance_metrics (
|
||||||
@@ -1369,6 +1391,25 @@ impl Database {
|
|||||||
println!("Added thumbnail_path column to materials table");
|
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()?;
|
// self.cleanup_invalid_projects()?;
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,16 @@ pub fn run() {
|
|||||||
commands::material_matching_commands::execute_material_matching_with_save,
|
commands::material_matching_commands::execute_material_matching_with_save,
|
||||||
commands::material_matching_commands::get_project_material_stats_for_matching,
|
commands::material_matching_commands::get_project_material_stats_for_matching,
|
||||||
commands::material_matching_commands::validate_template_binding_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::save_matching_result,
|
||||||
commands::template_matching_result_commands::get_matching_result_detail,
|
commands::template_matching_result_commands::get_matching_result_detail,
|
||||||
|
|||||||
@@ -37,8 +37,13 @@ pub async fn execute_material_matching(
|
|||||||
VideoClassificationRepository::new(database.inner().clone())
|
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(
|
let matching_service = MaterialMatchingService::new(
|
||||||
material_repo,
|
material_repo,
|
||||||
|
material_usage_repo,
|
||||||
template_service,
|
template_service,
|
||||||
video_classification_repo,
|
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_repo = Arc::new(TemplateMatchingResultRepository::new(database.inner().clone()));
|
||||||
let matching_result_service = Arc::new(TemplateMatchingResultService::new(matching_result_repo));
|
let matching_result_service = Arc::new(TemplateMatchingResultService::new(matching_result_repo));
|
||||||
|
|
||||||
let matching_service = MaterialMatchingService::new_with_result_service(
|
let matching_service = MaterialMatchingService::new_with_result_service(
|
||||||
material_repo,
|
material_repo,
|
||||||
|
material_usage_repo,
|
||||||
template_service,
|
template_service,
|
||||||
video_classification_repo,
|
video_classification_repo,
|
||||||
matching_result_service,
|
matching_result_service,
|
||||||
|
|||||||
@@ -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<MaterialUsageRecord, String> {
|
||||||
|
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<CreateMaterialUsageRecordRequest>,
|
||||||
|
) -> Result<Vec<MaterialUsageRecord>, 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<Vec<MaterialUsageRecord>, 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<Vec<MaterialUsageRecord>, 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<Vec<MaterialUsageStats>, 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<ProjectMaterialUsageOverview, String> {
|
||||||
|
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<serde_json::Value>, // 匹配结果的JSON格式
|
||||||
|
) -> Result<Vec<MaterialUsageRecord>, 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<String>,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
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<String, String> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod project_commands;
|
pub mod project_commands;
|
||||||
pub mod system_commands;
|
pub mod system_commands;
|
||||||
pub mod material_commands;
|
pub mod material_commands;
|
||||||
|
pub mod material_usage_commands;
|
||||||
pub mod database_commands;
|
pub mod database_commands;
|
||||||
pub mod material_segment_view_commands;
|
pub mod material_segment_view_commands;
|
||||||
pub mod model_commands;
|
pub mod model_commands;
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ export const ProjectDetails: React.FC = () => {
|
|||||||
const description = `模板 ${currentMatchingBinding.template_name} 的匹配结果,成功匹配 ${result.statistics.matched_segments} 个片段`;
|
const description = `模板 ${currentMatchingBinding.template_name} 的匹配结果,成功匹配 ${result.statistics.matched_segments} 个片段`;
|
||||||
|
|
||||||
// 调用后端API保存匹配结果
|
// 调用后端API保存匹配结果
|
||||||
await invoke('save_matching_result', {
|
const savedResult = await invoke('save_matching_result', {
|
||||||
serviceResult: {
|
serviceResult: {
|
||||||
project_id: project?.id,
|
project_id: project?.id,
|
||||||
template_id: currentMatchingBinding.binding.template_id,
|
template_id: currentMatchingBinding.binding.template_id,
|
||||||
@@ -388,6 +388,23 @@ export const ProjectDetails: React.FC = () => {
|
|||||||
matchingDurationMs: 0 // 使用默认值
|
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);
|
setShowMatchingResultDialog(false);
|
||||||
setMatchingResult(null);
|
setMatchingResult(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user