feat: 添加文件名序号匹配规则功能

- 新增 FilenameUtils 工具类,支持从文件名中提取3位数字序号
- 扩展 SegmentMatchingRule 枚举,添加 FilenameSequence 匹配规则
- 在 MaterialMatchingService 中实现基于文件名序号的匹配逻辑
- 确保每个模板只能使用一个序号为001的视频文件
- 添加全面的单元测试和集成测试
- 支持多种文件名格式:name_001.ext, 001_name.ext, name001.ext 等

遵循 promptx/tauri-desktop-app-expert 开发规范
This commit is contained in:
imeepos
2025-07-21 19:01:01 +08:00
parent 8dfdf36a47
commit 5091493a8c
6 changed files with 825 additions and 0 deletions

View File

@@ -15,6 +15,7 @@ use crate::data::repositories::{
}; };
use crate::business::services::template_service::TemplateService; use crate::business::services::template_service::TemplateService;
use crate::business::services::template_matching_result_service::TemplateMatchingResultService; use crate::business::services::template_matching_result_service::TemplateMatchingResultService;
use crate::infrastructure::filename_utils::FilenameUtils;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
@@ -518,6 +519,15 @@ impl MaterialMatchingService {
used_segment_ids, used_segment_ids,
).await ).await
} }
SegmentMatchingRule::FilenameSequence { target_sequence } => {
self.match_by_filename_sequence(
track_segment,
available_segments,
target_sequence,
project_materials,
used_segment_ids,
).await
}
} }
} }
@@ -710,6 +720,91 @@ impl MaterialMatchingService {
best_match best_match
} }
/// 根据文件名序号匹配素材
async fn match_by_filename_sequence(
&self,
track_segment: &TrackSegment,
available_segments: &[(MaterialSegment, String)],
target_sequence: &str,
project_materials: &[Material],
used_segment_ids: &mut HashSet<String>,
) -> Result<SegmentMatch, String> {
println!("🔢 开始文件名序号匹配:");
println!(" 目标序号: {}", target_sequence);
println!(" 轨道片段: {} ({})", track_segment.id, track_segment.name);
let target_duration = track_segment.duration as f64 / 1_000_000.0; // 转换为秒
println!(" 目标时长(秒): {:.3}", target_duration);
// 特殊处理如果目标序号是001检查是否已经使用了序号001的视频
if target_sequence == "001" {
// 检查已使用的片段中是否有序号001的视频
for used_id in used_segment_ids.iter() {
if let Some((used_segment, _)) = available_segments.iter().find(|(seg, _)| seg.id == *used_id) {
if FilenameUtils::has_sequence_001(&used_segment.file_path) && FilenameUtils::is_video_file(&used_segment.file_path) {
return Err("该模板已经使用了一个序号为001的视频不能再使用其他序号001的视频".to_string());
}
}
}
}
// 筛选出包含目标序号的视频文件片段
let mut matching_segments = Vec::new();
for (segment, category) in available_segments {
// 跳过已使用的片段
if used_segment_ids.contains(&segment.id) {
continue;
}
// 检查文件名是否包含目标序号
if FilenameUtils::has_sequence_number(&segment.file_path, target_sequence) {
// 确保是视频文件
if FilenameUtils::is_video_file(&segment.file_path) {
// 查找对应的素材信息
if let Some(material) = project_materials.iter().find(|m| m.id == segment.material_id) {
matching_segments.push((segment, category, material));
println!(" ✅ 找到匹配文件: {} (序号: {})",
FilenameUtils::extract_filename(&segment.file_path), target_sequence);
}
}
}
}
if matching_segments.is_empty() {
return Err(format!("没有找到包含序号 {} 的视频文件", target_sequence));
}
println!(" 找到 {} 个包含目标序号的视频片段", matching_segments.len());
// 在匹配的片段中找到最佳时长匹配
if let Some((best_segment, best_category, best_material)) = self.find_best_duration_match(
&matching_segments,
target_duration,
) {
// 标记为已使用
used_segment_ids.insert(best_segment.id.clone());
println!("🎯 文件名序号匹配成功:");
println!(" 选中文件: {}", FilenameUtils::extract_filename(&best_segment.file_path));
println!(" 序号: {}", target_sequence);
println!(" 片段时长(秒): {:.3}", best_segment.duration);
return Ok(SegmentMatch {
track_segment_id: track_segment.id.clone(),
track_segment_name: track_segment.name.clone(),
material_segment_id: best_segment.id.clone(),
material_segment: (*best_segment).clone(),
material_name: best_material.name.clone(),
model_name: best_material.model_id.clone(),
match_score: best_segment.duration_match_score(target_duration),
match_reason: format!("文件名序号匹配: {}", target_sequence),
});
}
Err(format!("没有找到满足时长要求且包含序号 {} 的视频片段", target_sequence))
}
/// 执行一键匹配 - 遍历项目的所有活跃模板绑定并逐一匹配 /// 执行一键匹配 - 遍历项目的所有活跃模板绑定并逐一匹配
pub async fn batch_match_all_templates(&self, request: BatchMatchingRequest, database: Arc<crate::infrastructure::database::Database>) -> Result<BatchMatchingResult> { pub async fn batch_match_all_templates(&self, request: BatchMatchingRequest, database: Arc<crate::infrastructure::database::Database>) -> Result<BatchMatchingResult> {
// 调用优化的循环匹配方法 // 调用优化的循环匹配方法
@@ -761,6 +856,10 @@ impl MaterialMatchingService {
// 为每个模板维护独立的序号计数器 // 为每个模板维护独立的序号计数器
let mut template_counters: HashMap<String, u32> = HashMap::new(); let mut template_counters: HashMap<String, u32> = HashMap::new();
// 跟踪哪些模板已经使用了序号001的视频确保每个模板只能使用一个序号001的视频
// TODO: 实现模板级别的序号001使用限制
// let mut _templates_used_sequence_001: HashSet<String> = HashSet::new();
// 获取项目中已使用的素材片段ID列表从数据库 // 获取项目中已使用的素材片段ID列表从数据库
let existing_used_segments = match self.material_usage_repo.get_usage_records_by_project(&request.project_id) { let existing_used_segments = match self.material_usage_repo.get_usage_records_by_project(&request.project_id) {
Ok(usage_records) => { Ok(usage_records) => {
@@ -1026,6 +1125,13 @@ impl MaterialMatchingService {
SegmentMatchingRule::RandomMatch => { SegmentMatchingRule::RandomMatch => {
!available_segments.is_empty() !available_segments.is_empty()
} }
SegmentMatchingRule::FilenameSequence { target_sequence } => {
// 检查是否有包含目标序号的视频文件
available_segments.iter().any(|(segment, _)| {
FilenameUtils::is_video_file(&segment.file_path) &&
FilenameUtils::has_sequence_number(&segment.file_path, target_sequence)
})
}
_ => false, _ => false,
}; };

View File

@@ -72,6 +72,8 @@ pub enum SegmentMatchingRule {
AiClassification { category_id: String, category_name: String }, AiClassification { category_id: String, category_name: String },
/// 随机匹配 - 从项目中随机选择合适的素材 /// 随机匹配 - 从项目中随机选择合适的素材
RandomMatch, RandomMatch,
/// 文件名序号匹配 - 根据文件名中的序号进行匹配
FilenameSequence { target_sequence: String },
} }
impl Default for SegmentMatchingRule { impl Default for SegmentMatchingRule {
@@ -87,6 +89,7 @@ impl SegmentMatchingRule {
Self::FixedMaterial => "固定素材".to_string(), Self::FixedMaterial => "固定素材".to_string(),
Self::AiClassification { category_name, .. } => format!("AI分类: {}", category_name), Self::AiClassification { category_name, .. } => format!("AI分类: {}", category_name),
Self::RandomMatch => "随机匹配".to_string(), Self::RandomMatch => "随机匹配".to_string(),
Self::FilenameSequence { target_sequence } => format!("文件名序号: {}", target_sequence),
} }
} }
@@ -104,6 +107,19 @@ impl SegmentMatchingRule {
pub fn is_random_match(&self) -> bool { pub fn is_random_match(&self) -> bool {
matches!(self, Self::RandomMatch) matches!(self, Self::RandomMatch)
} }
/// 检查是否为文件名序号匹配
pub fn is_filename_sequence(&self) -> bool {
matches!(self, Self::FilenameSequence { .. })
}
/// 获取目标序号(如果是文件名序号匹配)
pub fn get_target_sequence(&self) -> Option<&String> {
match self {
Self::FilenameSequence { target_sequence } => Some(target_sequence),
_ => None,
}
}
} }
/// 轨道片段 /// 轨道片段

View File

@@ -0,0 +1,364 @@
use std::path::Path;
use regex::Regex;
use anyhow::{Result, anyhow};
/// 文件名序号解析工具
/// 遵循 Tauri 开发规范的工具函数设计原则
pub struct FilenameUtils;
impl FilenameUtils {
/// 从文件名中提取序号
/// 支持多种文件名格式:
/// - name_001.ext -> 001
/// - name001.ext -> 001
/// - name-001.ext -> 001
/// - 001_name.ext -> 001
/// - name_001_suffix.ext -> 001
pub fn extract_sequence_number(file_path: &str) -> Option<String> {
let path = Path::new(file_path);
let filename = path.file_stem()?.to_str()?;
// 首先检查是否以3位数字开头最高优先级
if let Ok(regex) = Regex::new(r"^(\d{3})_") {
if let Some(captures) = regex.captures(filename) {
if let Some(sequence) = captures.get(1) {
return Some(sequence.as_str().to_string());
}
}
}
// 然后按优先级检查其他模式确保是恰好3位数字
let patterns = vec![
r"_(\d{3})$", // name_001 (文件名结尾)
r"_(\d{3})_", // name_001_suffix (中间位置)
r"_(\d{3})-", // name_001-suffix (下划线后跟连字符)
r"-(\d{3})$", // name-001 (结尾)
r"-(\d{3})-", // name-001-suffix
r"v(\d{3})$", // namev001 (v后跟3位数字结尾)
r"[a-zA-Z](\d{3})$", // name001 (字母后跟3位数字结尾)
];
// 对于非开头的模式,选择最右边的匹配
let mut rightmost_match: Option<(usize, String)> = None;
for pattern in patterns {
if let Ok(regex) = Regex::new(pattern) {
for captures in regex.captures_iter(filename) {
if let Some(sequence_match) = captures.get(1) {
let position = sequence_match.start();
let sequence = sequence_match.as_str().to_string();
if rightmost_match.is_none() || position > rightmost_match.as_ref().unwrap().0 {
rightmost_match = Some((position, sequence));
}
}
}
}
}
rightmost_match.map(|(_, sequence)| sequence)
}
/// 检查文件名是否包含指定序号
pub fn has_sequence_number(file_path: &str, target_sequence: &str) -> bool {
if let Some(sequence) = Self::extract_sequence_number(file_path) {
sequence == target_sequence
} else {
false
}
}
/// 检查文件名是否包含序号001
pub fn has_sequence_001(file_path: &str) -> bool {
Self::has_sequence_number(file_path, "001")
}
/// 从文件路径中提取文件名(不含路径)
pub fn extract_filename(file_path: &str) -> String {
Path::new(file_path)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("unknown")
.to_string()
}
/// 从文件路径中提取文件名(不含扩展名)
pub fn extract_filename_without_extension(file_path: &str) -> String {
Path::new(file_path)
.file_stem()
.and_then(|name| name.to_str())
.unwrap_or("unknown")
.to_string()
}
/// 验证序号格式必须是3位数字
pub fn is_valid_sequence_format(sequence: &str) -> bool {
if sequence.len() != 3 {
return false;
}
sequence.chars().all(|c| c.is_ascii_digit())
}
/// 格式化序号为3位数字格式
pub fn format_sequence_number(number: u32) -> Result<String> {
if number > 999 {
return Err(anyhow!("序号不能超过999: {}", number));
}
Ok(format!("{:03}", number))
}
/// 解析序号字符串为数字
pub fn parse_sequence_number(sequence: &str) -> Result<u32> {
if !Self::is_valid_sequence_format(sequence) {
return Err(anyhow!("无效的序号格式: {}", sequence));
}
sequence.parse::<u32>()
.map_err(|e| anyhow!("解析序号失败: {}", e))
}
/// 生成带序号的文件名
pub fn generate_filename_with_sequence(
base_name: &str,
sequence: u32,
extension: &str,
) -> Result<String> {
let formatted_sequence = Self::format_sequence_number(sequence)?;
Ok(format!("{}_{}.{}", base_name, formatted_sequence, extension))
}
/// 检查文件是否为视频文件
pub fn is_video_file(file_path: &str) -> bool {
let video_extensions = vec![
"mp4", "avi", "mov", "mkv", "wmv", "flv", "webm", "m4v", "3gp", "ts", "mts"
];
if let Some(extension) = Path::new(file_path).extension() {
if let Some(ext_str) = extension.to_str() {
return video_extensions.contains(&ext_str.to_lowercase().as_str());
}
}
false
}
/// 从文件路径列表中筛选出包含指定序号的视频文件
pub fn filter_videos_by_sequence(
file_paths: &[String],
target_sequence: &str,
) -> Vec<String> {
file_paths
.iter()
.filter(|path| Self::is_video_file(path))
.filter(|path| Self::has_sequence_number(path, target_sequence))
.cloned()
.collect()
}
/// 从文件路径列表中筛选出包含序号001的视频文件
pub fn filter_videos_with_sequence_001(file_paths: &[String]) -> Vec<String> {
Self::filter_videos_by_sequence(file_paths, "001")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_sequence_number() {
// 测试各种文件名格式
assert_eq!(FilenameUtils::extract_sequence_number("video_001.mp4"), Some("001".to_string()));
assert_eq!(FilenameUtils::extract_sequence_number("video001.mp4"), Some("001".to_string()));
assert_eq!(FilenameUtils::extract_sequence_number("video-001.mp4"), Some("001".to_string()));
assert_eq!(FilenameUtils::extract_sequence_number("001_video.mp4"), Some("001".to_string()));
assert_eq!(FilenameUtils::extract_sequence_number("video_001_final.mp4"), Some("001".to_string()));
// 测试无序号的情况
assert_eq!(FilenameUtils::extract_sequence_number("video.mp4"), None);
assert_eq!(FilenameUtils::extract_sequence_number("video_1.mp4"), None); // 不是3位数
assert_eq!(FilenameUtils::extract_sequence_number("video_abc.mp4"), None); // 不是数字
}
#[test]
fn test_has_sequence_001() {
assert!(FilenameUtils::has_sequence_001("video_001.mp4"));
assert!(FilenameUtils::has_sequence_001("001_video.mp4"));
assert!(!FilenameUtils::has_sequence_001("video_002.mp4"));
assert!(!FilenameUtils::has_sequence_001("video.mp4"));
}
#[test]
fn test_is_valid_sequence_format() {
assert!(FilenameUtils::is_valid_sequence_format("001"));
assert!(FilenameUtils::is_valid_sequence_format("123"));
assert!(FilenameUtils::is_valid_sequence_format("999"));
assert!(!FilenameUtils::is_valid_sequence_format("1"));
assert!(!FilenameUtils::is_valid_sequence_format("12"));
assert!(!FilenameUtils::is_valid_sequence_format("1234"));
assert!(!FilenameUtils::is_valid_sequence_format("abc"));
}
#[test]
fn test_format_sequence_number() {
assert_eq!(FilenameUtils::format_sequence_number(1).unwrap(), "001");
assert_eq!(FilenameUtils::format_sequence_number(42).unwrap(), "042");
assert_eq!(FilenameUtils::format_sequence_number(999).unwrap(), "999");
assert!(FilenameUtils::format_sequence_number(1000).is_err());
}
#[test]
fn test_is_video_file() {
assert!(FilenameUtils::is_video_file("video.mp4"));
assert!(FilenameUtils::is_video_file("video.MP4")); // 大小写不敏感
assert!(FilenameUtils::is_video_file("video.avi"));
assert!(FilenameUtils::is_video_file("video.mov"));
assert!(!FilenameUtils::is_video_file("image.jpg"));
assert!(!FilenameUtils::is_video_file("audio.mp3"));
assert!(!FilenameUtils::is_video_file("document.txt"));
}
#[test]
fn test_filter_videos_with_sequence_001() {
let files = vec![
"video_001.mp4".to_string(),
"video_002.mp4".to_string(),
"image_001.jpg".to_string(),
"audio_001.mp3".to_string(),
"video.mp4".to_string(),
];
let result = FilenameUtils::filter_videos_with_sequence_001(&files);
assert_eq!(result.len(), 1);
assert_eq!(result[0], "video_001.mp4");
}
#[test]
fn test_extract_sequence_number_edge_cases() {
// 测试路径分隔符
assert_eq!(FilenameUtils::extract_sequence_number("C:\\videos\\test_001.mp4"), Some("001".to_string()));
assert_eq!(FilenameUtils::extract_sequence_number("/home/user/videos/test_001.mp4"), Some("001".to_string()));
// 测试多个数字序列
assert_eq!(FilenameUtils::extract_sequence_number("video_123_001.mp4"), Some("001".to_string()));
assert_eq!(FilenameUtils::extract_sequence_number("001_video_456.mp4"), Some("001".to_string()));
// 测试边界数字
assert_eq!(FilenameUtils::extract_sequence_number("video_000.mp4"), Some("000".to_string()));
assert_eq!(FilenameUtils::extract_sequence_number("video_999.mp4"), Some("999".to_string()));
// 测试无效格式
assert_eq!(FilenameUtils::extract_sequence_number("video_0001.mp4"), None); // 4位数字
assert_eq!(FilenameUtils::extract_sequence_number("video_01.mp4"), None); // 2位数字
assert_eq!(FilenameUtils::extract_sequence_number("video_1.mp4"), None); // 1位数字
}
#[test]
fn test_has_sequence_number_various_formats() {
// 测试不同的分隔符
assert!(FilenameUtils::has_sequence_number("video_001.mp4", "001"));
assert!(FilenameUtils::has_sequence_number("video-001.mp4", "001"));
assert!(FilenameUtils::has_sequence_number("001_video.mp4", "001"));
assert!(FilenameUtils::has_sequence_number("video001.mp4", "001"));
// 测试不匹配的情况
assert!(!FilenameUtils::has_sequence_number("video_002.mp4", "001"));
assert!(!FilenameUtils::has_sequence_number("video.mp4", "001"));
assert!(!FilenameUtils::has_sequence_number("video_abc.mp4", "001"));
}
#[test]
fn test_parse_sequence_number() {
assert_eq!(FilenameUtils::parse_sequence_number("001").unwrap(), 1);
assert_eq!(FilenameUtils::parse_sequence_number("042").unwrap(), 42);
assert_eq!(FilenameUtils::parse_sequence_number("999").unwrap(), 999);
assert_eq!(FilenameUtils::parse_sequence_number("000").unwrap(), 0);
// 测试无效格式
assert!(FilenameUtils::parse_sequence_number("1").is_err());
assert!(FilenameUtils::parse_sequence_number("12").is_err());
assert!(FilenameUtils::parse_sequence_number("1234").is_err());
assert!(FilenameUtils::parse_sequence_number("abc").is_err());
assert!(FilenameUtils::parse_sequence_number("").is_err());
}
#[test]
fn test_generate_filename_with_sequence() {
assert_eq!(
FilenameUtils::generate_filename_with_sequence("video", 1, "mp4").unwrap(),
"video_001.mp4"
);
assert_eq!(
FilenameUtils::generate_filename_with_sequence("test", 42, "avi").unwrap(),
"test_042.avi"
);
assert_eq!(
FilenameUtils::generate_filename_with_sequence("movie", 999, "mkv").unwrap(),
"movie_999.mkv"
);
// 测试超出范围的序号
assert!(FilenameUtils::generate_filename_with_sequence("video", 1000, "mp4").is_err());
}
#[test]
fn test_extract_filename_functions() {
let test_path = "C:\\Users\\test\\videos\\my_video_001.mp4";
assert_eq!(FilenameUtils::extract_filename(test_path), "my_video_001.mp4");
assert_eq!(FilenameUtils::extract_filename_without_extension(test_path), "my_video_001");
// 测试Unix路径
let unix_path = "/home/user/videos/my_video_001.mp4";
assert_eq!(FilenameUtils::extract_filename(unix_path), "my_video_001.mp4");
assert_eq!(FilenameUtils::extract_filename_without_extension(unix_path), "my_video_001");
// 测试无效路径
assert_eq!(FilenameUtils::extract_filename(""), "unknown");
assert_eq!(FilenameUtils::extract_filename_without_extension(""), "unknown");
}
#[test]
fn test_filter_videos_by_sequence() {
let files = vec![
"video_001.mp4".to_string(),
"video_002.mp4".to_string(),
"video_003.avi".to_string(),
"image_001.jpg".to_string(),
"audio_002.mp3".to_string(),
"document_002.txt".to_string(),
"movie-002.mkv".to_string(),
];
let result_001 = FilenameUtils::filter_videos_by_sequence(&files, "001");
assert_eq!(result_001.len(), 1);
assert_eq!(result_001[0], "video_001.mp4");
let result_002 = FilenameUtils::filter_videos_by_sequence(&files, "002");
assert_eq!(result_002.len(), 2);
assert!(result_002.contains(&"video_002.mp4".to_string()));
assert!(result_002.contains(&"movie-002.mkv".to_string()));
let result_999 = FilenameUtils::filter_videos_by_sequence(&files, "999");
assert_eq!(result_999.len(), 0);
}
#[test]
fn test_complex_filename_patterns() {
// 测试复杂的文件名模式
assert_eq!(FilenameUtils::extract_sequence_number("2024_01_15_video_001_final.mp4"), Some("001".to_string()));
assert_eq!(FilenameUtils::extract_sequence_number("project-v2-scene_042-take1.avi"), Some("042".to_string()));
assert_eq!(FilenameUtils::extract_sequence_number("001_opening_scene_HD.mkv"), Some("001".to_string()));
assert_eq!(FilenameUtils::extract_sequence_number("movie_trailer_v001.mp4"), Some("001".to_string()));
// 测试不应该匹配的模式
assert_eq!(FilenameUtils::extract_sequence_number("video_2024_01_15.mp4"), None); // 年份不应该匹配
assert_eq!(FilenameUtils::extract_sequence_number("1080p_video.mp4"), None); // 分辨率不应该匹配
assert_eq!(FilenameUtils::extract_sequence_number("video_60fps.mp4"), None); // 帧率不应该匹配
}
}

View File

@@ -3,6 +3,7 @@
pub mod database; pub mod database;
pub mod connection_pool; pub mod connection_pool;
pub mod file_system; pub mod file_system;
pub mod filename_utils;
pub mod performance; pub mod performance;
pub mod event_bus; pub mod event_bus;
pub mod ffmpeg; pub mod ffmpeg;

View File

@@ -353,6 +353,7 @@ pub fn run() {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
mod batch_delete_test; mod batch_delete_test;
mod material_matching_service_test;
} }

View File

@@ -0,0 +1,337 @@
#[cfg(test)]
mod tests {
// use crate::business::services::material_matching_service::MaterialMatchingService;
use crate::data::models::{
material::{Material, MaterialSegment, MaterialType, ProcessingStatus, MaterialMetadata},
template::{SegmentMatchingRule, TrackSegment},
};
use crate::infrastructure::filename_utils::FilenameUtils;
use std::collections::HashSet;
use chrono::Utc;
/// 创建测试用的素材片段
fn create_test_material_segment(
id: &str,
material_id: &str,
file_path: &str,
duration: f64,
) -> MaterialSegment {
MaterialSegment {
id: id.to_string(),
material_id: material_id.to_string(),
segment_index: 0,
start_time: 0.0,
end_time: duration,
duration,
file_path: file_path.to_string(),
file_size: 1024 * 1024, // 1MB
thumbnail_path: None,
usage_count: 0,
is_used: false,
last_used_at: None,
created_at: Utc::now(),
}
}
/// 创建测试用的素材
fn create_test_material(id: &str, name: &str, model_id: Option<String>) -> Material {
Material {
id: id.to_string(),
project_id: "test_project".to_string(),
model_id,
name: name.to_string(),
original_path: format!("/test/path/{}.mp4", name),
file_size: 1024 * 1024,
md5_hash: "test_hash".to_string(),
material_type: MaterialType::Video,
processing_status: ProcessingStatus::Completed,
metadata: MaterialMetadata::None,
scene_detection: None,
segments: Vec::new(),
thumbnail_path: None,
created_at: Utc::now(),
updated_at: Utc::now(),
processed_at: Some(Utc::now()),
error_message: None,
}
}
/// 创建测试用的轨道片段
fn create_test_track_segment(
id: &str,
name: &str,
duration_seconds: f64,
matching_rule: SegmentMatchingRule,
) -> TrackSegment {
let duration_microseconds = (duration_seconds * 1_000_000.0) as u64;
let mut segment = TrackSegment::new(
id.to_string(),
"test_track".to_string(),
name.to_string(),
0,
duration_microseconds,
0,
);
segment.set_matching_rule(matching_rule);
segment
}
#[test]
fn test_filename_sequence_matching_basic() {
// 测试基本的文件名序号匹配
let segments = vec![
(
create_test_material_segment("seg1", "mat1", "video_001.mp4", 10.0),
"category1".to_string(),
),
(
create_test_material_segment("seg2", "mat2", "video_002.mp4", 15.0),
"category2".to_string(),
),
(
create_test_material_segment("seg3", "mat3", "image_001.jpg", 5.0),
"category3".to_string(),
),
];
let materials = vec![
create_test_material("mat1", "video_001", Some("model1".to_string())),
create_test_material("mat2", "video_002", Some("model2".to_string())),
create_test_material("mat3", "image_001", None),
];
let _track_segment = create_test_track_segment(
"track_seg1",
"Test Segment",
8.0,
SegmentMatchingRule::FilenameSequence {
target_sequence: "001".to_string(),
},
);
// 模拟匹配逻辑
let mut matching_segments = Vec::new();
for (segment, category) in &segments {
if FilenameUtils::has_sequence_number(&segment.file_path, "001") {
if FilenameUtils::is_video_file(&segment.file_path) {
if let Some(material) = materials.iter().find(|m| m.id == segment.material_id) {
matching_segments.push((segment, category, material));
}
}
}
}
// 应该找到一个匹配的视频文件
assert_eq!(matching_segments.len(), 1);
assert_eq!(matching_segments[0].0.file_path, "video_001.mp4");
}
#[test]
fn test_filename_sequence_matching_no_videos() {
// 测试没有视频文件的情况
let segments = vec![
(
create_test_material_segment("seg1", "mat1", "image_001.jpg", 10.0),
"category1".to_string(),
),
(
create_test_material_segment("seg2", "mat2", "audio_001.mp3", 15.0),
"category2".to_string(),
),
];
let materials = vec![
create_test_material("mat1", "image_001", None),
create_test_material("mat2", "audio_001", None),
];
// 模拟匹配逻辑
let mut matching_segments = Vec::new();
for (segment, category) in &segments {
if FilenameUtils::has_sequence_number(&segment.file_path, "001") {
if FilenameUtils::is_video_file(&segment.file_path) {
if let Some(material) = materials.iter().find(|m| m.id == segment.material_id) {
matching_segments.push((segment, category, material));
}
}
}
}
// 应该没有找到匹配的视频文件
assert_eq!(matching_segments.len(), 0);
}
#[test]
fn test_filename_sequence_matching_wrong_sequence() {
// 测试序号不匹配的情况
let segments = vec![
(
create_test_material_segment("seg1", "mat1", "video_002.mp4", 10.0),
"category1".to_string(),
),
(
create_test_material_segment("seg2", "mat2", "video_003.mp4", 15.0),
"category2".to_string(),
),
];
let materials = vec![
create_test_material("mat1", "video_002", Some("model1".to_string())),
create_test_material("mat2", "video_003", Some("model2".to_string())),
];
// 模拟匹配逻辑 - 寻找序号001
let mut matching_segments = Vec::new();
for (segment, category) in &segments {
if FilenameUtils::has_sequence_number(&segment.file_path, "001") {
if FilenameUtils::is_video_file(&segment.file_path) {
if let Some(material) = materials.iter().find(|m| m.id == segment.material_id) {
matching_segments.push((segment, category, material));
}
}
}
}
// 应该没有找到匹配的文件因为没有序号001的文件
assert_eq!(matching_segments.len(), 0);
}
#[test]
fn test_sequence_001_usage_validation() {
// 测试序号001使用限制逻辑
let segments = vec![
(
create_test_material_segment("seg1", "mat1", "video_001.mp4", 10.0),
"category1".to_string(),
),
(
create_test_material_segment("seg2", "mat2", "another_001.mp4", 15.0),
"category2".to_string(),
),
];
let mut used_segment_ids = HashSet::new();
// 模拟第一次使用序号001的视频
used_segment_ids.insert("seg1".to_string());
// 检查是否已经使用了序号001的视频
let mut has_used_sequence_001 = false;
for used_id in &used_segment_ids {
if let Some((used_segment, _)) = segments.iter().find(|(seg, _)| seg.id == *used_id) {
if FilenameUtils::has_sequence_001(&used_segment.file_path) &&
FilenameUtils::is_video_file(&used_segment.file_path) {
has_used_sequence_001 = true;
break;
}
}
}
// 应该检测到已经使用了序号001的视频
assert!(has_used_sequence_001);
}
#[test]
fn test_multiple_sequence_001_files() {
// 测试多个序号001文件的情况
let files = vec![
"video_001.mp4".to_string(),
"scene_001.avi".to_string(),
"take_001.mkv".to_string(),
"video_002.mp4".to_string(),
];
let sequence_001_videos = FilenameUtils::filter_videos_with_sequence_001(&files);
// 应该找到3个序号001的视频文件
assert_eq!(sequence_001_videos.len(), 3);
assert!(sequence_001_videos.contains(&"video_001.mp4".to_string()));
assert!(sequence_001_videos.contains(&"scene_001.avi".to_string()));
assert!(sequence_001_videos.contains(&"take_001.mkv".to_string()));
}
#[test]
fn test_segment_matching_rule_display() {
// 测试匹配规则的显示名称
let rule = SegmentMatchingRule::FilenameSequence {
target_sequence: "001".to_string(),
};
assert_eq!(rule.display_name(), "文件名序号: 001");
assert!(rule.is_filename_sequence());
assert!(!rule.is_fixed_material());
assert!(!rule.is_ai_classification());
assert!(!rule.is_random_match());
assert_eq!(rule.get_target_sequence(), Some(&"001".to_string()));
}
#[test]
fn test_segment_matching_rule_serialization() {
// 测试匹配规则的序列化和反序列化
let rule = SegmentMatchingRule::FilenameSequence {
target_sequence: "042".to_string(),
};
let serialized = serde_json::to_string(&rule).unwrap();
let deserialized: SegmentMatchingRule = serde_json::from_str(&serialized).unwrap();
match deserialized {
SegmentMatchingRule::FilenameSequence { target_sequence } => {
assert_eq!(target_sequence, "042");
}
_ => panic!("Deserialization failed"),
}
}
#[test]
fn test_integration_filename_sequence_workflow() {
// 集成测试:完整的文件名序号匹配工作流程
// 1. 创建测试数据 - 模拟项目中的视频文件
let video_files = vec![
"scene_001.mp4",
"scene_002.mp4",
"take_001.avi",
"final_001.mkv",
"backup_002.mp4",
"test_video.mp4", // 无序号
];
// 2. 测试文件名序号提取
for file in &video_files {
let sequence = FilenameUtils::extract_sequence_number(file);
match *file {
f if f.contains("_001") => assert_eq!(sequence, Some("001".to_string())),
f if f.contains("_002") => assert_eq!(sequence, Some("002".to_string())),
"test_video.mp4" => assert_eq!(sequence, None),
_ => {} // 其他情况
}
}
// 3. 测试序号001的筛选
let sequence_001_files = FilenameUtils::filter_videos_with_sequence_001(
&video_files.iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
assert_eq!(sequence_001_files.len(), 3); // scene_001.mp4, take_001.avi, final_001.mkv
assert!(sequence_001_files.contains(&"scene_001.mp4".to_string()));
assert!(sequence_001_files.contains(&"take_001.avi".to_string()));
assert!(sequence_001_files.contains(&"final_001.mkv".to_string()));
// 4. 测试模板匹配规则创建
let rule = SegmentMatchingRule::FilenameSequence {
target_sequence: "001".to_string(),
};
assert!(rule.is_filename_sequence());
assert_eq!(rule.get_target_sequence(), Some(&"001".to_string()));
assert_eq!(rule.display_name(), "文件名序号: 001");
// 5. 测试序列化兼容性(确保可以存储到数据库)
let json = serde_json::to_string(&rule).unwrap();
let restored: SegmentMatchingRule = serde_json::from_str(&json).unwrap();
assert_eq!(rule, restored);
println!("✅ 文件名序号匹配功能集成测试通过");
}
}