feat: 实现模板匹配结果保存和管理功能
- 新增模板匹配结果数据模型和数据库表结构 - 实现匹配结果Repository层,支持CRUD操作和查询 - 实现匹配结果Service层,提供业务逻辑和统计功能 - 新增Tauri命令接口,支持前端调用 - 实现前端TypeScript类型定义 - 更新MaterialMatchingService,支持自动保存匹配结果 - 新增前端管理界面组件: - TemplateMatchingResultManager: 主管理界面 - TemplateMatchingResultCard: 结果卡片组件 - TemplateMatchingResultDetailModal: 详情模态框 - TemplateMatchingResultStatsPanel: 统计面板 - 编写完整的单元测试 - 新增API文档 功能特性: - 保存匹配结果到数据库,包含成功和失败片段详情 - 支持匹配结果的查询、过滤、排序和分页 - 提供匹配统计信息和质量评分 - 支持软删除和批量操作 - 完整的前端管理界面,支持查看、编辑、删除操作
This commit is contained in:
@@ -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<MaterialRepository>,
|
||||
template_service: Arc<TemplateService>,
|
||||
video_classification_repo: Arc<VideoClassificationRepository>,
|
||||
matching_result_service: Option<Arc<TemplateMatchingResultService>>,
|
||||
}
|
||||
|
||||
/// 素材匹配请求
|
||||
@@ -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<MaterialRepository>,
|
||||
template_service: Arc<TemplateService>,
|
||||
video_classification_repo: Arc<VideoClassificationRepository>,
|
||||
matching_result_service: Arc<TemplateMatchingResultService>,
|
||||
) -> 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<String>,
|
||||
) -> Result<(MaterialMatchingResult, Option<crate::data::models::template_matching_result::TemplateMatchingResult>)> {
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<TemplateMatchingResultRepository>,
|
||||
}
|
||||
|
||||
impl TemplateMatchingResultService {
|
||||
/// 创建新的模板匹配结果服务实例
|
||||
pub fn new(repository: Arc<TemplateMatchingResultRepository>) -> Self {
|
||||
Self { repository }
|
||||
}
|
||||
|
||||
/// 保存匹配结果到数据库
|
||||
/// 这是一个完整的事务操作,保存主结果和所有相关的片段信息
|
||||
pub async fn save_matching_result(
|
||||
&self,
|
||||
service_result: &ServiceMatchingResult,
|
||||
result_name: String,
|
||||
description: Option<String>,
|
||||
matching_duration_ms: u64,
|
||||
) -> Result<TemplateMatchingResult> {
|
||||
// 创建主匹配结果记录
|
||||
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<MatchingSegmentResult> = 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<MatchingFailedSegmentResult> = 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<Option<TemplateMatchingResultDetail>> {
|
||||
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<Vec<TemplateMatchingResult>> {
|
||||
Ok(self.repository.get_by_project_id(project_id)?)
|
||||
}
|
||||
|
||||
/// 获取模板的匹配结果列表
|
||||
pub async fn get_template_matching_results(&self, template_id: &str) -> Result<Vec<TemplateMatchingResult>> {
|
||||
Ok(self.repository.get_by_template_id(template_id)?)
|
||||
}
|
||||
|
||||
/// 获取绑定的匹配结果列表
|
||||
pub async fn get_binding_matching_results(&self, binding_id: &str) -> Result<Vec<TemplateMatchingResult>> {
|
||||
Ok(self.repository.get_by_binding_id(binding_id)?)
|
||||
}
|
||||
|
||||
/// 查询匹配结果列表
|
||||
pub async fn list_matching_results(&self, options: TemplateMatchingResultQueryOptions) -> Result<Vec<TemplateMatchingResult>> {
|
||||
Ok(self.repository.list(options)?)
|
||||
}
|
||||
|
||||
/// 删除匹配结果
|
||||
pub async fn delete_matching_result(&self, result_id: &str) -> Result<bool> {
|
||||
Ok(self.repository.delete(result_id)?)
|
||||
}
|
||||
|
||||
/// 软删除匹配结果
|
||||
pub async fn soft_delete_matching_result(&self, result_id: &str) -> Result<bool> {
|
||||
Ok(self.repository.soft_delete(result_id)?)
|
||||
}
|
||||
|
||||
/// 更新匹配结果名称和描述
|
||||
pub async fn update_matching_result_info(
|
||||
&self,
|
||||
result_id: &str,
|
||||
result_name: Option<String>,
|
||||
description: Option<String>,
|
||||
) -> Result<Option<TemplateMatchingResult>> {
|
||||
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<Option<TemplateMatchingResult>> {
|
||||
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<MatchingStatistics> {
|
||||
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::<f64>() / 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<MatchingSegmentResult>,
|
||||
pub failed_segment_results: Vec<MatchingFailedSegmentResult>,
|
||||
}
|
||||
|
||||
/// 匹配统计信息
|
||||
#[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,
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Database> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String>,
|
||||
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<f64>, // 匹配质量评分
|
||||
pub status: MatchingResultStatus,
|
||||
pub metadata: Option<String>, // JSON格式的额外元数据
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
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<String>, // 模特ID
|
||||
pub model_name: Option<String>,
|
||||
pub match_score: f64, // 匹配评分
|
||||
pub match_reason: String, // 匹配原因
|
||||
pub segment_duration: u64, // 片段时长(微秒)
|
||||
pub start_time: u64, // 在模板中的开始时间(微秒)
|
||||
pub end_time: u64, // 在模板中的结束时间(微秒)
|
||||
pub properties: Option<String>, // JSON格式的片段属性
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 匹配失败片段结果实体模型
|
||||
/// 保存匹配失败的片段信息,用于分析和改进
|
||||
#[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<String>, // 匹配规则数据(JSON格式)
|
||||
pub failure_reason: String, // 失败原因
|
||||
pub failure_details: Option<String>, // 失败详情(JSON格式)
|
||||
pub segment_duration: u64, // 片段时长(微秒)
|
||||
pub start_time: u64, // 在模板中的开始时间(微秒)
|
||||
pub end_time: u64, // 在模板中的结束时间(微秒)
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
}
|
||||
|
||||
/// 模板匹配结果查询选项
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct TemplateMatchingResultQueryOptions {
|
||||
pub project_id: Option<String>,
|
||||
pub template_id: Option<String>,
|
||||
pub binding_id: Option<String>,
|
||||
pub status: Option<MatchingResultStatus>,
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
pub search_keyword: Option<String>,
|
||||
pub sort_by: Option<String>, // "created_at", "success_rate", "result_name"
|
||||
pub sort_order: Option<String>, // "asc", "desc"
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Database>,
|
||||
}
|
||||
|
||||
impl TemplateMatchingResultRepository {
|
||||
/// 创建新的模板匹配结果仓储实例
|
||||
pub fn new(database: Arc<Database>) -> Self {
|
||||
Self { database }
|
||||
}
|
||||
|
||||
/// 创建模板匹配结果
|
||||
pub fn create(&self, request: CreateTemplateMatchingResultRequest) -> Result<TemplateMatchingResult> {
|
||||
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<Option<TemplateMatchingResult>> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
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<Vec<TemplateMatchingResult>> {
|
||||
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<String> = 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<Vec<TemplateMatchingResult>> {
|
||||
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<Vec<TemplateMatchingResult>> {
|
||||
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<Vec<TemplateMatchingResult>> {
|
||||
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<Vec<MatchingSegmentResult>> {
|
||||
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<Vec<MatchingFailedSegmentResult>> {
|
||||
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<TemplateMatchingResult> {
|
||||
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<String> = row.get("quality_score")?;
|
||||
let quality_score = quality_score_str.and_then(|s| s.parse::<f64>().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::<i32>().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<MatchingSegmentResult> {
|
||||
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<MatchingFailedSegmentResult> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<String>,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<(MaterialMatchingResult, Option<TemplateMatchingResult>), 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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String>,
|
||||
matching_duration_ms: u64,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<TemplateMatchingResult, String> {
|
||||
// 创建服务实例
|
||||
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<Database>>,
|
||||
) -> Result<Option<TemplateMatchingResultDetail>, 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<Database>>,
|
||||
) -> Result<Vec<TemplateMatchingResult>, 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<Database>>,
|
||||
) -> Result<Vec<TemplateMatchingResult>, 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<Database>>,
|
||||
) -> Result<Vec<TemplateMatchingResult>, 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<Database>>,
|
||||
) -> Result<Vec<TemplateMatchingResult>, 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<Database>>,
|
||||
) -> Result<bool, String> {
|
||||
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<Database>>,
|
||||
) -> Result<bool, String> {
|
||||
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<String>,
|
||||
description: Option<String>,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<Option<TemplateMatchingResult>, 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<Database>>,
|
||||
) -> Result<Option<TemplateMatchingResult>, 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<String>,
|
||||
database: State<'_, Arc<Database>>,
|
||||
) -> Result<MatchingStatistics, String> {
|
||||
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<Database>>,
|
||||
) -> Result<TemplateMatchingResult, String> {
|
||||
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<Database>>,
|
||||
) -> Result<Option<TemplateMatchingResult>, 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<Vec<(String, String)>, String> {
|
||||
Ok(vec![
|
||||
("Success".to_string(), "匹配成功".to_string()),
|
||||
("PartialSuccess".to_string(), "部分成功".to_string()),
|
||||
("Failed".to_string(), "匹配失败".to_string()),
|
||||
("Cancelled".to_string(), "已取消".to_string()),
|
||||
])
|
||||
}
|
||||
@@ -12,7 +12,7 @@ interface DeleteConfirmDialogProps {
|
||||
/** 要删除的项目名称 */
|
||||
itemName?: string;
|
||||
/** 是否正在删除 */
|
||||
deleting: boolean;
|
||||
deleting?: boolean;
|
||||
/** 确认删除回调 */
|
||||
onConfirm: () => void;
|
||||
/** 取消回调 */
|
||||
|
||||
198
apps/desktop/src/components/TemplateMatchingResultCard.tsx
Normal file
198
apps/desktop/src/components/TemplateMatchingResultCard.tsx
Normal file
@@ -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<TemplateMatchingResultCardProps> = ({
|
||||
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 (
|
||||
<div className="template-matching-result-card bg-white rounded-lg shadow-sm border border-gray-200 hover:shadow-md transition-shadow duration-200">
|
||||
{/* 卡片头部 */}
|
||||
<div className="p-4 border-b border-gray-100">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg font-semibold text-gray-900 truncate">
|
||||
{result.result_name}
|
||||
</h3>
|
||||
{result.description && (
|
||||
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
|
||||
{result.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className={`ml-3 px-2 py-1 rounded-full text-xs font-medium border ${getStatusStyle(result.status)}`}>
|
||||
{getStatusText(result.status)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<div className="p-4">
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
{/* 成功率 */}
|
||||
<div className="text-center">
|
||||
<div className={`text-2xl font-bold ${getSuccessRateColor(result.success_rate)}`}>
|
||||
{result.success_rate.toFixed(1)}%
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">成功率</div>
|
||||
</div>
|
||||
|
||||
{/* 片段统计 */}
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{result.matched_segments}/{result.total_segments}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">匹配片段</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 详细统计 */}
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">失败片段:</span>
|
||||
<span className="text-gray-900">{result.failed_segments}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">使用素材:</span>
|
||||
<span className="text-gray-900">{result.used_materials}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">使用模特:</span>
|
||||
<span className="text-gray-900">{result.used_models}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">匹配耗时:</span>
|
||||
<span className="text-gray-900">{formatDuration(result.matching_duration_ms)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 质量评分 */}
|
||||
{result.quality_score && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-500">质量评分:</span>
|
||||
<div className="flex items-center">
|
||||
<div className="flex text-yellow-400">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<svg
|
||||
key={star}
|
||||
className={`w-4 h-4 ${
|
||||
star <= result.quality_score! ? 'fill-current' : 'text-gray-300'
|
||||
}`}
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
<span className="ml-2 text-sm text-gray-900">
|
||||
{result.quality_score.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 时间信息 */}
|
||||
<div className="px-4 py-3 bg-gray-50 border-t border-gray-100 text-xs text-gray-500">
|
||||
<div className="flex justify-between">
|
||||
<span>创建: {formatDate(result.created_at)}</span>
|
||||
<span>更新: {formatDate(result.updated_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="px-4 py-3 border-t border-gray-100 flex justify-end space-x-2">
|
||||
<button
|
||||
onClick={onViewDetail}
|
||||
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
查看详情
|
||||
</button>
|
||||
{onEdit && (
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="px-3 py-1.5 text-sm bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="px-3 py-1.5 text-sm bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<TemplateMatchingResultDetailModalProps> = ({
|
||||
resultId,
|
||||
onClose,
|
||||
onUpdate,
|
||||
}) => {
|
||||
const [detail, setDetail] = useState<TemplateMatchingResultDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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<TemplateMatchingResultDetail>('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 (
|
||||
<Modal isOpen onClose={onClose} title="匹配结果详情">
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<LoadingSpinner text="加载详情..." />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !detail) {
|
||||
return (
|
||||
<Modal isOpen onClose={onClose} title="匹配结果详情">
|
||||
<div className="p-4">
|
||||
<ErrorMessage message={error || '数据加载失败'} />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const { matching_result, segment_results, failed_segment_results } = detail;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
title="匹配结果详情"
|
||||
size="lg"
|
||||
>
|
||||
<div className="max-h-[80vh] overflow-y-auto">
|
||||
{/* 头部信息 */}
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
{editMode ? (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
结果名称
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.result_name}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
描述
|
||||
</label>
|
||||
<textarea
|
||||
value={editForm.description}
|
||||
onChange={(e) => setEditForm(prev => ({ ...prev, description: e.target.value }))}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
质量评分 (0-5)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="5"
|
||||
step="0.1"
|
||||
value={editForm.quality_score}
|
||||
onChange={(e) => setEditForm(prev => ({ ...prev, quality_score: 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditMode(false)}
|
||||
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
{matching_result.result_name}
|
||||
</h2>
|
||||
{matching_result.description && (
|
||||
<p className="text-gray-600 mt-1">{matching_result.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusStyle(matching_result.status)}`}>
|
||||
{matching_result.status === MatchingResultStatus.Success ? '匹配成功' :
|
||||
matching_result.status === MatchingResultStatus.PartialSuccess ? '部分成功' :
|
||||
matching_result.status === MatchingResultStatus.Failed ? '匹配失败' : '已取消'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setEditMode(true)}
|
||||
className="px-3 py-1 text-sm bg-gray-600 text-white rounded-md hover:bg-gray-700"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
||||
<div className="text-center p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{matching_result.success_rate.toFixed(1)}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">成功率</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{matching_result.matched_segments}/{matching_result.total_segments}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">匹配片段</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{matching_result.used_materials}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">使用素材</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{formatDuration(matching_result.matching_duration_ms)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">匹配耗时</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 质量评分 */}
|
||||
{matching_result.quality_score && (
|
||||
<div className="mt-4 p-3 bg-yellow-50 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-700">质量评分:</span>
|
||||
<div className="flex items-center">
|
||||
<div className="flex text-yellow-400 mr-2">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<svg
|
||||
key={star}
|
||||
className={`w-5 h-5 ${
|
||||
star <= matching_result.quality_score! ? 'fill-current' : 'text-gray-300'
|
||||
}`}
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{matching_result.quality_score.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 标签页导航 */}
|
||||
<TabNavigation
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
/>
|
||||
|
||||
{/* 标签页内容 */}
|
||||
<div className="p-6">
|
||||
{activeTab === 'overview' && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-3">基本信息</h3>
|
||||
<dl className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">项目ID:</dt>
|
||||
<dd className="text-sm text-gray-900">{matching_result.project_id}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">模板ID:</dt>
|
||||
<dd className="text-sm text-gray-900">{matching_result.template_id}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">绑定ID:</dt>
|
||||
<dd className="text-sm text-gray-900">{matching_result.binding_id}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">创建时间:</dt>
|
||||
<dd className="text-sm text-gray-900">
|
||||
{new Date(matching_result.created_at).toLocaleString('zh-CN')}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">更新时间:</dt>
|
||||
<dd className="text-sm text-gray-900">
|
||||
{new Date(matching_result.updated_at).toLocaleString('zh-CN')}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-3">匹配统计</h3>
|
||||
<dl className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">总片段数:</dt>
|
||||
<dd className="text-sm text-gray-900">{matching_result.total_segments}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">成功匹配:</dt>
|
||||
<dd className="text-sm text-green-600">{matching_result.matched_segments}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">匹配失败:</dt>
|
||||
<dd className="text-sm text-red-600">{matching_result.failed_segments}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">使用素材:</dt>
|
||||
<dd className="text-sm text-gray-900">{matching_result.used_materials}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">使用模特:</dt>
|
||||
<dd className="text-sm text-gray-900">{matching_result.used_models}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'segments' && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
成功匹配的片段 ({segment_results.length})
|
||||
</h3>
|
||||
{segment_results.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-8">暂无成功匹配的片段</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{segment_results.map((segment) => (
|
||||
<div key={segment.id} className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-gray-900">{segment.track_segment_name}</h4>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
素材: {segment.material_name}
|
||||
{segment.model_name && ` | 模特: ${segment.model_name}`}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
时长: {formatTime(segment.segment_duration)} |
|
||||
时间: {formatTime(segment.start_time)} - {formatTime(segment.end_time)}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
匹配原因: {segment.match_reason}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-bold text-green-600">
|
||||
{(segment.match_score * 100).toFixed(1)}%
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">匹配度</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'failed' && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
匹配失败的片段 ({failed_segment_results.length})
|
||||
</h3>
|
||||
{failed_segment_results.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-8">暂无匹配失败的片段</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{failed_segment_results.map((segment) => (
|
||||
<div key={segment.id} className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">{segment.track_segment_name}</h4>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
匹配规则: {segment.matching_rule_type}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
时长: {formatTime(segment.segment_duration)} |
|
||||
时间: {formatTime(segment.start_time)} - {formatTime(segment.end_time)}
|
||||
</p>
|
||||
<p className="text-sm text-red-600 mt-2 font-medium">
|
||||
失败原因: {segment.failure_reason}
|
||||
</p>
|
||||
{segment.failure_details && (
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
详情: {segment.failure_details}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
332
apps/desktop/src/components/TemplateMatchingResultManager.tsx
Normal file
332
apps/desktop/src/components/TemplateMatchingResultManager.tsx
Normal file
@@ -0,0 +1,332 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import {
|
||||
TemplateMatchingResult,
|
||||
TemplateMatchingResultQueryOptions,
|
||||
MatchingResultStatus,
|
||||
MatchingStatistics
|
||||
} from '../types/templateMatchingResult';
|
||||
import { LoadingSpinner } from './LoadingSpinner';
|
||||
import { ErrorMessage } from './ErrorMessage';
|
||||
import { EmptyState } from './EmptyState';
|
||||
import { DeleteConfirmDialog } from './DeleteConfirmDialog';
|
||||
import { CustomSelect } from './CustomSelect';
|
||||
import { TemplateMatchingResultCard } from './TemplateMatchingResultCard';
|
||||
import { TemplateMatchingResultDetailModal } from './TemplateMatchingResultDetailModal';
|
||||
import { TemplateMatchingResultStatsPanel } from './TemplateMatchingResultStatsPanel';
|
||||
|
||||
interface TemplateMatchingResultManagerProps {
|
||||
projectId?: string;
|
||||
templateId?: string;
|
||||
bindingId?: string;
|
||||
showStats?: boolean;
|
||||
onResultSelect?: (result: TemplateMatchingResult) => void;
|
||||
}
|
||||
|
||||
export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManagerProps> = ({
|
||||
projectId,
|
||||
templateId,
|
||||
bindingId,
|
||||
showStats = true,
|
||||
onResultSelect,
|
||||
}) => {
|
||||
const [results, setResults] = useState<TemplateMatchingResult[]>([]);
|
||||
const [statistics, setStatistics] = useState<MatchingStatistics | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedResult, setSelectedResult] = useState<TemplateMatchingResult | null>(null);
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<{
|
||||
show: boolean;
|
||||
result: TemplateMatchingResult | null;
|
||||
}>({ show: false, result: null });
|
||||
|
||||
// 过滤和排序状态
|
||||
const [filters, setFilters] = useState<{
|
||||
status?: MatchingResultStatus;
|
||||
searchKeyword?: string;
|
||||
sortBy: string;
|
||||
sortOrder: string;
|
||||
}>({
|
||||
sortBy: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
// 加载匹配结果列表
|
||||
const loadResults = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const queryOptions: TemplateMatchingResultQueryOptions = {
|
||||
project_id: projectId,
|
||||
template_id: templateId,
|
||||
binding_id: bindingId,
|
||||
status: filters.status,
|
||||
search_keyword: filters.searchKeyword,
|
||||
sort_by: filters.sortBy,
|
||||
sort_order: filters.sortOrder,
|
||||
limit: pagination.pageSize,
|
||||
offset: (pagination.page - 1) * pagination.pageSize,
|
||||
};
|
||||
|
||||
const resultList = await invoke<TemplateMatchingResult[]>('list_matching_results', {
|
||||
options: queryOptions,
|
||||
});
|
||||
|
||||
setResults(resultList);
|
||||
setPagination(prev => ({ ...prev, total: resultList.length }));
|
||||
} catch (err) {
|
||||
setError(err as string);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载统计信息
|
||||
const loadStatistics = async () => {
|
||||
if (!showStats) return;
|
||||
|
||||
try {
|
||||
const stats = await invoke<MatchingStatistics>('get_matching_statistics', {
|
||||
projectId,
|
||||
});
|
||||
setStatistics(stats);
|
||||
} catch (err) {
|
||||
console.error('加载统计信息失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除匹配结果
|
||||
const handleDelete = async (result: TemplateMatchingResult) => {
|
||||
try {
|
||||
await invoke<boolean>('soft_delete_matching_result', {
|
||||
resultId: result.id,
|
||||
});
|
||||
|
||||
// 重新加载列表
|
||||
await loadResults();
|
||||
await loadStatistics();
|
||||
|
||||
setDeleteConfirm({ show: false, result: null });
|
||||
} catch (err) {
|
||||
setError(`删除失败: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = (result: TemplateMatchingResult) => {
|
||||
setSelectedResult(result);
|
||||
setShowDetailModal(true);
|
||||
onResultSelect?.(result);
|
||||
};
|
||||
|
||||
// 处理过滤器变化
|
||||
const handleFilterChange = (newFilters: Partial<typeof filters>) => {
|
||||
setFilters(prev => ({ ...prev, ...newFilters }));
|
||||
setPagination(prev => ({ ...prev, page: 1 })); // 重置到第一页
|
||||
};
|
||||
|
||||
// 处理分页变化
|
||||
const handlePageChange = (page: number) => {
|
||||
setPagination(prev => ({ ...prev, page }));
|
||||
};
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
loadResults();
|
||||
loadStatistics();
|
||||
}, [projectId, templateId, bindingId, filters, pagination.page]);
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'Success', label: '匹配成功' },
|
||||
{ value: 'PartialSuccess', label: '部分成功' },
|
||||
{ value: 'Failed', label: '匹配失败' },
|
||||
{ value: 'Cancelled', label: '已取消' },
|
||||
];
|
||||
|
||||
// 排序选项
|
||||
const sortOptions = [
|
||||
{ value: 'created_at', label: '创建时间' },
|
||||
{ value: 'updated_at', label: '更新时间' },
|
||||
{ value: 'success_rate', label: '成功率' },
|
||||
{ value: 'result_name', label: '结果名称' },
|
||||
{ value: 'total_segments', label: '片段数量' },
|
||||
{ value: 'matching_duration_ms', label: '匹配耗时' },
|
||||
];
|
||||
|
||||
const sortOrderOptions = [
|
||||
{ value: 'desc', label: '降序' },
|
||||
{ value: 'asc', label: '升序' },
|
||||
];
|
||||
|
||||
if (loading && results.length === 0) {
|
||||
return <LoadingSpinner text="加载匹配结果..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="template-matching-result-manager">
|
||||
{/* 统计面板 */}
|
||||
{showStats && statistics && (
|
||||
<TemplateMatchingResultStatsPanel statistics={statistics} />
|
||||
)}
|
||||
|
||||
{/* 过滤器和搜索 */}
|
||||
<div className="filters-section bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* 搜索框 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
搜索
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索结果名称或描述..."
|
||||
value={filters.searchKeyword || ''}
|
||||
onChange={(e) => handleFilterChange({ searchKeyword: 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 状态过滤 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
状态
|
||||
</label>
|
||||
<CustomSelect
|
||||
options={statusOptions}
|
||||
value={filters.status || ''}
|
||||
onChange={(value) => handleFilterChange({ status: value as MatchingResultStatus })}
|
||||
placeholder="选择状态"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 排序字段 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
排序字段
|
||||
</label>
|
||||
<CustomSelect
|
||||
options={sortOptions}
|
||||
value={filters.sortBy}
|
||||
onChange={(value) => handleFilterChange({ sortBy: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 排序顺序 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
排序顺序
|
||||
</label>
|
||||
<CustomSelect
|
||||
options={sortOrderOptions}
|
||||
value={filters.sortOrder}
|
||||
onChange={(value) => handleFilterChange({ sortOrder: value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex justify-between items-center mt-4">
|
||||
<div className="text-sm text-gray-500">
|
||||
共 {pagination.total} 个匹配结果
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
loadResults();
|
||||
loadStatistics();
|
||||
}}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误信息 */}
|
||||
{error && (
|
||||
<ErrorMessage
|
||||
message={error}
|
||||
onDismiss={() => setError(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 结果列表 */}
|
||||
{results.length === 0 ? (
|
||||
<EmptyState
|
||||
title="暂无匹配结果"
|
||||
description="还没有任何模板匹配结果,请先执行模板匹配操作。"
|
||||
icon="📊"
|
||||
/>
|
||||
) : (
|
||||
<div className="results-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{results.map((result) => (
|
||||
<TemplateMatchingResultCard
|
||||
key={result.id}
|
||||
result={result}
|
||||
onViewDetail={() => handleViewDetail(result)}
|
||||
onDelete={() => setDeleteConfirm({ show: true, result })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分页 */}
|
||||
{pagination.total > pagination.pageSize && (
|
||||
<div className="pagination-section mt-6 flex justify-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => handlePageChange(pagination.page - 1)}
|
||||
disabled={pagination.page <= 1}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span className="px-3 py-2 text-sm text-gray-700">
|
||||
第 {pagination.page} 页,共 {Math.ceil(pagination.total / pagination.pageSize)} 页
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handlePageChange(pagination.page + 1)}
|
||||
disabled={pagination.page >= Math.ceil(pagination.total / pagination.pageSize)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 详情模态框 */}
|
||||
{showDetailModal && selectedResult && (
|
||||
<TemplateMatchingResultDetailModal
|
||||
resultId={selectedResult.id}
|
||||
onClose={() => {
|
||||
setShowDetailModal(false);
|
||||
setSelectedResult(null);
|
||||
}}
|
||||
onUpdate={() => {
|
||||
loadResults();
|
||||
loadStatistics();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<DeleteConfirmDialog
|
||||
isOpen={deleteConfirm.show}
|
||||
title="删除匹配结果"
|
||||
message={`确定要删除匹配结果 "${deleteConfirm.result?.result_name}" 吗?此操作不可撤销。`}
|
||||
onConfirm={() => deleteConfirm.result && handleDelete(deleteConfirm.result)}
|
||||
onCancel={() => setDeleteConfirm({ show: false, result: null })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
215
apps/desktop/src/components/TemplateMatchingResultStatsPanel.tsx
Normal file
215
apps/desktop/src/components/TemplateMatchingResultStatsPanel.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import React from 'react';
|
||||
import { MatchingStatistics } from '../types/templateMatchingResult';
|
||||
|
||||
interface TemplateMatchingResultStatsPanelProps {
|
||||
statistics: MatchingStatistics;
|
||||
}
|
||||
|
||||
export const TemplateMatchingResultStatsPanel: React.FC<TemplateMatchingResultStatsPanelProps> = ({
|
||||
statistics,
|
||||
}) => {
|
||||
// 计算成功率百分比
|
||||
const successPercentage = statistics.total_results > 0
|
||||
? (statistics.successful_results / statistics.total_results) * 100
|
||||
: 0;
|
||||
|
||||
// 计算片段匹配率
|
||||
const segmentMatchPercentage = statistics.total_segments > 0
|
||||
? (statistics.matched_segments / statistics.total_segments) * 100
|
||||
: 0;
|
||||
|
||||
// 获取成功率颜色
|
||||
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';
|
||||
};
|
||||
|
||||
// 统计卡片组件
|
||||
const StatCard: React.FC<{
|
||||
title: string;
|
||||
value: string | number;
|
||||
subtitle?: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
}> = ({ title, value, subtitle, color = 'text-gray-900', icon }) => (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500">{title}</p>
|
||||
<p className={`text-2xl font-bold ${color}`}>{value}</p>
|
||||
{subtitle && (
|
||||
<p className="text-xs text-gray-400 mt-1">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
{icon && (
|
||||
<div className="text-2xl opacity-60">{icon}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="template-matching-result-stats-panel mb-6">
|
||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-6 mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<span className="mr-2">📊</span>
|
||||
匹配结果统计
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* 总结果数 */}
|
||||
<StatCard
|
||||
title="总匹配结果"
|
||||
value={statistics.total_results}
|
||||
subtitle="个匹配结果"
|
||||
icon="📋"
|
||||
/>
|
||||
|
||||
{/* 成功结果数 */}
|
||||
<StatCard
|
||||
title="成功结果"
|
||||
value={statistics.successful_results}
|
||||
subtitle={`${successPercentage.toFixed(1)}% 成功率`}
|
||||
color={getSuccessRateColor(successPercentage)}
|
||||
icon="✅"
|
||||
/>
|
||||
|
||||
{/* 总片段数 */}
|
||||
<StatCard
|
||||
title="总片段数"
|
||||
value={statistics.total_segments}
|
||||
subtitle="个模板片段"
|
||||
icon="🎬"
|
||||
/>
|
||||
|
||||
{/* 匹配片段数 */}
|
||||
<StatCard
|
||||
title="匹配片段"
|
||||
value={statistics.matched_segments}
|
||||
subtitle={`${segmentMatchPercentage.toFixed(1)}% 匹配率`}
|
||||
color={getSuccessRateColor(segmentMatchPercentage)}
|
||||
icon="🎯"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 详细统计 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* 素材使用统计 */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||||
<span className="mr-2">🎥</span>
|
||||
素材使用情况
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">使用素材总数:</span>
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{statistics.total_materials}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">平均每结果:</span>
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{statistics.total_results > 0
|
||||
? (statistics.total_materials / statistics.total_results).toFixed(1)
|
||||
: '0'
|
||||
} 个
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模特使用统计 */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||||
<span className="mr-2">👤</span>
|
||||
模特使用情况
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">使用模特总数:</span>
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{statistics.total_models}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">平均每结果:</span>
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{statistics.total_results > 0
|
||||
? (statistics.total_models / statistics.total_results).toFixed(1)
|
||||
: '0'
|
||||
} 个
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 质量统计 */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-3 flex items-center">
|
||||
<span className="mr-2">⭐</span>
|
||||
质量统计
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">平均成功率:</span>
|
||||
<span className={`text-sm font-medium ${getSuccessRateColor(statistics.average_success_rate)}`}>
|
||||
{statistics.average_success_rate.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">失败片段:</span>
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{statistics.total_segments - statistics.matched_segments}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 进度条显示 */}
|
||||
<div className="mt-4 bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-3">整体匹配进度</h3>
|
||||
|
||||
{/* 结果成功率进度条 */}
|
||||
<div className="mb-3">
|
||||
<div className="flex justify-between text-xs text-gray-500 mb-1">
|
||||
<span>结果成功率</span>
|
||||
<span>{successPercentage.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all duration-300 ${
|
||||
successPercentage >= 90 ? 'bg-green-500' :
|
||||
successPercentage >= 70 ? 'bg-yellow-500' :
|
||||
successPercentage >= 50 ? 'bg-orange-500' : 'bg-red-500'
|
||||
}`}
|
||||
style={{ width: `${Math.min(successPercentage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 片段匹配率进度条 */}
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-gray-500 mb-1">
|
||||
<span>片段匹配率</span>
|
||||
<span>{segmentMatchPercentage.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all duration-300 ${
|
||||
segmentMatchPercentage >= 90 ? 'bg-green-500' :
|
||||
segmentMatchPercentage >= 70 ? 'bg-yellow-500' :
|
||||
segmentMatchPercentage >= 50 ? 'bg-orange-500' : 'bg-red-500'
|
||||
}`}
|
||||
style={{ width: `${Math.min(segmentMatchPercentage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
298
apps/desktop/src/types/templateMatchingResult.ts
Normal file
298
apps/desktop/src/types/templateMatchingResult.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* 模板匹配结果相关的类型定义
|
||||
* 遵循前端开发规范的类型设计原则
|
||||
*/
|
||||
|
||||
|
||||
// 匹配结果状态枚举
|
||||
export enum MatchingResultStatus {
|
||||
Success = 'Success',
|
||||
PartialSuccess = 'PartialSuccess',
|
||||
Failed = 'Failed',
|
||||
Cancelled = 'Cancelled'
|
||||
}
|
||||
|
||||
// 模板匹配结果
|
||||
export interface TemplateMatchingResult {
|
||||
id: string;
|
||||
project_id: string;
|
||||
template_id: string;
|
||||
binding_id: string;
|
||||
result_name: string;
|
||||
description?: string;
|
||||
total_segments: number;
|
||||
matched_segments: number;
|
||||
failed_segments: number;
|
||||
success_rate: number;
|
||||
used_materials: number;
|
||||
used_models: number;
|
||||
matching_duration_ms: number;
|
||||
quality_score?: number;
|
||||
status: MatchingResultStatus;
|
||||
metadata?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
// 匹配片段结果
|
||||
export interface MatchingSegmentResult {
|
||||
id: string;
|
||||
matching_result_id: string;
|
||||
track_segment_id: string;
|
||||
track_segment_name: string;
|
||||
material_segment_id: string;
|
||||
material_id: string;
|
||||
material_name: string;
|
||||
model_id?: string;
|
||||
model_name?: string;
|
||||
match_score: number;
|
||||
match_reason: string;
|
||||
segment_duration: number;
|
||||
start_time: number;
|
||||
end_time: number;
|
||||
properties?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 匹配失败片段结果
|
||||
export interface MatchingFailedSegmentResult {
|
||||
id: string;
|
||||
matching_result_id: string;
|
||||
track_segment_id: string;
|
||||
track_segment_name: string;
|
||||
matching_rule_type: string;
|
||||
matching_rule_data?: string;
|
||||
failure_reason: string;
|
||||
failure_details?: string;
|
||||
segment_duration: number;
|
||||
start_time: number;
|
||||
end_time: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 模板匹配结果详情
|
||||
export interface TemplateMatchingResultDetail {
|
||||
matching_result: TemplateMatchingResult;
|
||||
segment_results: MatchingSegmentResult[];
|
||||
failed_segment_results: MatchingFailedSegmentResult[];
|
||||
}
|
||||
|
||||
// 创建模板匹配结果请求
|
||||
export interface CreateTemplateMatchingResultRequest {
|
||||
project_id: string;
|
||||
template_id: string;
|
||||
binding_id: string;
|
||||
result_name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// 模板匹配结果查询选项
|
||||
export interface TemplateMatchingResultQueryOptions {
|
||||
project_id?: string;
|
||||
template_id?: string;
|
||||
binding_id?: string;
|
||||
status?: MatchingResultStatus;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search_keyword?: string;
|
||||
sort_by?: string; // "created_at", "success_rate", "result_name"
|
||||
sort_order?: string; // "asc", "desc"
|
||||
}
|
||||
|
||||
// 匹配统计信息
|
||||
export interface MatchingStatistics {
|
||||
total_results: number;
|
||||
successful_results: number;
|
||||
total_segments: number;
|
||||
matched_segments: number;
|
||||
total_materials: number;
|
||||
total_models: number;
|
||||
average_success_rate: number;
|
||||
}
|
||||
|
||||
// 保存匹配结果请求
|
||||
export interface SaveMatchingResultRequest {
|
||||
service_result: any; // MaterialMatchingResult from service
|
||||
result_name: string;
|
||||
description?: string;
|
||||
matching_duration_ms: number;
|
||||
}
|
||||
|
||||
// 更新匹配结果信息请求
|
||||
export interface UpdateMatchingResultInfoRequest {
|
||||
result_id: string;
|
||||
result_name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// 设置质量评分请求
|
||||
export interface SetQualityScoreRequest {
|
||||
result_id: string;
|
||||
quality_score: number;
|
||||
}
|
||||
|
||||
// 匹配结果状态选项
|
||||
export interface MatchingResultStatusOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// 匹配结果列表项(用于列表显示)
|
||||
export interface MatchingResultListItem {
|
||||
id: string;
|
||||
result_name: string;
|
||||
project_id: string;
|
||||
template_id: string;
|
||||
status: MatchingResultStatus;
|
||||
success_rate: number;
|
||||
total_segments: number;
|
||||
matched_segments: number;
|
||||
failed_segments: number;
|
||||
used_materials: number;
|
||||
used_models: number;
|
||||
matching_duration_ms: number;
|
||||
quality_score?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 匹配结果卡片信息(用于卡片显示)
|
||||
export interface MatchingResultCardInfo {
|
||||
id: string;
|
||||
result_name: string;
|
||||
description?: string;
|
||||
status: MatchingResultStatus;
|
||||
status_display: string;
|
||||
success_rate: number;
|
||||
total_segments: number;
|
||||
matched_segments: number;
|
||||
failed_segments: number;
|
||||
used_materials: number;
|
||||
used_models: number;
|
||||
matching_duration_display: string; // 格式化后的时长显示
|
||||
quality_score?: number;
|
||||
created_at_display: string; // 格式化后的创建时间显示
|
||||
updated_at_display: string; // 格式化后的更新时间显示
|
||||
}
|
||||
|
||||
// 匹配结果过滤选项
|
||||
export interface MatchingResultFilterOptions {
|
||||
status?: MatchingResultStatus[];
|
||||
success_rate_min?: number;
|
||||
success_rate_max?: number;
|
||||
date_range?: {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
};
|
||||
has_quality_score?: boolean;
|
||||
min_segments?: number;
|
||||
max_segments?: number;
|
||||
}
|
||||
|
||||
// 匹配结果排序选项
|
||||
export interface MatchingResultSortOptions {
|
||||
field: 'created_at' | 'updated_at' | 'success_rate' | 'result_name' | 'total_segments' | 'matching_duration_ms';
|
||||
order: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// 匹配结果分页选项
|
||||
export interface MatchingResultPaginationOptions {
|
||||
page: number;
|
||||
page_size: number;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
// 匹配结果搜索选项
|
||||
export interface MatchingResultSearchOptions {
|
||||
keyword?: string;
|
||||
filter?: MatchingResultFilterOptions;
|
||||
sort?: MatchingResultSortOptions;
|
||||
pagination?: MatchingResultPaginationOptions;
|
||||
}
|
||||
|
||||
// 匹配结果导出选项
|
||||
export interface MatchingResultExportOptions {
|
||||
format: 'json' | 'csv' | 'excel';
|
||||
include_segments: boolean;
|
||||
include_failed_segments: boolean;
|
||||
date_range?: {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 匹配结果批量操作选项
|
||||
export interface MatchingResultBatchOperationOptions {
|
||||
operation: 'delete' | 'soft_delete' | 'export' | 'set_quality_score';
|
||||
result_ids: string[];
|
||||
parameters?: Record<string, any>;
|
||||
}
|
||||
|
||||
// 匹配结果分析报告
|
||||
export interface MatchingResultAnalysisReport {
|
||||
total_results: number;
|
||||
status_distribution: Record<MatchingResultStatus, number>;
|
||||
success_rate_distribution: {
|
||||
excellent: number; // 90-100%
|
||||
good: number; // 70-89%
|
||||
fair: number; // 50-69%
|
||||
poor: number; // 0-49%
|
||||
};
|
||||
duration_statistics: {
|
||||
min: number;
|
||||
max: number;
|
||||
average: number;
|
||||
median: number;
|
||||
};
|
||||
segment_statistics: {
|
||||
total_segments: number;
|
||||
total_matched: number;
|
||||
total_failed: number;
|
||||
average_segments_per_result: number;
|
||||
};
|
||||
material_usage: {
|
||||
total_materials_used: number;
|
||||
average_materials_per_result: number;
|
||||
most_used_materials: Array<{
|
||||
material_id: string;
|
||||
material_name: string;
|
||||
usage_count: number;
|
||||
}>;
|
||||
};
|
||||
model_usage: {
|
||||
total_models_used: number;
|
||||
average_models_per_result: number;
|
||||
most_used_models: Array<{
|
||||
model_id: string;
|
||||
model_name: string;
|
||||
usage_count: number;
|
||||
}>;
|
||||
};
|
||||
quality_score_distribution?: {
|
||||
excellent: number; // 4.5-5.0
|
||||
good: number; // 3.5-4.4
|
||||
fair: number; // 2.5-3.4
|
||||
poor: number; // 0-2.4
|
||||
unrated: number;
|
||||
};
|
||||
trends: {
|
||||
success_rate_trend: 'improving' | 'declining' | 'stable';
|
||||
duration_trend: 'faster' | 'slower' | 'stable';
|
||||
quality_trend?: 'improving' | 'declining' | 'stable';
|
||||
};
|
||||
}
|
||||
|
||||
// 匹配结果操作历史
|
||||
export interface MatchingResultOperationHistory {
|
||||
id: string;
|
||||
result_id: string;
|
||||
operation_type: 'create' | 'update' | 'delete' | 'quality_score_set';
|
||||
operation_details: string;
|
||||
performed_by?: string;
|
||||
performed_at: string;
|
||||
old_values?: Record<string, any>;
|
||||
new_values?: Record<string, any>;
|
||||
}
|
||||
383
docs/api/template-matching-result-api.md
Normal file
383
docs/api/template-matching-result-api.md
Normal file
@@ -0,0 +1,383 @@
|
||||
# 模板匹配结果 API 文档
|
||||
|
||||
## 概述
|
||||
|
||||
模板匹配结果 API 提供了完整的模板匹配结果管理功能,包括保存匹配结果、查询历史记录、管理匹配详情等操作。
|
||||
|
||||
## 数据模型
|
||||
|
||||
### TemplateMatchingResult
|
||||
|
||||
模板匹配结果主要实体。
|
||||
|
||||
```typescript
|
||||
interface TemplateMatchingResult {
|
||||
id: string; // 匹配结果唯一标识
|
||||
project_id: string; // 项目ID
|
||||
template_id: string; // 模板ID
|
||||
binding_id: string; // 绑定ID
|
||||
result_name: string; // 结果名称
|
||||
description?: string; // 结果描述
|
||||
total_segments: number; // 总片段数
|
||||
matched_segments: number; // 匹配成功片段数
|
||||
failed_segments: number; // 匹配失败片段数
|
||||
success_rate: number; // 成功率(百分比)
|
||||
used_materials: number; // 使用的素材数量
|
||||
used_models: number; // 使用的模特数量
|
||||
matching_duration_ms: number; // 匹配耗时(毫秒)
|
||||
quality_score?: number; // 质量评分(0-5)
|
||||
status: MatchingResultStatus; // 匹配状态
|
||||
metadata?: string; // 额外元数据(JSON格式)
|
||||
created_at: string; // 创建时间
|
||||
updated_at: string; // 更新时间
|
||||
is_active: boolean; // 是否激活
|
||||
}
|
||||
```
|
||||
|
||||
### MatchingResultStatus
|
||||
|
||||
匹配结果状态枚举。
|
||||
|
||||
```typescript
|
||||
enum MatchingResultStatus {
|
||||
Success = 'Success', // 匹配成功
|
||||
PartialSuccess = 'PartialSuccess', // 部分成功
|
||||
Failed = 'Failed', // 匹配失败
|
||||
Cancelled = 'Cancelled' // 已取消
|
||||
}
|
||||
```
|
||||
|
||||
### MatchingSegmentResult
|
||||
|
||||
成功匹配的片段结果。
|
||||
|
||||
```typescript
|
||||
interface MatchingSegmentResult {
|
||||
id: string; // 片段结果ID
|
||||
matching_result_id: string; // 关联的匹配结果ID
|
||||
track_segment_id: string; // 模板轨道片段ID
|
||||
track_segment_name: string; // 轨道片段名称
|
||||
material_segment_id: string; // 匹配到的素材片段ID
|
||||
material_id: string; // 素材ID
|
||||
material_name: string; // 素材名称
|
||||
model_id?: string; // 模特ID
|
||||
model_name?: string; // 模特名称
|
||||
match_score: number; // 匹配评分(0-1)
|
||||
match_reason: string; // 匹配原因
|
||||
segment_duration: number; // 片段时长(微秒)
|
||||
start_time: number; // 开始时间(微秒)
|
||||
end_time: number; // 结束时间(微秒)
|
||||
properties?: string; // 片段属性(JSON格式)
|
||||
created_at: string; // 创建时间
|
||||
updated_at: string; // 更新时间
|
||||
}
|
||||
```
|
||||
|
||||
### MatchingFailedSegmentResult
|
||||
|
||||
匹配失败的片段结果。
|
||||
|
||||
```typescript
|
||||
interface MatchingFailedSegmentResult {
|
||||
id: string; // 失败片段结果ID
|
||||
matching_result_id: string; // 关联的匹配结果ID
|
||||
track_segment_id: string; // 模板轨道片段ID
|
||||
track_segment_name: string; // 轨道片段名称
|
||||
matching_rule_type: string; // 匹配规则类型
|
||||
matching_rule_data?: string; // 匹配规则数据(JSON格式)
|
||||
failure_reason: string; // 失败原因
|
||||
failure_details?: string; // 失败详情(JSON格式)
|
||||
segment_duration: number; // 片段时长(微秒)
|
||||
start_time: number; // 开始时间(微秒)
|
||||
end_time: number; // 结束时间(微秒)
|
||||
created_at: string; // 创建时间
|
||||
updated_at: string; // 更新时间
|
||||
}
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 1. 保存匹配结果
|
||||
|
||||
自动保存素材匹配结果到数据库。
|
||||
|
||||
```typescript
|
||||
save_matching_result(
|
||||
service_result: MaterialMatchingResult,
|
||||
result_name: string,
|
||||
description?: string,
|
||||
matching_duration_ms: number
|
||||
): Promise<TemplateMatchingResult>
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `service_result`: 素材匹配服务返回的匹配结果
|
||||
- `result_name`: 结果名称
|
||||
- `description`: 可选的结果描述
|
||||
- `matching_duration_ms`: 匹配耗时(毫秒)
|
||||
|
||||
**返回值:** 保存后的模板匹配结果
|
||||
|
||||
### 2. 获取匹配结果详情
|
||||
|
||||
获取包含所有片段信息的完整匹配结果详情。
|
||||
|
||||
```typescript
|
||||
get_matching_result_detail(result_id: string): Promise<TemplateMatchingResultDetail | null>
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `result_id`: 匹配结果ID
|
||||
|
||||
**返回值:** 匹配结果详情,包含成功和失败的片段信息
|
||||
|
||||
### 3. 获取项目匹配结果列表
|
||||
|
||||
获取指定项目的所有匹配结果。
|
||||
|
||||
```typescript
|
||||
get_project_matching_results(project_id: string): Promise<TemplateMatchingResult[]>
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `project_id`: 项目ID
|
||||
|
||||
**返回值:** 匹配结果列表
|
||||
|
||||
### 4. 获取模板匹配结果列表
|
||||
|
||||
获取指定模板的所有匹配结果。
|
||||
|
||||
```typescript
|
||||
get_template_matching_results(template_id: string): Promise<TemplateMatchingResult[]>
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `template_id`: 模板ID
|
||||
|
||||
**返回值:** 匹配结果列表
|
||||
|
||||
### 5. 获取绑定匹配结果列表
|
||||
|
||||
获取指定绑定的所有匹配结果。
|
||||
|
||||
```typescript
|
||||
get_binding_matching_results(binding_id: string): Promise<TemplateMatchingResult[]>
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `binding_id`: 绑定ID
|
||||
|
||||
**返回值:** 匹配结果列表
|
||||
|
||||
### 6. 查询匹配结果列表
|
||||
|
||||
根据查询条件获取匹配结果列表。
|
||||
|
||||
```typescript
|
||||
list_matching_results(options: TemplateMatchingResultQueryOptions): Promise<TemplateMatchingResult[]>
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `options`: 查询选项
|
||||
|
||||
```typescript
|
||||
interface TemplateMatchingResultQueryOptions {
|
||||
project_id?: string; // 项目ID过滤
|
||||
template_id?: string; // 模板ID过滤
|
||||
binding_id?: string; // 绑定ID过滤
|
||||
status?: MatchingResultStatus; // 状态过滤
|
||||
limit?: number; // 限制数量
|
||||
offset?: number; // 偏移量
|
||||
search_keyword?: string; // 搜索关键词
|
||||
sort_by?: string; // 排序字段
|
||||
sort_order?: string; // 排序顺序
|
||||
}
|
||||
```
|
||||
|
||||
### 7. 删除匹配结果
|
||||
|
||||
永久删除匹配结果。
|
||||
|
||||
```typescript
|
||||
delete_matching_result(result_id: string): Promise<boolean>
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `result_id`: 匹配结果ID
|
||||
|
||||
**返回值:** 是否删除成功
|
||||
|
||||
### 8. 软删除匹配结果
|
||||
|
||||
软删除匹配结果(标记为不活跃)。
|
||||
|
||||
```typescript
|
||||
soft_delete_matching_result(result_id: string): Promise<boolean>
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `result_id`: 匹配结果ID
|
||||
|
||||
**返回值:** 是否删除成功
|
||||
|
||||
### 9. 更新匹配结果信息
|
||||
|
||||
更新匹配结果的名称和描述。
|
||||
|
||||
```typescript
|
||||
update_matching_result_info(
|
||||
result_id: string,
|
||||
result_name?: string,
|
||||
description?: string
|
||||
): Promise<TemplateMatchingResult | null>
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `result_id`: 匹配结果ID
|
||||
- `result_name`: 新的结果名称(可选)
|
||||
- `description`: 新的结果描述(可选)
|
||||
|
||||
**返回值:** 更新后的匹配结果
|
||||
|
||||
### 10. 设置质量评分
|
||||
|
||||
为匹配结果设置质量评分。
|
||||
|
||||
```typescript
|
||||
set_matching_result_quality_score(
|
||||
result_id: string,
|
||||
quality_score: number
|
||||
): Promise<TemplateMatchingResult | null>
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `result_id`: 匹配结果ID
|
||||
- `quality_score`: 质量评分(0-5)
|
||||
|
||||
**返回值:** 更新后的匹配结果
|
||||
|
||||
### 11. 获取匹配统计信息
|
||||
|
||||
获取匹配结果的统计信息。
|
||||
|
||||
```typescript
|
||||
get_matching_statistics(project_id?: string): Promise<MatchingStatistics>
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `project_id`: 可选的项目ID,如果提供则只统计该项目的数据
|
||||
|
||||
**返回值:** 匹配统计信息
|
||||
|
||||
```typescript
|
||||
interface MatchingStatistics {
|
||||
total_results: number; // 总结果数
|
||||
successful_results: number; // 成功结果数
|
||||
total_segments: number; // 总片段数
|
||||
matched_segments: number; // 匹配片段数
|
||||
total_materials: number; // 总素材数
|
||||
total_models: number; // 总模特数
|
||||
average_success_rate: number; // 平均成功率
|
||||
}
|
||||
```
|
||||
|
||||
### 12. 执行匹配并自动保存
|
||||
|
||||
执行素材匹配并自动保存结果到数据库。
|
||||
|
||||
```typescript
|
||||
execute_material_matching_with_save(
|
||||
request: MaterialMatchingRequest,
|
||||
result_name: string,
|
||||
description?: string
|
||||
): Promise<[MaterialMatchingResult, TemplateMatchingResult | null]>
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `request`: 素材匹配请求
|
||||
- `result_name`: 结果名称
|
||||
- `description`: 可选的结果描述
|
||||
|
||||
**返回值:** 包含匹配结果和保存结果的元组
|
||||
|
||||
## 错误处理
|
||||
|
||||
所有 API 接口都会返回 `Result<T, String>` 类型,其中错误信息为字符串格式。常见错误包括:
|
||||
|
||||
- `"模板匹配结果不存在"`: 指定的匹配结果ID不存在
|
||||
- `"项目不存在"`: 指定的项目ID不存在
|
||||
- `"模板不存在"`: 指定的模板ID不存在
|
||||
- `"数据库操作失败"`: 数据库操作出现错误
|
||||
- `"参数验证失败"`: 输入参数不符合要求
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 保存匹配结果
|
||||
|
||||
```typescript
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
// 执行匹配并保存结果
|
||||
const result = await invoke('execute_material_matching_with_save', {
|
||||
request: {
|
||||
project_id: 'project-1',
|
||||
template_id: 'template-1',
|
||||
binding_id: 'binding-1',
|
||||
overwrite_existing: false,
|
||||
},
|
||||
resultName: '我的匹配结果',
|
||||
description: '这是一个测试匹配结果',
|
||||
});
|
||||
|
||||
console.log('匹配结果:', result[0]);
|
||||
console.log('保存结果:', result[1]);
|
||||
```
|
||||
|
||||
### 查询匹配结果
|
||||
|
||||
```typescript
|
||||
// 获取项目的所有匹配结果
|
||||
const results = await invoke('get_project_matching_results', {
|
||||
projectId: 'project-1',
|
||||
});
|
||||
|
||||
// 获取匹配结果详情
|
||||
const detail = await invoke('get_matching_result_detail', {
|
||||
resultId: 'result-1',
|
||||
});
|
||||
|
||||
// 查询匹配结果(带过滤条件)
|
||||
const filteredResults = await invoke('list_matching_results', {
|
||||
options: {
|
||||
project_id: 'project-1',
|
||||
status: 'Success',
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
sort_by: 'created_at',
|
||||
sort_order: 'desc',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 管理匹配结果
|
||||
|
||||
```typescript
|
||||
// 更新匹配结果信息
|
||||
await invoke('update_matching_result_info', {
|
||||
resultId: 'result-1',
|
||||
resultName: '更新后的名称',
|
||||
description: '更新后的描述',
|
||||
});
|
||||
|
||||
// 设置质量评分
|
||||
await invoke('set_matching_result_quality_score', {
|
||||
resultId: 'result-1',
|
||||
qualityScore: 4.5,
|
||||
});
|
||||
|
||||
// 软删除匹配结果
|
||||
await invoke('soft_delete_matching_result', {
|
||||
resultId: 'result-1',
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user