Files
mixvideo-v2/apps/desktop/src-tauri/src/business/services/outfit_matching_service.rs
imeepos 7b1bb2fb0e feat: 实现服装搭配功能的图像上传和AI分析
新功能:
- 实现完整的图像上传组件 (ImageUploader)
  - 支持拖拽上传、文件选择、预览和验证
  - 文件大小和格式限制
  - 实时预览和文件管理
- 实现AI图像分析结果展示 (OutfitAnalysisResult)
  - 实时状态更新和进度跟踪
  - 分析结果详情查看
  - 自动刷新机制
- 添加文件保存功能 (save_outfit_image 命令)
- 更新OutfitMatch页面为现代化标签页设计

 技术改进:
- 修复服装搭配相关模型的编译错误
- 添加缺失的trait实现 (Eq, Hash)
- 修复生命周期参数问题
- 完善错误处理和用户反馈

 UI/UX优化:
- 现代化的标签页设计
- 响应式布局和优雅动画
- 统一的设计语言和交互体验
- 完善的加载状态和错误处理
2025-07-17 19:05:40 +08:00

964 lines
34 KiB
Rust

use crate::data::models::outfit_matching::{
OutfitMatching, CreateOutfitMatchingRequest, UpdateOutfitMatchingRequest,
OutfitMatchingQueryOptions, OutfitMatchingStats, MatchingType,
MatchingScoreDetails, MatchingSuggestion, MatchingOutfitItem,
SmartMatchingRequest, SmartMatchingResult, ConfidenceLevel
};
use crate::data::models::outfit_item::{OutfitItem, OutfitCategory, OutfitStyle};
use crate::data::models::outfit_analysis::ColorHSV;
use crate::data::repositories::outfit_matching_repository::OutfitMatchingRepository;
use crate::data::repositories::outfit_item_repository::OutfitItemRepository;
use crate::business::errors::BusinessError;
use anyhow::Result;
use std::sync::Arc;
use std::collections::HashMap;
/// 服装搭配业务服务
/// 遵循 Tauri 开发规范的业务逻辑层设计原则
pub struct OutfitMatchingService {
repository: Arc<OutfitMatchingRepository>,
item_repository: Arc<OutfitItemRepository>,
}
impl OutfitMatchingService {
/// 创建新的服装搭配服务实例
pub fn new(
repository: Arc<OutfitMatchingRepository>,
item_repository: Arc<OutfitItemRepository>
) -> Self {
Self { repository, item_repository }
}
/// 创建服装搭配
pub async fn create_matching(&self, request: CreateOutfitMatchingRequest) -> Result<OutfitMatching> {
// 验证输入数据
self.validate_create_request(&request)?;
// 检查项目中是否存在相同名称的搭配
let existing_matchings = self.repository.list(&OutfitMatchingQueryOptions {
project_id: Some(request.project_id.clone()),
filters: None,
sort_by: None,
sort_order: None,
limit: None,
offset: None,
})?;
if existing_matchings.iter().any(|m| m.matching_name == request.matching_name) {
return Err(BusinessError::DuplicateName(request.matching_name).into());
}
// 检查所有单品是否存在
let mut items = Vec::new();
for item_id in &request.item_ids {
let item = self.item_repository.get_by_id(item_id)?
.ok_or_else(|| BusinessError::NotFound(format!("服装单品不存在: {}", item_id)))?;
items.push(item);
}
// 创建搭配记录
let matching = self.repository.create(&request)?;
// 计算搭配评分和建议
let (score_details, suggestions) = self.calculate_matching_score_and_suggestions(&items);
// 构建搭配单品列表
let matching_items = self.build_matching_items(&items);
// 提取色彩搭配方案
let color_palette = self.extract_color_palette(&items);
// 更新搭配记录
let mut update_request = UpdateOutfitMatchingRequest {
matching_name: None,
matching_type: None,
item_ids: None,
occasion_tags: None,
season_tags: None,
style_description: None,
is_favorite: None,
};
// 更新搭配记录(在实际实现中,这些字段应该直接在创建时设置)
// 这里模拟更新操作
self.repository.update(&matching.id, &update_request)?;
// 返回完整的搭配记录
let updated_matching = self.repository.get_by_id(&matching.id)?
.ok_or_else(|| BusinessError::NotFound(format!("搭配记录不存在: {}", matching.id)))?;
Ok(updated_matching)
}
/// 获取服装搭配列表
pub async fn get_matchings(&self, options: OutfitMatchingQueryOptions) -> Result<Vec<OutfitMatching>> {
Ok(self.repository.list(&options)?)
}
/// 根据ID获取服装搭配
pub async fn get_matching_by_id(&self, id: &str) -> Result<Option<OutfitMatching>> {
if id.trim().is_empty() {
return Err(BusinessError::InvalidInput("ID不能为空".to_string()).into());
}
Ok(self.repository.get_by_id(id)?)
}
/// 更新服装搭配
pub async fn update_matching(&self, id: &str, request: UpdateOutfitMatchingRequest) -> Result<()> {
if id.trim().is_empty() {
return Err(BusinessError::InvalidInput("ID不能为空".to_string()).into());
}
// 检查搭配是否存在
let matching = self.repository.get_by_id(id)?
.ok_or_else(|| BusinessError::NotFound(format!("搭配记录不存在: {}", id)))?;
// 检查名称是否重复
if let Some(name) = &request.matching_name {
if name != &matching.matching_name {
let existing_matchings = self.repository.list(&OutfitMatchingQueryOptions {
project_id: Some(matching.project_id.clone()),
filters: None,
sort_by: None,
sort_order: None,
limit: None,
offset: None,
})?;
if existing_matchings.iter().any(|m| m.matching_name == *name) {
return Err(BusinessError::DuplicateName(name.clone()).into());
}
}
}
// 更新搭配记录
self.repository.update(id, &request)?;
Ok(())
}
/// 删除服装搭配
pub async fn delete_matching(&self, id: &str) -> Result<()> {
if id.trim().is_empty() {
return Err(BusinessError::InvalidInput("ID不能为空".to_string()).into());
}
// 检查搭配是否存在
if self.repository.get_by_id(id)?.is_none() {
return Err(BusinessError::NotFound(format!("搭配记录不存在: {}", id)).into());
}
self.repository.delete(id)?;
Ok(())
}
/// 增加穿着次数
pub async fn increment_wear_count(&self, id: &str) -> Result<()> {
if id.trim().is_empty() {
return Err(BusinessError::InvalidInput("ID不能为空".to_string()).into());
}
// 检查搭配是否存在
if self.repository.get_by_id(id)?.is_none() {
return Err(BusinessError::NotFound(format!("搭配记录不存在: {}", id)).into());
}
self.repository.increment_wear_count(id)?;
Ok(())
}
/// 智能搭配推荐
pub async fn smart_matching(&self, request: SmartMatchingRequest) -> Result<SmartMatchingResult> {
// 验证请求
if request.project_id.trim().is_empty() {
return Err(BusinessError::InvalidInput("项目ID不能为空".to_string()).into());
}
// 获取项目中的所有单品
let all_items = self.item_repository.list(&crate::data::models::outfit_item::OutfitItemQueryOptions {
project_id: Some(request.project_id.clone()),
category: None,
styles: None,
color_similarity_threshold: None,
target_color: None,
brand: None,
tags: None,
limit: None,
offset: None,
})?;
// 过滤掉排除的单品
let candidate_items: Vec<OutfitItem> = if let Some(exclude_ids) = &request.exclude_item_ids {
all_items.into_iter()
.filter(|item| !exclude_ids.contains(&item.id))
.collect()
} else {
all_items
};
// 如果指定了基础单品,则基于它进行搭配
let mut base_item: Option<OutfitItem> = None;
if let Some(base_id) = &request.base_item_id {
base_item = self.item_repository.get_by_id(base_id)?;
if base_item.is_none() {
return Err(BusinessError::NotFound(format!("基础单品不存在: {}", base_id)).into());
}
}
// 生成搭配组合
let matchings = self.generate_outfit_combinations(
&candidate_items,
base_item.as_ref(),
request.preferred_styles.as_deref(),
request.target_occasion.as_deref(),
request.target_season.as_deref(),
request.color_preferences.as_deref(),
request.max_results.unwrap_or(5)
)?;
// 构建推荐结果
let result = SmartMatchingResult {
recommended_matchings: matchings,
reasoning: "基于您的偏好和搭配规则生成的推荐搭配".to_string(),
confidence_score: 0.85, // 示例置信度
};
Ok(result)
}
/// 生成服装搭配组合
fn generate_outfit_combinations(
&self,
items: &[OutfitItem],
base_item: Option<&OutfitItem>,
preferred_styles: Option<&[OutfitStyle]>,
target_occasion: Option<&str>,
target_season: Option<&str>,
color_preferences: Option<&[ColorHSV]>,
max_results: u32
) -> Result<Vec<OutfitMatching>> {
// 按类别分组单品
let mut items_by_category: HashMap<OutfitCategory, Vec<&OutfitItem>> = HashMap::new();
for item in items {
items_by_category.entry(item.category.clone())
.or_insert_with(Vec::new)
.push(item);
}
// 生成搭配组合(简化版)
let mut matchings = Vec::new();
// 如果有基础单品,则基于它生成搭配
if let Some(base) = base_item {
// 为每个类别选择最匹配的单品
let mut matching_items = vec![base.clone()];
for (category, category_items) in &items_by_category {
// 跳过与基础单品相同类别的单品
if *category == base.category {
continue;
}
// 选择最匹配的单品
if let Some(best_match) = self.find_best_matching_item(
base,
category_items,
preferred_styles,
color_preferences
) {
matching_items.push(best_match.clone());
}
}
// 创建搭配
if matching_items.len() > 1 {
let matching = self.create_matching_from_items(
&matching_items,
MatchingType::StyleConsistent,
target_occasion,
target_season
);
matchings.push(matching);
}
} else {
// 没有基础单品,生成多种搭配组合
// 这里简化实现,实际应用中应该有更复杂的算法
// 尝试上装+下装的组合
if let (Some(tops), Some(bottoms)) = (
items_by_category.get(&OutfitCategory::Top),
items_by_category.get(&OutfitCategory::Bottom)
) {
for top in tops.iter().take(3) {
for bottom in bottoms.iter().take(3) {
let mut outfit_items = vec![(*top).clone(), (*bottom).clone()];
// 可能添加外套
if let Some(outerwears) = items_by_category.get(&OutfitCategory::Outerwear) {
if let Some(outerwear) = outerwears.first() {
outfit_items.push((*outerwear).clone());
}
}
// 可能添加鞋子
if let Some(footwears) = items_by_category.get(&OutfitCategory::Footwear) {
if let Some(footwear) = footwears.first() {
outfit_items.push((*footwear).clone());
}
}
let matching = self.create_matching_from_items(
&outfit_items,
MatchingType::StyleConsistent,
target_occasion,
target_season
);
matchings.push(matching);
if matchings.len() >= max_results as usize {
break;
}
}
if matchings.len() >= max_results as usize {
break;
}
}
}
// 尝试连衣裙+配饰的组合
if matchings.len() < max_results as usize {
if let Some(dresses) = items_by_category.get(&OutfitCategory::Dress) {
for dress in dresses.iter().take(2) {
let mut outfit_items = vec![(*dress).clone()];
// 添加配饰
if let Some(accessories) = items_by_category.get(&OutfitCategory::Accessory) {
if let Some(accessory) = accessories.first() {
outfit_items.push((*accessory).clone());
}
}
// 可能添加外套
if let Some(outerwears) = items_by_category.get(&OutfitCategory::Outerwear) {
if let Some(outerwear) = outerwears.first() {
outfit_items.push((*outerwear).clone());
}
}
let matching = self.create_matching_from_items(
&outfit_items,
MatchingType::StyleConsistent,
target_occasion,
target_season
);
matchings.push(matching);
if matchings.len() >= max_results as usize {
break;
}
}
}
}
}
// 限制结果数量
matchings.truncate(max_results as usize);
Ok(matchings)
}
/// 查找最匹配的单品
fn find_best_matching_item<'a>(
&self,
base_item: &OutfitItem,
candidates: &[&'a OutfitItem],
preferred_styles: Option<&[OutfitStyle]>,
color_preferences: Option<&[ColorHSV]>
) -> Option<&'a OutfitItem> {
if candidates.is_empty() {
return None;
}
// 计算每个候选项的匹配分数
let mut scored_candidates: Vec<(&OutfitItem, f64)> = candidates.iter()
.map(|item| {
let mut score = 0.0;
// 颜色匹配度 (权重: 0.4)
let color_similarity = base_item.color_primary.similarity(&item.color_primary);
score += color_similarity * 0.4;
// 风格匹配度 (权重: 0.3)
let style_similarity = self.calculate_style_similarity(&base_item.styles, &item.styles);
score += style_similarity * 0.3;
// 偏好风格匹配 (权重: 0.2)
if let Some(styles) = preferred_styles {
let preferred_match = item.styles.iter()
.filter(|style| styles.contains(style))
.count() as f64 / styles.len().max(1) as f64;
score += preferred_match * 0.2;
}
// 偏好颜色匹配 (权重: 0.1)
if let Some(colors) = color_preferences {
let mut best_color_match: f64 = 0.0;
for color in colors {
let match_score = item.color_primary.similarity(color);
best_color_match = best_color_match.max(match_score);
}
score += best_color_match * 0.1;
}
(*item, score)
})
.collect();
// 按分数降序排序
scored_candidates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
// 返回最高分的候选项
scored_candidates.first().map(move |(item, _)| *item)
}
/// 从单品列表创建搭配
fn create_matching_from_items(
&self,
items: &[OutfitItem],
matching_type: MatchingType,
occasion: Option<&str>,
season: Option<&str>
) -> OutfitMatching {
// 计算搭配评分和建议
let (score_details, suggestions) = self.calculate_matching_score_and_suggestions(items);
// 构建搭配单品列表
let matching_items = self.build_matching_items(items);
// 提取色彩搭配方案
let color_palette = self.extract_color_palette(items);
// 生成风格描述
let style_description = self.generate_style_description(items);
// 构建场合标签
let occasion_tags = if let Some(occ) = occasion {
vec![occ.to_string()]
} else {
Vec::new()
};
// 构建季节标签
let season_tags = if let Some(sea) = season {
vec![sea.to_string()]
} else {
Vec::new()
};
// 创建搭配记录
OutfitMatching {
id: uuid::Uuid::new_v4().to_string(),
project_id: items.first().map(|i| i.project_id.clone()).unwrap_or_default(),
matching_name: format!("搭配 {}", chrono::Utc::now().format("%Y%m%d%H%M%S")),
matching_type,
items: matching_items,
score_details,
suggestions,
occasion_tags,
season_tags,
style_description,
color_palette,
is_favorite: false,
wear_count: 0,
last_worn_date: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
}
}
/// 计算搭配评分和建议
fn calculate_matching_score_and_suggestions(&self, items: &[OutfitItem]) -> (MatchingScoreDetails, Vec<MatchingSuggestion>) {
// 计算颜色和谐度
let color_harmony_score = self.calculate_color_harmony(items);
// 计算风格一致性
let style_consistency_score = self.calculate_style_consistency(items);
// 计算比例协调度
let proportion_score = 0.75; // 简化实现
// 计算场合适宜度
let occasion_appropriateness = 0.8; // 简化实现
// 计算时尚度
let trend_factor = 0.7; // 简化实现
// 创建评分详情
let mut score_details = MatchingScoreDetails {
color_harmony_score,
style_consistency_score,
proportion_score,
occasion_appropriateness,
trend_factor,
overall_score: 0.0,
};
// 计算综合评分
score_details.calculate_overall_score();
// 生成搭配建议
let mut suggestions = Vec::new();
// 如果颜色和谐度较低,添加颜色建议
if color_harmony_score < 0.6 {
suggestions.push(MatchingSuggestion {
suggestion_type: "颜色调整".to_string(),
description: "考虑选择更协调的颜色组合,可以尝试使用互补色或类似色".to_string(),
priority: 4,
});
}
// 如果风格一致性较低,添加风格建议
if style_consistency_score < 0.6 {
suggestions.push(MatchingSuggestion {
suggestion_type: "风格优化".to_string(),
description: "搭配中的单品风格不够统一,建议选择风格更一致的单品".to_string(),
priority: 3,
});
}
(score_details, suggestions)
}
/// 计算颜色和谐度
fn calculate_color_harmony(&self, items: &[OutfitItem]) -> f64 {
if items.len() <= 1 {
return 1.0; // 单个单品默认和谐度为1.0
}
let mut total_similarity = 0.0;
let mut comparison_count = 0;
// 计算所有单品之间的颜色相似度
for i in 0..items.len() {
for j in i+1..items.len() {
let similarity = items[i].color_primary.similarity(&items[j].color_primary);
total_similarity += similarity;
comparison_count += 1;
}
}
if comparison_count == 0 {
return 1.0;
}
// 返回平均相似度
total_similarity / comparison_count as f64
}
/// 计算风格一致性
fn calculate_style_consistency(&self, items: &[OutfitItem]) -> f64 {
if items.len() <= 1 {
return 1.0; // 单个单品默认一致性为1.0
}
// 收集所有风格
let mut all_styles = Vec::new();
for item in items {
for style in &item.styles {
if !all_styles.contains(style) {
all_styles.push(style.clone());
}
}
}
if all_styles.is_empty() {
return 0.5; // 没有风格信息,返回中等一致性
}
// 计算每个单品与所有风格的匹配度
let mut total_consistency = 0.0;
for item in items {
let mut style_matches = 0;
for style in &all_styles {
if item.styles.contains(style) {
style_matches += 1;
}
}
let item_consistency = style_matches as f64 / all_styles.len() as f64;
total_consistency += item_consistency;
}
// 返回平均一致性
total_consistency / items.len() as f64
}
/// 构建搭配单品列表
fn build_matching_items(&self, items: &[OutfitItem]) -> Vec<MatchingOutfitItem> {
items.iter().map(|item| {
MatchingOutfitItem {
item_id: item.id.clone(),
item_name: item.name.clone(),
category: item.category.clone(),
color_primary: item.color_primary.clone(),
styles: item.styles.clone(),
role_in_outfit: self.determine_item_role(item),
image_url: item.image_urls.first().cloned(),
}
}).collect()
}
/// 确定单品在搭配中的角色
fn determine_item_role(&self, item: &OutfitItem) -> String {
match item.category {
OutfitCategory::Top | OutfitCategory::Bottom | OutfitCategory::Dress => "主角".to_string(),
OutfitCategory::Outerwear => "重点".to_string(),
OutfitCategory::Footwear | OutfitCategory::Accessory => "配角".to_string(),
_ => "点缀".to_string(),
}
}
/// 提取色彩搭配方案
fn extract_color_palette(&self, items: &[OutfitItem]) -> Vec<ColorHSV> {
let mut palette = Vec::new();
for item in items {
palette.push(item.color_primary.clone());
if let Some(secondary) = &item.color_secondary {
palette.push(secondary.clone());
}
}
// 去重
palette.dedup_by(|a, b| a.similarity(b) > 0.9);
palette
}
/// 生成风格描述
fn generate_style_description(&self, items: &[OutfitItem]) -> String {
if items.is_empty() {
return "未知风格".to_string();
}
// 统计风格出现频率
let mut style_counts: HashMap<OutfitStyle, usize> = HashMap::new();
for item in items {
for style in &item.styles {
*style_counts.entry(style.clone()).or_insert(0) += 1;
}
}
// 找出最常见的风格
let mut sorted_styles: Vec<(OutfitStyle, usize)> = style_counts.into_iter().collect();
sorted_styles.sort_by(|a, b| b.1.cmp(&a.1));
if sorted_styles.is_empty() {
return "简约搭配".to_string();
}
// 生成描述
let main_style = &sorted_styles[0].0;
let style_name = main_style.to_string();
if sorted_styles.len() == 1 || sorted_styles[0].1 > sorted_styles[1].1 {
format!("{}风格搭配", style_name)
} else {
let second_style = &sorted_styles[1].0;
format!("{}{}混搭风格", style_name, second_style.to_string())
}
}
/// 计算风格相似度
fn calculate_style_similarity(&self, styles1: &[OutfitStyle], styles2: &[OutfitStyle]) -> f64 {
if styles1.is_empty() || styles2.is_empty() {
return 0.5; // 默认中等相似度
}
let common_styles = styles1.iter()
.filter(|style1| styles2.contains(style1))
.count();
let total_unique_styles = styles1.len() + styles2.len() - common_styles;
if total_unique_styles == 0 {
1.0
} else {
common_styles as f64 / total_unique_styles as f64
}
}
/// 获取服装搭配统计信息
pub async fn get_matching_stats(&self, project_id: Option<&str>) -> Result<OutfitMatchingStats> {
Ok(self.repository.get_stats(project_id)?)
}
/// 验证创建请求
fn validate_create_request(&self, request: &CreateOutfitMatchingRequest) -> Result<()> {
if request.project_id.trim().is_empty() {
return Err(BusinessError::InvalidInput("项目ID不能为空".to_string()).into());
}
if request.matching_name.trim().is_empty() {
return Err(BusinessError::InvalidInput("搭配名称不能为空".to_string()).into());
}
if request.item_ids.is_empty() {
return Err(BusinessError::InvalidInput("搭配单品列表不能为空".to_string()).into());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data::models::outfit_analysis::ColorHSV;
use crate::data::models::outfit_item::{OutfitCategory, OutfitStyle};
#[test]
fn test_calculate_color_harmony() {
// 创建测试用的服装单品
let items = vec![
OutfitItem {
id: "1".to_string(),
project_id: "test".to_string(),
analysis_id: None,
name: "红色T恤".to_string(),
category: OutfitCategory::Top,
brand: None,
model: None,
color_primary: ColorHSV::new(0.0, 0.8, 0.9), // 红色
color_secondary: None,
styles: vec![OutfitStyle::Casual],
design_elements: vec![],
size: None,
material: None,
price: None,
purchase_date: None,
image_urls: vec![],
tags: vec![],
notes: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
},
OutfitItem {
id: "2".to_string(),
project_id: "test".to_string(),
analysis_id: None,
name: "红色裤子".to_string(),
category: OutfitCategory::Bottom,
brand: None,
model: None,
color_primary: ColorHSV::new(0.02, 0.75, 0.85), // 相似的红色
color_secondary: None,
styles: vec![OutfitStyle::Casual],
design_elements: vec![],
size: None,
material: None,
price: None,
purchase_date: None,
image_urls: vec![],
tags: vec![],
notes: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
},
];
// 创建模拟的仓库
let matching_repo = std::sync::Arc::new(unsafe { std::mem::zeroed() });
let item_repo = std::sync::Arc::new(unsafe { std::mem::zeroed() });
let service = OutfitMatchingService::new(matching_repo, item_repo);
let harmony_score = service.calculate_color_harmony(&items);
// 相似颜色应该有较高的和谐度
assert!(harmony_score > 0.7);
}
#[test]
fn test_calculate_style_consistency() {
let items = vec![
OutfitItem {
id: "1".to_string(),
project_id: "test".to_string(),
analysis_id: None,
name: "休闲T恤".to_string(),
category: OutfitCategory::Top,
brand: None,
model: None,
color_primary: ColorHSV::new(0.0, 0.8, 0.9),
color_secondary: None,
styles: vec![OutfitStyle::Casual, OutfitStyle::Trendy],
design_elements: vec![],
size: None,
material: None,
price: None,
purchase_date: None,
image_urls: vec![],
tags: vec![],
notes: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
},
OutfitItem {
id: "2".to_string(),
project_id: "test".to_string(),
analysis_id: None,
name: "休闲裤子".to_string(),
category: OutfitCategory::Bottom,
brand: None,
model: None,
color_primary: ColorHSV::new(0.6, 0.5, 0.8),
color_secondary: None,
styles: vec![OutfitStyle::Casual],
design_elements: vec![],
size: None,
material: None,
price: None,
purchase_date: None,
image_urls: vec![],
tags: vec![],
notes: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
},
];
let matching_repo = std::sync::Arc::new(unsafe { std::mem::zeroed() });
let item_repo = std::sync::Arc::new(unsafe { std::mem::zeroed() });
let service = OutfitMatchingService::new(matching_repo, item_repo);
let consistency_score = service.calculate_style_consistency(&items);
// 有共同风格的单品应该有较高的一致性
assert!(consistency_score > 0.5);
}
#[test]
fn test_determine_item_role() {
let matching_repo = std::sync::Arc::new(unsafe { std::mem::zeroed() });
let item_repo = std::sync::Arc::new(unsafe { std::mem::zeroed() });
let service = OutfitMatchingService::new(matching_repo, item_repo);
let top_item = OutfitItem {
id: "1".to_string(),
project_id: "test".to_string(),
analysis_id: None,
name: "T恤".to_string(),
category: OutfitCategory::Top,
brand: None,
model: None,
color_primary: ColorHSV::new(0.0, 0.8, 0.9),
color_secondary: None,
styles: vec![],
design_elements: vec![],
size: None,
material: None,
price: None,
purchase_date: None,
image_urls: vec![],
tags: vec![],
notes: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let role = service.determine_item_role(&top_item);
assert_eq!(role, "主角");
let accessory_item = OutfitItem {
category: OutfitCategory::Accessory,
..top_item.clone()
};
let accessory_role = service.determine_item_role(&accessory_item);
assert_eq!(accessory_role, "配角");
}
#[test]
fn test_generate_style_description() {
let items = vec![
OutfitItem {
id: "1".to_string(),
project_id: "test".to_string(),
analysis_id: None,
name: "休闲T恤".to_string(),
category: OutfitCategory::Top,
brand: None,
model: None,
color_primary: ColorHSV::new(0.0, 0.8, 0.9),
color_secondary: None,
styles: vec![OutfitStyle::Casual, OutfitStyle::Casual], // 重复的休闲风格
design_elements: vec![],
size: None,
material: None,
price: None,
purchase_date: None,
image_urls: vec![],
tags: vec![],
notes: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
},
];
let matching_repo = std::sync::Arc::new(unsafe { std::mem::zeroed() });
let item_repo = std::sync::Arc::new(unsafe { std::mem::zeroed() });
let service = OutfitMatchingService::new(matching_repo, item_repo);
let description = service.generate_style_description(&items);
assert!(description.contains("休闲"));
}
#[test]
fn test_validate_create_request() {
let matching_repo = std::sync::Arc::new(unsafe { std::mem::zeroed() });
let item_repo = std::sync::Arc::new(unsafe { std::mem::zeroed() });
let service = OutfitMatchingService::new(matching_repo, item_repo);
// 测试有效请求
let valid_request = CreateOutfitMatchingRequest {
project_id: "test_project".to_string(),
matching_name: "测试搭配".to_string(),
matching_type: MatchingType::StyleConsistent,
item_ids: vec!["item1".to_string(), "item2".to_string()],
occasion_tags: vec![],
season_tags: vec![],
style_description: None,
};
assert!(service.validate_create_request(&valid_request).is_ok());
// 测试无效请求 - 空项目ID
let invalid_request = CreateOutfitMatchingRequest {
project_id: "".to_string(),
..valid_request.clone()
};
assert!(service.validate_create_request(&invalid_request).is_err());
// 测试无效请求 - 空搭配名称
let invalid_request = CreateOutfitMatchingRequest {
matching_name: "".to_string(),
..valid_request.clone()
};
assert!(service.validate_create_request(&invalid_request).is_err());
// 测试无效请求 - 空单品列表
let invalid_request = CreateOutfitMatchingRequest {
item_ids: vec![],
..valid_request.clone()
};
assert!(service.validate_create_request(&invalid_request).is_err());
}
}