新功能: - 实现完整的图像上传组件 (ImageUploader) - 支持拖拽上传、文件选择、预览和验证 - 文件大小和格式限制 - 实时预览和文件管理 - 实现AI图像分析结果展示 (OutfitAnalysisResult) - 实时状态更新和进度跟踪 - 分析结果详情查看 - 自动刷新机制 - 添加文件保存功能 (save_outfit_image 命令) - 更新OutfitMatch页面为现代化标签页设计 技术改进: - 修复服装搭配相关模型的编译错误 - 添加缺失的trait实现 (Eq, Hash) - 修复生命周期参数问题 - 完善错误处理和用户反馈 UI/UX优化: - 现代化的标签页设计 - 响应式布局和优雅动画 - 统一的设计语言和交互体验 - 完善的加载状态和错误处理
964 lines
34 KiB
Rust
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());
|
|
}
|
|
}
|