diff --git a/apps/desktop/src-tauri/src/business/services/mod.rs b/apps/desktop/src-tauri/src/business/services/mod.rs index 1e760d7..345c39c 100644 --- a/apps/desktop/src-tauri/src/business/services/mod.rs +++ b/apps/desktop/src-tauri/src/business/services/mod.rs @@ -22,9 +22,6 @@ pub mod template_matching_result_service; pub mod export_record_service; pub mod video_generation_service; pub mod jianying_export; -pub mod outfit_analysis_service; -pub mod outfit_item_service; -pub mod outfit_matching_service; #[cfg(test)] pub mod tests; diff --git a/apps/desktop/src-tauri/src/business/services/outfit_analysis_service.rs b/apps/desktop/src-tauri/src/business/services/outfit_analysis_service.rs deleted file mode 100644 index 7071e0b..0000000 --- a/apps/desktop/src-tauri/src/business/services/outfit_analysis_service.rs +++ /dev/null @@ -1,314 +0,0 @@ -use crate::data::models::outfit_analysis::{ - OutfitAnalysis, CreateOutfitAnalysisRequest, UpdateOutfitAnalysisRequest, - OutfitAnalysisQueryOptions, OutfitAnalysisStats, AnalysisStatus, ImageAnalysisResult, - OutfitProduct, ColorHSV -}; -use crate::data::repositories::outfit_analysis_repository::OutfitAnalysisRepository; -use crate::business::errors::BusinessError; -use crate::infrastructure::gemini_service::GeminiService; -use anyhow::Result; -use std::sync::Arc; -use std::path::Path; -use tokio::fs; -use base64::{Engine as _, engine::general_purpose}; - -/// 服装分析业务服务 -/// 遵循 Tauri 开发规范的业务逻辑层设计原则 -pub struct OutfitAnalysisService { - repository: Arc, - gemini_service: Arc, -} - -impl OutfitAnalysisService { - /// 创建新的服装分析服务实例 - pub fn new(repository: Arc, gemini_service: Arc) -> Self { - Self { repository, gemini_service } - } - - /// 创建服装分析记录 - pub async fn create_analysis(&self, request: CreateOutfitAnalysisRequest) -> Result { - // 验证输入数据 - self.validate_create_request(&request)?; - - // 检查图像文件是否存在 - if !Path::new(&request.image_path).exists() { - return Err(BusinessError::InvalidInput(format!("图像文件不存在: {}", request.image_path)).into()); - } - - // 创建分析记录 - let analysis = self.repository.create(&request)?; - - Ok(analysis) - } - - /// 开始分析图像 - pub async fn start_analysis(&self, analysis_id: &str) -> Result<()> { - // 获取分析记录 - let analysis = self.repository.get_by_id(analysis_id)? - .ok_or_else(|| BusinessError::NotFound(format!("分析记录不存在: {}", analysis_id)))?; - - // 检查状态 - if analysis.analysis_status != AnalysisStatus::Pending { - return Err(BusinessError::InvalidState(format!("分析记录状态不正确: {:?}", analysis.analysis_status)).into()); - } - - // 更新状态为处理中 - self.repository.update(analysis_id, &UpdateOutfitAnalysisRequest { - analysis_status: Some(AnalysisStatus::Processing), - analysis_result: None, - error_message: None, - })?; - - // 异步执行分析 - let repository = Arc::clone(&self.repository); - let gemini_service = Arc::clone(&self.gemini_service); - let analysis_clone = analysis.clone(); - - tokio::spawn(async move { - let mut gemini_service_mut = (*gemini_service).clone(); - let result = Self::perform_analysis(&mut gemini_service_mut, &analysis_clone).await; - - match result { - Ok(analysis_result) => { - let _ = repository.update(&analysis_clone.id, &UpdateOutfitAnalysisRequest { - analysis_status: Some(AnalysisStatus::Completed), - analysis_result: Some(analysis_result), - error_message: None, - }); - } - Err(error) => { - let _ = repository.update(&analysis_clone.id, &UpdateOutfitAnalysisRequest { - analysis_status: Some(AnalysisStatus::Failed), - analysis_result: None, - error_message: Some(error.to_string()), - }); - } - } - }); - - Ok(()) - } - - /// 执行图像分析 - async fn perform_analysis(gemini_service: &mut GeminiService, analysis: &OutfitAnalysis) -> Result { - // 读取图像文件 - let image_data = fs::read(&analysis.image_path).await?; - let image_base64 = general_purpose::STANDARD.encode(&image_data); - - // 构建分析提示词 - let prompt = Self::build_analysis_prompt(); - - // 调用Gemini API进行分析 - let response = gemini_service.analyze_image_with_prompt(&image_base64, &prompt).await?; - - // 解析响应 - let analysis_result = Self::parse_analysis_response(&response)?; - - Ok(analysis_result) - } - - /// 构建分析提示词 - fn build_analysis_prompt() -> String { - r#" - 你是一个专业的服装搭配分析师。请分析这张图片中的服装搭配,并按照以下JSON格式返回结果: - - { - "environment_tags": ["环境标签1", "环境标签2"], - "environment_color_pattern": {"hue": 0.5, "saturation": 0.3, "value": 0.8}, - "dress_color_pattern": {"hue": 0.6, "saturation": 0.4, "value": 0.7}, - "style_description": "整体风格描述", - "products": [ - { - "category": "服装类别", - "description": "服装描述", - "color_pattern": {"hue": 0.4, "saturation": 0.5, "value": 0.6}, - "design_styles": ["设计风格1", "设计风格2"], - "color_pattern_match_dress": 0.8, - "color_pattern_match_environment": 0.7 - } - ] - } - - 注意: - 1. HSV颜色值范围都是0.0-1.0 - 2. 匹配度范围是0.0-1.0 - 3. 环境标签用英文 - 4. 设计风格标签用英文 - 5. 类别和描述用中文 - "#.to_string() - } - - /// 解析分析响应 - fn parse_analysis_response(response: &str) -> Result { - // 尝试从响应中提取JSON - let json_start = response.find('{').unwrap_or(0); - let json_end = response.rfind('}').map(|i| i + 1).unwrap_or(response.len()); - let json_str = &response[json_start..json_end]; - - // 解析JSON - let parsed: serde_json::Value = serde_json::from_str(json_str) - .map_err(|e| BusinessError::InvalidInput(format!("解析分析结果失败: {}", e)))?; - - // 构建分析结果 - let environment_tags = parsed["environment_tags"] - .as_array() - .unwrap_or(&Vec::new()) - .iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect(); - - let environment_color = Self::parse_color_hsv(&parsed["environment_color_pattern"])?; - let dress_color = Self::parse_color_hsv(&parsed["dress_color_pattern"])?; - - let style_description = parsed["style_description"] - .as_str() - .unwrap_or("未知风格") - .to_string(); - - let products = parsed["products"] - .as_array() - .unwrap_or(&Vec::new()) - .iter() - .filter_map(|product| Self::parse_outfit_product(product).ok()) - .collect(); - - Ok(ImageAnalysisResult { - environment_tags, - environment_color_pattern: environment_color, - dress_color_pattern: dress_color, - style_description, - products, - }) - } - - /// 解析颜色HSV值 - fn parse_color_hsv(color_value: &serde_json::Value) -> Result { - let hue = color_value["hue"].as_f64().unwrap_or(0.0); - let saturation = color_value["saturation"].as_f64().unwrap_or(0.0); - let value = color_value["value"].as_f64().unwrap_or(0.0); - - Ok(ColorHSV::new(hue, saturation, value)) - } - - /// 解析服装产品信息 - fn parse_outfit_product(product_value: &serde_json::Value) -> Result { - let category = product_value["category"] - .as_str() - .unwrap_or("未知类别") - .to_string(); - - let description = product_value["description"] - .as_str() - .unwrap_or("无描述") - .to_string(); - - let color_pattern = Self::parse_color_hsv(&product_value["color_pattern"])?; - - let design_styles = product_value["design_styles"] - .as_array() - .unwrap_or(&Vec::new()) - .iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect(); - - let color_pattern_match_dress = product_value["color_pattern_match_dress"] - .as_f64() - .unwrap_or(0.0); - - let color_pattern_match_environment = product_value["color_pattern_match_environment"] - .as_f64() - .unwrap_or(0.0); - - Ok(OutfitProduct { - category, - description, - color_pattern, - design_styles, - color_pattern_match_dress, - color_pattern_match_environment, - }) - } - - /// 获取分析记录列表 - pub async fn get_analyses(&self, options: OutfitAnalysisQueryOptions) -> Result> { - Ok(self.repository.list(&options)?) - } - - /// 根据ID获取分析记录 - pub async fn get_analysis_by_id(&self, id: &str) -> Result> { - if id.trim().is_empty() { - return Err(BusinessError::InvalidInput("ID不能为空".to_string()).into()); - } - - Ok(self.repository.get_by_id(id)?) - } - - /// 删除分析记录 - pub async fn delete_analysis(&self, id: &str) -> Result<()> { - if id.trim().is_empty() { - return Err(BusinessError::InvalidInput("ID不能为空".to_string()).into()); - } - - self.repository.delete(id)?; - Ok(()) - } - - /// 获取分析统计信息 - pub async fn get_analysis_stats(&self, project_id: Option<&str>) -> Result { - Ok(self.repository.get_stats(project_id)?) - } - - /// 验证创建请求 - fn validate_create_request(&self, request: &CreateOutfitAnalysisRequest) -> Result<()> { - if request.project_id.trim().is_empty() { - return Err(BusinessError::InvalidInput("项目ID不能为空".to_string()).into()); - } - - if request.image_path.trim().is_empty() { - return Err(BusinessError::InvalidInput("图像路径不能为空".to_string()).into()); - } - - if request.image_name.trim().is_empty() { - return Err(BusinessError::InvalidInput("图像名称不能为空".to_string()).into()); - } - - // 检查文件扩展名 - let path = Path::new(&request.image_path); - if let Some(extension) = path.extension() { - let ext = extension.to_string_lossy().to_lowercase(); - if !matches!(ext.as_str(), "jpg" | "jpeg" | "png" | "gif" | "bmp" | "webp") { - return Err(BusinessError::InvalidInput(format!("不支持的图像格式: {}", ext)).into()); - } - } else { - return Err(BusinessError::InvalidInput("无法识别图像文件格式".to_string()).into()); - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_color_hsv() { - let color_json = serde_json::json!({ - "hue": 0.5, - "saturation": 0.8, - "value": 0.9 - }); - - let color = OutfitAnalysisService::parse_color_hsv(&color_json).unwrap(); - assert_eq!(color.hue, 0.5); - assert_eq!(color.saturation, 0.8); - assert_eq!(color.value, 0.9); - } - - #[test] - fn test_build_analysis_prompt() { - let prompt = OutfitAnalysisService::build_analysis_prompt(); - assert!(prompt.contains("服装搭配分析师")); - assert!(prompt.contains("JSON格式")); - } -} diff --git a/apps/desktop/src-tauri/src/business/services/outfit_item_service.rs b/apps/desktop/src-tauri/src/business/services/outfit_item_service.rs deleted file mode 100644 index 14db64c..0000000 --- a/apps/desktop/src-tauri/src/business/services/outfit_item_service.rs +++ /dev/null @@ -1,516 +0,0 @@ -use crate::data::models::outfit_item::{ - OutfitItem, CreateOutfitItemRequest, UpdateOutfitItemRequest, - OutfitItemQueryOptions, OutfitItemStats, OutfitCategory, OutfitStyle -}; -use crate::data::models::outfit_analysis::ColorHSV; -use crate::data::repositories::outfit_item_repository::OutfitItemRepository; -use crate::business::errors::BusinessError; -use anyhow::Result; -use std::sync::Arc; - -/// 服装单品业务服务 -/// 遵循 Tauri 开发规范的业务逻辑层设计原则 -pub struct OutfitItemService { - repository: Arc, -} - -impl OutfitItemService { - /// 创建新的服装单品服务实例 - pub fn new(repository: Arc) -> Self { - Self { repository } - } - - /// 创建服装单品 - pub async fn create_item(&self, request: CreateOutfitItemRequest) -> Result { - // 验证输入数据 - self.validate_create_request(&request)?; - - // 创建服装单品 - let item = self.repository.create(&request)?; - - Ok(item) - } - - /// 批量创建服装单品(从分析结果) - pub async fn create_items_from_analysis(&self, - project_id: &str, - analysis_id: &str, - products: &[crate::data::models::outfit_analysis::OutfitProduct] - ) -> Result> { - let mut created_items = Vec::new(); - - for (index, product) in products.iter().enumerate() { - let request = CreateOutfitItemRequest { - project_id: project_id.to_string(), - analysis_id: Some(analysis_id.to_string()), - name: format!("{} #{}", product.category, index + 1), - category: OutfitCategory::from_string(&product.category), - brand: None, - model: None, - color_primary: product.color_pattern.clone(), - color_secondary: None, - styles: product.design_styles.iter() - .map(|s| OutfitStyle::from_string(s)) - .collect(), - design_elements: product.design_styles.clone(), - size: None, - material: None, - price: None, - purchase_date: None, - image_urls: Vec::new(), - tags: vec![product.category.clone()], - notes: Some(product.description.clone()), - }; - - match self.repository.create(&request) { - Ok(item) => created_items.push(item), - Err(e) => { - eprintln!("创建服装单品失败: {}", e); - // 继续处理其他单品,不中断整个流程 - } - } - } - - Ok(created_items) - } - - /// 获取服装单品列表 - pub async fn get_items(&self, options: OutfitItemQueryOptions) -> Result> { - Ok(self.repository.list(&options)?) - } - - /// 根据ID获取服装单品 - pub async fn get_item_by_id(&self, id: &str) -> Result> { - if id.trim().is_empty() { - return Err(BusinessError::InvalidInput("ID不能为空".to_string()).into()); - } - - Ok(self.repository.get_by_id(id)?) - } - - /// 更新服装单品 - pub async fn update_item(&self, id: &str, request: UpdateOutfitItemRequest) -> Result<()> { - if id.trim().is_empty() { - return Err(BusinessError::InvalidInput("ID不能为空".to_string()).into()); - } - - // 验证更新请求 - self.validate_update_request(&request)?; - - // 检查单品是否存在 - if self.repository.get_by_id(id)?.is_none() { - return Err(BusinessError::NotFound(format!("服装单品不存在: {}", id)).into()); - } - - self.repository.update(id, &request)?; - Ok(()) - } - - /// 删除服装单品 - pub async fn delete_item(&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 search_by_color(&self, - project_id: &str, - target_color: &ColorHSV, - threshold: f64 - ) -> Result> { - if project_id.trim().is_empty() { - return Err(BusinessError::InvalidInput("项目ID不能为空".to_string()).into()); - } - - if threshold < 0.0 || threshold > 1.0 { - return Err(BusinessError::InvalidInput("相似度阈值必须在0.0-1.0之间".to_string()).into()); - } - - Ok(self.repository.search_by_color(project_id, target_color, threshold)?) - } - - /// 根据类别搜索服装单品 - pub async fn search_by_category(&self, - project_id: &str, - category: &OutfitCategory - ) -> Result> { - if project_id.trim().is_empty() { - return Err(BusinessError::InvalidInput("项目ID不能为空".to_string()).into()); - } - - let options = OutfitItemQueryOptions { - project_id: Some(project_id.to_string()), - category: Some(category.clone()), - styles: None, - color_similarity_threshold: None, - target_color: None, - brand: None, - tags: None, - limit: None, - offset: None, - }; - - Ok(self.repository.list(&options)?) - } - - /// 根据风格搜索服装单品 - pub async fn search_by_styles(&self, - project_id: &str, - styles: &[OutfitStyle] - ) -> Result> { - if project_id.trim().is_empty() { - return Err(BusinessError::InvalidInput("项目ID不能为空".to_string()).into()); - } - - if styles.is_empty() { - return Err(BusinessError::InvalidInput("风格列表不能为空".to_string()).into()); - } - - let options = OutfitItemQueryOptions { - project_id: Some(project_id.to_string()), - category: None, - styles: Some(styles.to_vec()), - color_similarity_threshold: None, - target_color: None, - brand: None, - tags: None, - limit: None, - offset: None, - }; - - Ok(self.repository.list(&options)?) - } - - /// 智能推荐搭配单品 - pub async fn recommend_matching_items(&self, - project_id: &str, - base_item_id: &str, - max_results: Option - ) -> Result> { - // 获取基础单品 - let base_item = self.repository.get_by_id(base_item_id)? - .ok_or_else(|| BusinessError::NotFound(format!("基础单品不存在: {}", base_item_id)))?; - - // 获取项目中的所有单品 - let all_items = self.repository.list(&OutfitItemQueryOptions { - project_id: Some(project_id.to_string()), - category: None, - styles: None, - color_similarity_threshold: None, - target_color: None, - brand: None, - tags: None, - limit: None, - offset: None, - })?; - - // 过滤掉基础单品本身 - let candidate_items: Vec = all_items.into_iter() - .filter(|item| item.id != base_item_id) - .collect(); - - // 计算匹配度并排序 - let mut scored_items: Vec<(OutfitItem, f64)> = candidate_items.into_iter() - .map(|item| { - let score = self.calculate_matching_score(&base_item, &item); - (item, score) - }) - .collect(); - - // 按匹配度降序排序 - scored_items.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); - - // 限制结果数量 - let limit = max_results.unwrap_or(10) as usize; - let recommended_items: Vec = scored_items.into_iter() - .take(limit) - .map(|(item, _)| item) - .collect(); - - Ok(recommended_items) - } - - /// 计算两个单品的匹配度 - fn calculate_matching_score(&self, item1: &OutfitItem, item2: &OutfitItem) -> f64 { - let mut score = 0.0; - - // 颜色匹配度 (权重: 0.4) - let color_similarity = item1.color_primary.similarity(&item2.color_primary); - score += color_similarity * 0.4; - - // 风格匹配度 (权重: 0.3) - let style_similarity = self.calculate_style_similarity(&item1.styles, &item2.styles); - score += style_similarity * 0.3; - - // 类别互补性 (权重: 0.3) - let category_compatibility = self.calculate_category_compatibility(&item1.category, &item2.category); - score += category_compatibility * 0.3; - - score.clamp(0.0, 1.0) - } - - /// 计算风格相似度 - 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 - } - } - - /// 计算类别兼容性 - fn calculate_category_compatibility(&self, category1: &OutfitCategory, category2: &OutfitCategory) -> f64 { - match (category1, category2) { - // 相同类别,兼容性较低(避免重复) - (a, b) if a == b => 0.2, - // 上装与下装,兼容性高 - (OutfitCategory::Top, OutfitCategory::Bottom) | - (OutfitCategory::Bottom, OutfitCategory::Top) => 0.9, - // 连衣裙与配饰,兼容性高 - (OutfitCategory::Dress, OutfitCategory::Accessory) | - (OutfitCategory::Accessory, OutfitCategory::Dress) => 0.8, - // 外套与其他类别,兼容性中等 - (OutfitCategory::Outerwear, _) | - (_, OutfitCategory::Outerwear) => 0.7, - // 鞋类与其他类别,兼容性中等 - (OutfitCategory::Footwear, _) | - (_, OutfitCategory::Footwear) => 0.6, - // 配饰与其他类别,兼容性中等 - (OutfitCategory::Accessory, _) | - (_, OutfitCategory::Accessory) => 0.6, - // 其他组合,默认兼容性 - _ => 0.5, - } - } - - /// 获取服装单品统计信息 - pub async fn get_item_stats(&self, project_id: Option<&str>) -> Result { - Ok(self.repository.get_stats(project_id)?) - } - - /// 验证创建请求 - fn validate_create_request(&self, request: &CreateOutfitItemRequest) -> Result<()> { - if request.project_id.trim().is_empty() { - return Err(BusinessError::InvalidInput("项目ID不能为空".to_string()).into()); - } - - if request.name.trim().is_empty() { - return Err(BusinessError::InvalidInput("服装名称不能为空".to_string()).into()); - } - - Ok(()) - } - - /// 验证更新请求 - fn validate_update_request(&self, request: &UpdateOutfitItemRequest) -> Result<()> { - if let Some(name) = &request.name { - if name.trim().is_empty() { - return Err(BusinessError::InvalidInput("服装名称不能为空".to_string()).into()); - } - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::data::models::outfit_analysis::ColorHSV; - - #[test] - fn test_calculate_style_similarity() { - let service = OutfitItemService::new(Arc::new( - // Mock repository - in real tests, use a proper mock - unsafe { std::mem::zeroed() } - )); - - let styles1 = vec![OutfitStyle::Casual, OutfitStyle::Trendy]; - let styles2 = vec![OutfitStyle::Casual, OutfitStyle::Formal]; - - let similarity = service.calculate_style_similarity(&styles1, &styles2); - assert!(similarity > 0.0 && similarity < 1.0); - } - - #[test] - fn test_calculate_category_compatibility() { - let service = OutfitItemService::new(Arc::new( - // Mock repository - in real tests, use a proper mock - unsafe { std::mem::zeroed() } - )); - - let compatibility = service.calculate_category_compatibility( - &OutfitCategory::Top, - &OutfitCategory::Bottom - ); - assert_eq!(compatibility, 0.9); - - let same_category = service.calculate_category_compatibility( - &OutfitCategory::Top, - &OutfitCategory::Top - ); - assert_eq!(same_category, 0.2); - } - - #[test] - fn test_calculate_matching_score() { - let service = OutfitItemService::new(Arc::new( - unsafe { std::mem::zeroed() } - )); - - let item1 = 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(), - }; - - let item2 = 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.7, 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 score = service.calculate_matching_score(&item1, &item2); - - // 应该有一定的匹配度(相同风格,不同类别) - assert!(score > 0.0 && score <= 1.0); - - // 由于风格相同且类别互补,分数应该比较高 - assert!(score > 0.5); - } - - #[test] - fn test_validate_create_request() { - let service = OutfitItemService::new(Arc::new( - unsafe { std::mem::zeroed() } - )); - - // 测试有效请求 - let valid_request = CreateOutfitItemRequest { - project_id: "test_project".to_string(), - analysis_id: None, - name: "测试T恤".to_string(), - category: OutfitCategory::Top, - brand: None, - model: None, - color_primary: ColorHSV::new(0.5, 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, - }; - - assert!(service.validate_create_request(&valid_request).is_ok()); - - // 测试无效请求 - 空项目ID - let invalid_request = CreateOutfitItemRequest { - project_id: "".to_string(), - ..valid_request.clone() - }; - - assert!(service.validate_create_request(&invalid_request).is_err()); - - // 测试无效请求 - 空名称 - let invalid_request = CreateOutfitItemRequest { - name: "".to_string(), - ..valid_request.clone() - }; - - assert!(service.validate_create_request(&invalid_request).is_err()); - } - - #[test] - fn test_validate_update_request() { - let service = OutfitItemService::new(Arc::new( - unsafe { std::mem::zeroed() } - )); - - // 测试有效更新请求 - let valid_request = UpdateOutfitItemRequest { - name: Some("新名称".to_string()), - category: Some(OutfitCategory::Bottom), - brand: None, - model: None, - color_primary: None, - color_secondary: None, - styles: None, - design_elements: None, - size: None, - material: None, - price: None, - purchase_date: None, - image_urls: None, - tags: None, - notes: None, - }; - - assert!(service.validate_update_request(&valid_request).is_ok()); - - // 测试无效更新请求 - 空名称 - let invalid_request = UpdateOutfitItemRequest { - name: Some("".to_string()), - ..valid_request.clone() - }; - - assert!(service.validate_update_request(&invalid_request).is_err()); - } -} diff --git a/apps/desktop/src-tauri/src/business/services/outfit_matching_service.rs b/apps/desktop/src-tauri/src/business/services/outfit_matching_service.rs deleted file mode 100644 index b5aec44..0000000 --- a/apps/desktop/src-tauri/src/business/services/outfit_matching_service.rs +++ /dev/null @@ -1,963 +0,0 @@ -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, - item_repository: Arc, -} - -impl OutfitMatchingService { - /// 创建新的服装搭配服务实例 - pub fn new( - repository: Arc, - item_repository: Arc - ) -> Self { - Self { repository, item_repository } - } - - /// 创建服装搭配 - pub async fn create_matching(&self, request: CreateOutfitMatchingRequest) -> Result { - // 验证输入数据 - 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> { - Ok(self.repository.list(&options)?) - } - - /// 根据ID获取服装搭配 - pub async fn get_matching_by_id(&self, id: &str) -> Result> { - 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 { - // 验证请求 - 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 = 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 = 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> { - // 按类别分组单品 - let mut items_by_category: HashMap> = 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) { - // 计算颜色和谐度 - 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 { - 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 { - 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 = 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 { - 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()); - } -} diff --git a/apps/desktop/src-tauri/src/data/repositories/mod.rs b/apps/desktop/src-tauri/src/data/repositories/mod.rs index bfac0f3..c1e9e9f 100644 --- a/apps/desktop/src-tauri/src/data/repositories/mod.rs +++ b/apps/desktop/src-tauri/src/data/repositories/mod.rs @@ -8,6 +8,3 @@ pub mod project_template_binding_repository; pub mod template_matching_result_repository; pub mod export_record_repository; pub mod video_generation_repository; -pub mod outfit_analysis_repository; -pub mod outfit_item_repository; -pub mod outfit_matching_repository; diff --git a/apps/desktop/src-tauri/src/data/repositories/outfit_analysis_repository.rs b/apps/desktop/src-tauri/src/data/repositories/outfit_analysis_repository.rs deleted file mode 100644 index 6a5f782..0000000 --- a/apps/desktop/src-tauri/src/data/repositories/outfit_analysis_repository.rs +++ /dev/null @@ -1,263 +0,0 @@ -use rusqlite::{Result, Row, OptionalExtension}; -use std::sync::Arc; -use chrono::{DateTime, Utc}; -use crate::data::models::outfit_analysis::{ - OutfitAnalysis, CreateOutfitAnalysisRequest, UpdateOutfitAnalysisRequest, - OutfitAnalysisQueryOptions, OutfitAnalysisStats, AnalysisStatus, ImageAnalysisResult -}; -use crate::infrastructure::database::Database; - -/// 服装分析数据仓库 -/// 遵循 Tauri 开发规范的数据访问层设计 -pub struct OutfitAnalysisRepository { - database: Arc, -} - -impl OutfitAnalysisRepository { - /// 创建新的服装分析仓库实例 - pub fn new(database: Arc) -> Result { - Ok(OutfitAnalysisRepository { database }) - } - - /// 创建服装分析记录 - pub fn create(&self, request: &CreateOutfitAnalysisRequest) -> Result { - let conn = self.database.get_connection(); - let conn = conn.lock().unwrap(); - - let analysis = OutfitAnalysis { - id: uuid::Uuid::new_v4().to_string(), - project_id: request.project_id.clone(), - image_path: request.image_path.clone(), - image_name: request.image_name.clone(), - analysis_status: AnalysisStatus::Pending, - analysis_result: None, - error_message: None, - created_at: Utc::now(), - updated_at: Utc::now(), - analyzed_at: None, - }; - - conn.execute( - "INSERT INTO outfit_analyses ( - id, project_id, image_path, image_name, analysis_status, - analysis_result, error_message, created_at, updated_at, analyzed_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", - [ - analysis.id.as_str(), - analysis.project_id.as_str(), - analysis.image_path.as_str(), - analysis.image_name.as_str(), - &serde_json::to_string(&analysis.analysis_status).unwrap(), - analysis.analysis_result.as_ref().map(|r| serde_json::to_string(r).unwrap()).as_deref().unwrap_or(""), - analysis.error_message.as_deref().unwrap_or(""), - analysis.created_at.to_rfc3339().as_str(), - analysis.updated_at.to_rfc3339().as_str(), - analysis.analyzed_at.as_ref().map(|d| d.to_rfc3339()).as_deref().unwrap_or(""), - ], - )?; - - Ok(analysis) - } - - /// 根据ID获取服装分析记录 - pub fn get_by_id(&self, id: &str) -> Result> { - let conn = self.database.get_read_connection(); - let conn = conn.lock().unwrap(); - - let mut stmt = conn.prepare( - "SELECT id, project_id, image_path, image_name, analysis_status, - analysis_result, error_message, created_at, updated_at, analyzed_at - FROM outfit_analyses WHERE id = ?1" - )?; - - let analysis = stmt.query_row([id], |row| { - self.row_to_outfit_analysis(row) - }).optional()?; - - Ok(analysis) - } - - /// 更新服装分析记录 - pub fn update(&self, id: &str, request: &UpdateOutfitAnalysisRequest) -> Result<()> { - let conn = self.database.get_connection(); - let conn = conn.lock().unwrap(); - - let mut updates = Vec::new(); - let mut params: Vec> = Vec::new(); - - if let Some(status) = &request.analysis_status { - updates.push("analysis_status = ?"); - params.push(Box::new(serde_json::to_string(status).unwrap())); - } - - if let Some(result) = &request.analysis_result { - updates.push("analysis_result = ?"); - params.push(Box::new(serde_json::to_string(result).unwrap())); - } - - if let Some(error) = &request.error_message { - updates.push("error_message = ?"); - params.push(Box::new(error.clone())); - } - - if !updates.is_empty() { - updates.push("updated_at = ?"); - params.push(Box::new(Utc::now().to_rfc3339())); - - // 如果状态更新为已完成,设置分析时间 - if let Some(AnalysisStatus::Completed) = &request.analysis_status { - updates.push("analyzed_at = ?"); - params.push(Box::new(Utc::now().to_rfc3339())); - } - - params.push(Box::new(id.to_string())); - - let sql = format!( - "UPDATE outfit_analyses SET {} WHERE id = ?", - updates.join(", ") - ); - - let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect(); - conn.execute(&sql, param_refs.as_slice())?; - } - - Ok(()) - } - - /// 删除服装分析记录 - pub fn delete(&self, id: &str) -> Result<()> { - let conn = self.database.get_connection(); - let conn = conn.lock().unwrap(); - - conn.execute("DELETE FROM outfit_analyses WHERE id = ?1", [id])?; - Ok(()) - } - - /// 查询服装分析记录列表 - pub fn list(&self, options: &OutfitAnalysisQueryOptions) -> Result> { - let conn = self.database.get_read_connection(); - let conn = conn.lock().unwrap(); - - let mut sql = "SELECT id, project_id, image_path, image_name, analysis_status, - analysis_result, error_message, created_at, updated_at, analyzed_at - FROM outfit_analyses WHERE 1=1".to_string(); - let mut params: Vec> = Vec::new(); - - if let Some(project_id) = &options.project_id { - sql.push_str(" AND project_id = ?"); - params.push(Box::new(project_id.clone())); - } - - if let Some(status) = &options.status { - sql.push_str(" AND analysis_status = ?"); - params.push(Box::new(serde_json::to_string(status).unwrap())); - } - - sql.push_str(" ORDER BY created_at DESC"); - - if let Some(limit) = options.limit { - sql.push_str(&format!(" LIMIT {}", limit)); - } - - if let Some(offset) = options.offset { - sql.push_str(&format!(" OFFSET {}", offset)); - } - - let mut stmt = conn.prepare(&sql)?; - let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect(); - - let analysis_iter = stmt.query_map(param_refs.as_slice(), |row| { - self.row_to_outfit_analysis(row) - })?; - - let mut analyses = Vec::new(); - for analysis in analysis_iter { - analyses.push(analysis?); - } - - Ok(analyses) - } - - /// 获取服装分析统计信息 - pub fn get_stats(&self, project_id: Option<&str>) -> Result { - let conn = self.database.get_read_connection(); - let conn = conn.lock().unwrap(); - - let mut sql = "SELECT analysis_status, COUNT(*) as count FROM outfit_analyses".to_string(); - let mut params: Vec> = Vec::new(); - - if let Some(project_id) = project_id { - sql.push_str(" WHERE project_id = ?"); - params.push(Box::new(project_id.to_string())); - } - - sql.push_str(" GROUP BY analysis_status"); - - let mut stmt = conn.prepare(&sql)?; - let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect(); - - let mut total_count = 0u32; - let mut pending_count = 0u32; - let mut processing_count = 0u32; - let mut completed_count = 0u32; - let mut failed_count = 0u32; - - let rows = stmt.query_map(param_refs.as_slice(), |row| { - let status_str: String = row.get(0)?; - let count: u32 = row.get(1)?; - Ok((status_str, count)) - })?; - - for row in rows { - let (status_str, count) = row?; - total_count += count; - - if let Ok(status) = serde_json::from_str::(&status_str) { - match status { - AnalysisStatus::Pending => pending_count = count, - AnalysisStatus::Processing => processing_count = count, - AnalysisStatus::Completed => completed_count = count, - AnalysisStatus::Failed => failed_count = count, - } - } - } - - Ok(OutfitAnalysisStats { - total_count, - pending_count, - processing_count, - completed_count, - failed_count, - }) - } - - /// 将数据库行转换为OutfitAnalysis对象 - fn row_to_outfit_analysis(&self, row: &Row) -> Result { - let analysis_status_str: String = row.get(4)?; - let analysis_status = serde_json::from_str(&analysis_status_str) - .unwrap_or(AnalysisStatus::Pending); - - let analysis_result: Option = row.get(5)?; - let analysis_result = analysis_result - .and_then(|s| serde_json::from_str::(&s).ok()); - - let created_at_str: String = row.get(7)?; - let updated_at_str: String = row.get(8)?; - let analyzed_at_str: Option = row.get(9)?; - - Ok(OutfitAnalysis { - id: row.get(0)?, - project_id: row.get(1)?, - image_path: row.get(2)?, - image_name: row.get(3)?, - analysis_status, - analysis_result, - error_message: row.get(6)?, - created_at: DateTime::parse_from_rfc3339(&created_at_str).unwrap().with_timezone(&Utc), - updated_at: DateTime::parse_from_rfc3339(&updated_at_str).unwrap().with_timezone(&Utc), - analyzed_at: analyzed_at_str - .and_then(|s| DateTime::parse_from_rfc3339(&s).ok()) - .map(|dt| dt.with_timezone(&Utc)), - }) - } -} diff --git a/apps/desktop/src-tauri/src/data/repositories/outfit_item_repository.rs b/apps/desktop/src-tauri/src/data/repositories/outfit_item_repository.rs deleted file mode 100644 index a4f61a8..0000000 --- a/apps/desktop/src-tauri/src/data/repositories/outfit_item_repository.rs +++ /dev/null @@ -1,331 +0,0 @@ -use rusqlite::{Result, Row, OptionalExtension}; -use std::sync::Arc; -use chrono::{DateTime, Utc}; -use crate::data::models::outfit_item::{ - OutfitItem, CreateOutfitItemRequest, UpdateOutfitItemRequest, - OutfitItemQueryOptions, OutfitItemStats, OutfitCategory, OutfitStyle -}; -use crate::data::models::outfit_analysis::ColorHSV; -use crate::infrastructure::database::Database; - -/// 服装单品数据仓库 -/// 遵循 Tauri 开发规范的数据访问层设计 -pub struct OutfitItemRepository { - database: Arc, -} - -impl OutfitItemRepository { - /// 创建新的服装单品仓库实例 - pub fn new(database: Arc) -> Result { - Ok(OutfitItemRepository { database }) - } - - /// 创建服装单品 - pub fn create(&self, request: &CreateOutfitItemRequest) -> Result { - let conn = self.database.get_connection(); - let conn = conn.lock().unwrap(); - - let item = OutfitItem { - id: uuid::Uuid::new_v4().to_string(), - project_id: request.project_id.clone(), - analysis_id: request.analysis_id.clone(), - name: request.name.clone(), - category: request.category.clone(), - brand: request.brand.clone(), - model: request.model.clone(), - color_primary: request.color_primary.clone(), - color_secondary: request.color_secondary.clone(), - styles: request.styles.clone(), - design_elements: request.design_elements.clone(), - size: request.size.clone(), - material: request.material.clone(), - price: request.price, - purchase_date: request.purchase_date, - image_urls: request.image_urls.clone(), - tags: request.tags.clone(), - notes: request.notes.clone(), - created_at: Utc::now(), - updated_at: Utc::now(), - }; - - conn.execute( - "INSERT INTO outfit_items ( - id, project_id, analysis_id, name, category, brand, model, - color_primary, color_secondary, styles, design_elements, - size_info, material_info, price, purchase_date, image_urls, - tags, notes, created_at, updated_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20)", - [ - item.id.as_str(), - item.project_id.as_str(), - item.analysis_id.as_deref().unwrap_or(""), - item.name.as_str(), - &serde_json::to_string(&item.category).unwrap(), - item.brand.as_deref().unwrap_or(""), - item.model.as_deref().unwrap_or(""), - &serde_json::to_string(&item.color_primary).unwrap(), - item.color_secondary.as_ref().map(|c| serde_json::to_string(c).unwrap()).as_deref().unwrap_or(""), - &serde_json::to_string(&item.styles).unwrap(), - &serde_json::to_string(&item.design_elements).unwrap(), - item.size.as_ref().map(|s| serde_json::to_string(s).unwrap()).as_deref().unwrap_or(""), - item.material.as_ref().map(|m| serde_json::to_string(m).unwrap()).as_deref().unwrap_or(""), - item.price.map(|p| p.to_string()).as_deref().unwrap_or(""), - item.purchase_date.as_ref().map(|d| d.to_rfc3339()).as_deref().unwrap_or(""), - &serde_json::to_string(&item.image_urls).unwrap(), - &serde_json::to_string(&item.tags).unwrap(), - item.notes.as_deref().unwrap_or(""), - item.created_at.to_rfc3339().as_str(), - item.updated_at.to_rfc3339().as_str(), - ], - )?; - - Ok(item) - } - - /// 根据ID获取服装单品 - pub fn get_by_id(&self, id: &str) -> Result> { - let conn = self.database.get_read_connection(); - let conn = conn.lock().unwrap(); - - let mut stmt = conn.prepare( - "SELECT id, project_id, analysis_id, name, category, brand, model, - color_primary, color_secondary, styles, design_elements, - size_info, material_info, price, purchase_date, image_urls, - tags, notes, created_at, updated_at - FROM outfit_items WHERE id = ?1" - )?; - - let item = stmt.query_row([id], |row| { - self.row_to_outfit_item(row) - }).optional()?; - - Ok(item) - } - - /// 更新服装单品 - pub fn update(&self, id: &str, request: &UpdateOutfitItemRequest) -> Result<()> { - let conn = self.database.get_connection(); - let conn = conn.lock().unwrap(); - - let mut updates = Vec::new(); - let mut params: Vec> = Vec::new(); - - if let Some(name) = &request.name { - updates.push("name = ?"); - params.push(Box::new(name.clone())); - } - - if let Some(category) = &request.category { - updates.push("category = ?"); - params.push(Box::new(serde_json::to_string(category).unwrap())); - } - - if let Some(brand) = &request.brand { - updates.push("brand = ?"); - params.push(Box::new(brand.clone())); - } - - if let Some(color_primary) = &request.color_primary { - updates.push("color_primary = ?"); - params.push(Box::new(serde_json::to_string(color_primary).unwrap())); - } - - if let Some(styles) = &request.styles { - updates.push("styles = ?"); - params.push(Box::new(serde_json::to_string(styles).unwrap())); - } - - if let Some(tags) = &request.tags { - updates.push("tags = ?"); - params.push(Box::new(serde_json::to_string(tags).unwrap())); - } - - if !updates.is_empty() { - updates.push("updated_at = ?"); - params.push(Box::new(Utc::now().to_rfc3339())); - params.push(Box::new(id.to_string())); - - let sql = format!( - "UPDATE outfit_items SET {} WHERE id = ?", - updates.join(", ") - ); - - let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect(); - conn.execute(&sql, param_refs.as_slice())?; - } - - Ok(()) - } - - /// 删除服装单品 - pub fn delete(&self, id: &str) -> Result<()> { - let conn = self.database.get_connection(); - let conn = conn.lock().unwrap(); - - conn.execute("DELETE FROM outfit_items WHERE id = ?1", [id])?; - Ok(()) - } - - /// 查询服装单品列表 - pub fn list(&self, options: &OutfitItemQueryOptions) -> Result> { - let conn = self.database.get_read_connection(); - let conn = conn.lock().unwrap(); - - let mut sql = "SELECT id, project_id, analysis_id, name, category, brand, model, - color_primary, color_secondary, styles, design_elements, - size_info, material_info, price, purchase_date, image_urls, - tags, notes, created_at, updated_at - FROM outfit_items WHERE 1=1".to_string(); - let mut params: Vec> = Vec::new(); - - if let Some(project_id) = &options.project_id { - sql.push_str(" AND project_id = ?"); - params.push(Box::new(project_id.clone())); - } - - if let Some(category) = &options.category { - sql.push_str(" AND category = ?"); - params.push(Box::new(serde_json::to_string(category).unwrap())); - } - - if let Some(brand) = &options.brand { - sql.push_str(" AND brand = ?"); - params.push(Box::new(brand.clone())); - } - - sql.push_str(" ORDER BY created_at DESC"); - - if let Some(limit) = options.limit { - sql.push_str(&format!(" LIMIT {}", limit)); - } - - if let Some(offset) = options.offset { - sql.push_str(&format!(" OFFSET {}", offset)); - } - - let mut stmt = conn.prepare(&sql)?; - let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect(); - - let item_iter = stmt.query_map(param_refs.as_slice(), |row| { - self.row_to_outfit_item(row) - })?; - - let mut items = Vec::new(); - for item in item_iter { - items.push(item?); - } - - Ok(items) - } - - /// 根据颜色相似度搜索服装单品 - pub fn search_by_color(&self, project_id: &str, target_color: &ColorHSV, threshold: f64) -> Result> { - let items = self.list(&OutfitItemQueryOptions { - project_id: Some(project_id.to_string()), - category: None, - styles: None, - color_similarity_threshold: None, - target_color: None, - brand: None, - tags: None, - limit: None, - offset: None, - })?; - - let mut matching_items = Vec::new(); - for item in items { - let similarity = item.color_primary.similarity(target_color); - if similarity >= threshold { - matching_items.push(item); - } - } - - // 按相似度排序 - matching_items.sort_by(|a, b| { - let sim_a = a.color_primary.similarity(target_color); - let sim_b = b.color_primary.similarity(target_color); - sim_b.partial_cmp(&sim_a).unwrap_or(std::cmp::Ordering::Equal) - }); - - Ok(matching_items) - } - - /// 获取服装单品统计信息 - pub fn get_stats(&self, project_id: Option<&str>) -> Result { - let conn = self.database.get_read_connection(); - let conn = conn.lock().unwrap(); - - let mut sql = "SELECT COUNT(*) as total FROM outfit_items".to_string(); - let mut params: Vec> = Vec::new(); - - if let Some(project_id) = project_id { - sql.push_str(" WHERE project_id = ?"); - params.push(Box::new(project_id.to_string())); - } - - let mut stmt = conn.prepare(&sql)?; - let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect(); - let total_count: u32 = stmt.query_row(param_refs.as_slice(), |row| row.get(0))?; - - Ok(OutfitItemStats { - total_count, - category_counts: std::collections::HashMap::new(), - style_counts: std::collections::HashMap::new(), - brand_counts: std::collections::HashMap::new(), - }) - } - - /// 将数据库行转换为OutfitItem对象 - fn row_to_outfit_item(&self, row: &Row) -> Result { - let category_str: String = row.get(4)?; - let category = serde_json::from_str(&category_str).unwrap_or(OutfitCategory::Other); - - let color_primary_str: String = row.get(7)?; - let color_primary = serde_json::from_str(&color_primary_str) - .unwrap_or(ColorHSV::new(0.0, 0.0, 0.0)); - - let color_secondary_str: Option = row.get(8)?; - let color_secondary = color_secondary_str - .and_then(|s| serde_json::from_str(&s).ok()); - - let styles_str: String = row.get(9)?; - let styles = serde_json::from_str(&styles_str).unwrap_or_default(); - - let design_elements_str: String = row.get(10)?; - let design_elements = serde_json::from_str(&design_elements_str).unwrap_or_default(); - - let image_urls_str: String = row.get(15)?; - let image_urls = serde_json::from_str(&image_urls_str).unwrap_or_default(); - - let tags_str: String = row.get(16)?; - let tags = serde_json::from_str(&tags_str).unwrap_or_default(); - - let created_at_str: String = row.get(18)?; - let updated_at_str: String = row.get(19)?; - - Ok(OutfitItem { - id: row.get(0)?, - project_id: row.get(1)?, - analysis_id: row.get(2)?, - name: row.get(3)?, - category, - brand: row.get(5)?, - model: row.get(6)?, - color_primary, - color_secondary, - styles, - design_elements, - size: None, // TODO: 解析size_info JSON - material: None, // TODO: 解析material_info JSON - price: row.get::<_, Option>(13)?.and_then(|s| s.parse().ok()), - purchase_date: row.get::<_, Option>(14)? - .and_then(|s| DateTime::parse_from_rfc3339(&s).ok()) - .map(|dt| dt.with_timezone(&Utc)), - image_urls, - tags, - notes: row.get(17)?, - created_at: DateTime::parse_from_rfc3339(&created_at_str).unwrap().with_timezone(&Utc), - updated_at: DateTime::parse_from_rfc3339(&updated_at_str).unwrap().with_timezone(&Utc), - }) - } -} diff --git a/apps/desktop/src-tauri/src/data/repositories/outfit_matching_repository.rs b/apps/desktop/src-tauri/src/data/repositories/outfit_matching_repository.rs deleted file mode 100644 index 56a0c04..0000000 --- a/apps/desktop/src-tauri/src/data/repositories/outfit_matching_repository.rs +++ /dev/null @@ -1,347 +0,0 @@ -use rusqlite::{Result, Row, OptionalExtension}; -use std::sync::Arc; -use chrono::{DateTime, Utc}; -use crate::data::models::outfit_matching::{ - OutfitMatching, CreateOutfitMatchingRequest, UpdateOutfitMatchingRequest, - OutfitMatchingQueryOptions, OutfitMatchingStats, MatchingType, - MatchingScoreDetails, MatchingSuggestion, MatchingOutfitItem -}; -use crate::data::models::outfit_analysis::ColorHSV; -use crate::infrastructure::database::Database; - -/// 服装搭配数据仓库 -/// 遵循 Tauri 开发规范的数据访问层设计 -pub struct OutfitMatchingRepository { - database: Arc, -} - -impl OutfitMatchingRepository { - /// 创建新的服装搭配仓库实例 - pub fn new(database: Arc) -> Result { - Ok(OutfitMatchingRepository { database }) - } - - /// 创建服装搭配 - pub fn create(&self, request: &CreateOutfitMatchingRequest) -> Result { - let conn = self.database.get_connection(); - let conn = conn.lock().unwrap(); - - // 创建默认评分详情 - let mut score_details = MatchingScoreDetails { - color_harmony_score: 0.0, - style_consistency_score: 0.0, - proportion_score: 0.0, - occasion_appropriateness: 0.0, - trend_factor: 0.0, - overall_score: 0.0, - }; - score_details.calculate_overall_score(); - - let matching = OutfitMatching { - id: uuid::Uuid::new_v4().to_string(), - project_id: request.project_id.clone(), - matching_name: request.matching_name.clone(), - matching_type: request.matching_type.clone(), - items: Vec::new(), // 将在后续填充 - score_details, - suggestions: Vec::new(), - occasion_tags: request.occasion_tags.clone(), - season_tags: request.season_tags.clone(), - style_description: request.style_description.clone().unwrap_or_default(), - color_palette: Vec::new(), - is_favorite: false, - wear_count: 0, - last_worn_date: None, - created_at: Utc::now(), - updated_at: Utc::now(), - }; - - conn.execute( - "INSERT INTO outfit_matchings ( - id, project_id, matching_name, matching_type, items, - score_details, suggestions, occasion_tags, season_tags, - style_description, color_palette, is_favorite, wear_count, - last_worn_date, created_at, updated_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)", - [ - matching.id.as_str(), - matching.project_id.as_str(), - matching.matching_name.as_str(), - &serde_json::to_string(&matching.matching_type).unwrap(), - &serde_json::to_string(&request.item_ids).unwrap(), // 暂时存储item_ids - &serde_json::to_string(&matching.score_details).unwrap(), - &serde_json::to_string(&matching.suggestions).unwrap(), - &serde_json::to_string(&matching.occasion_tags).unwrap(), - &serde_json::to_string(&matching.season_tags).unwrap(), - matching.style_description.as_str(), - &serde_json::to_string(&matching.color_palette).unwrap(), - if matching.is_favorite { "1" } else { "0" }, - &matching.wear_count.to_string(), - matching.last_worn_date.as_ref().map(|d| d.to_rfc3339()).as_deref().unwrap_or(""), - matching.created_at.to_rfc3339().as_str(), - matching.updated_at.to_rfc3339().as_str(), - ], - )?; - - Ok(matching) - } - - /// 根据ID获取服装搭配 - pub fn get_by_id(&self, id: &str) -> Result> { - let conn = self.database.get_read_connection(); - let conn = conn.lock().unwrap(); - - let mut stmt = conn.prepare( - "SELECT id, project_id, matching_name, matching_type, items, - score_details, suggestions, occasion_tags, season_tags, - style_description, color_palette, is_favorite, wear_count, - last_worn_date, created_at, updated_at - FROM outfit_matchings WHERE id = ?1" - )?; - - let matching = stmt.query_row([id], |row| { - self.row_to_outfit_matching(row) - }).optional()?; - - Ok(matching) - } - - /// 更新服装搭配 - pub fn update(&self, id: &str, request: &UpdateOutfitMatchingRequest) -> Result<()> { - let conn = self.database.get_connection(); - let conn = conn.lock().unwrap(); - - let mut updates = Vec::new(); - let mut params: Vec> = Vec::new(); - - if let Some(name) = &request.matching_name { - updates.push("matching_name = ?"); - params.push(Box::new(name.clone())); - } - - if let Some(matching_type) = &request.matching_type { - updates.push("matching_type = ?"); - params.push(Box::new(serde_json::to_string(matching_type).unwrap())); - } - - if let Some(item_ids) = &request.item_ids { - updates.push("items = ?"); - params.push(Box::new(serde_json::to_string(item_ids).unwrap())); - } - - if let Some(occasion_tags) = &request.occasion_tags { - updates.push("occasion_tags = ?"); - params.push(Box::new(serde_json::to_string(occasion_tags).unwrap())); - } - - if let Some(season_tags) = &request.season_tags { - updates.push("season_tags = ?"); - params.push(Box::new(serde_json::to_string(season_tags).unwrap())); - } - - if let Some(style_description) = &request.style_description { - updates.push("style_description = ?"); - params.push(Box::new(style_description.clone())); - } - - if let Some(is_favorite) = request.is_favorite { - updates.push("is_favorite = ?"); - params.push(Box::new(if is_favorite { "1" } else { "0" })); - } - - if !updates.is_empty() { - updates.push("updated_at = ?"); - params.push(Box::new(Utc::now().to_rfc3339())); - params.push(Box::new(id.to_string())); - - let sql = format!( - "UPDATE outfit_matchings SET {} WHERE id = ?", - updates.join(", ") - ); - - let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect(); - conn.execute(&sql, param_refs.as_slice())?; - } - - Ok(()) - } - - /// 删除服装搭配 - pub fn delete(&self, id: &str) -> Result<()> { - let conn = self.database.get_connection(); - let conn = conn.lock().unwrap(); - - conn.execute("DELETE FROM outfit_matchings WHERE id = ?1", [id])?; - Ok(()) - } - - /// 查询服装搭配列表 - pub fn list(&self, options: &OutfitMatchingQueryOptions) -> Result> { - let conn = self.database.get_read_connection(); - let conn = conn.lock().unwrap(); - - let mut sql = "SELECT id, project_id, matching_name, matching_type, items, - score_details, suggestions, occasion_tags, season_tags, - style_description, color_palette, is_favorite, wear_count, - last_worn_date, created_at, updated_at - FROM outfit_matchings WHERE 1=1".to_string(); - let mut params: Vec> = Vec::new(); - - if let Some(project_id) = &options.project_id { - sql.push_str(" AND project_id = ?"); - params.push(Box::new(project_id.clone())); - } - - // 添加排序 - let sort_by = options.sort_by.as_deref().unwrap_or("created_at"); - let sort_order = options.sort_order.as_deref().unwrap_or("desc"); - sql.push_str(&format!(" ORDER BY {} {}", sort_by, sort_order.to_uppercase())); - - if let Some(limit) = options.limit { - sql.push_str(&format!(" LIMIT {}", limit)); - } - - if let Some(offset) = options.offset { - sql.push_str(&format!(" OFFSET {}", offset)); - } - - let mut stmt = conn.prepare(&sql)?; - let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect(); - - let matching_iter = stmt.query_map(param_refs.as_slice(), |row| { - self.row_to_outfit_matching(row) - })?; - - let mut matchings = Vec::new(); - for matching in matching_iter { - matchings.push(matching?); - } - - Ok(matchings) - } - - /// 增加穿着次数 - pub fn increment_wear_count(&self, id: &str) -> Result<()> { - let conn = self.database.get_connection(); - let conn = conn.lock().unwrap(); - - conn.execute( - "UPDATE outfit_matchings - SET wear_count = wear_count + 1, - last_worn_date = ?, - updated_at = ? - WHERE id = ?", - [ - Utc::now().to_rfc3339().as_str(), - Utc::now().to_rfc3339().as_str(), - id, - ], - )?; - - Ok(()) - } - - /// 获取服装搭配统计信息 - pub fn get_stats(&self, project_id: Option<&str>) -> Result { - let conn = self.database.get_read_connection(); - let conn = conn.lock().unwrap(); - - let mut sql = "SELECT COUNT(*) as total, - COUNT(CASE WHEN is_favorite = '1' THEN 1 END) as favorites, - AVG(CAST(JSON_EXTRACT(score_details, '$.overall_score') AS REAL)) as avg_score - FROM outfit_matchings".to_string(); - let mut params: Vec> = Vec::new(); - - if let Some(project_id) = project_id { - sql.push_str(" WHERE project_id = ?"); - params.push(Box::new(project_id.to_string())); - } - - let mut stmt = conn.prepare(&sql)?; - let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect(); - - let (total_matchings, favorite_count, average_score) = stmt.query_row(param_refs.as_slice(), |row| { - Ok(( - row.get::<_, u32>(0)?, - row.get::<_, u32>(1)?, - row.get::<_, Option>(2)?.unwrap_or(0.0), - )) - })?; - - Ok(OutfitMatchingStats { - total_matchings, - favorite_count, - average_score, - most_worn_matching_id: None, // TODO: 实现最常穿搭配查询 - style_distribution: std::collections::HashMap::new(), - occasion_distribution: std::collections::HashMap::new(), - season_distribution: std::collections::HashMap::new(), - }) - } - - /// 将数据库行转换为OutfitMatching对象 - fn row_to_outfit_matching(&self, row: &Row) -> Result { - let matching_type_str: String = row.get(3)?; - let matching_type = serde_json::from_str(&matching_type_str) - .unwrap_or(MatchingType::ColorHarmony); - - let items_str: String = row.get(4)?; - let items = serde_json::from_str(&items_str).unwrap_or_default(); - - let score_details_str: String = row.get(5)?; - let score_details = serde_json::from_str(&score_details_str) - .unwrap_or_else(|_| MatchingScoreDetails { - color_harmony_score: 0.0, - style_consistency_score: 0.0, - proportion_score: 0.0, - occasion_appropriateness: 0.0, - trend_factor: 0.0, - overall_score: 0.0, - }); - - let suggestions_str: String = row.get(6)?; - let suggestions = serde_json::from_str(&suggestions_str).unwrap_or_default(); - - let occasion_tags_str: String = row.get(7)?; - let occasion_tags = serde_json::from_str(&occasion_tags_str).unwrap_or_default(); - - let season_tags_str: String = row.get(8)?; - let season_tags = serde_json::from_str(&season_tags_str).unwrap_or_default(); - - let color_palette_str: String = row.get(10)?; - let color_palette = serde_json::from_str(&color_palette_str).unwrap_or_default(); - - let is_favorite_str: String = row.get(11)?; - let is_favorite = is_favorite_str == "1"; - - let wear_count_str: String = row.get(12)?; - let wear_count = wear_count_str.parse().unwrap_or(0); - - let last_worn_date_str: Option = row.get(13)?; - let last_worn_date = last_worn_date_str - .and_then(|s| DateTime::parse_from_rfc3339(&s).ok()) - .map(|dt| dt.with_timezone(&Utc)); - - let created_at_str: String = row.get(14)?; - let updated_at_str: String = row.get(15)?; - - Ok(OutfitMatching { - id: row.get(0)?, - project_id: row.get(1)?, - matching_name: row.get(2)?, - matching_type, - items, - score_details, - suggestions, - occasion_tags, - season_tags, - style_description: row.get(9)?, - color_palette, - is_favorite, - wear_count, - last_worn_date, - created_at: DateTime::parse_from_rfc3339(&created_at_str).unwrap().with_timezone(&Utc), - updated_at: DateTime::parse_from_rfc3339(&updated_at_str).unwrap().with_timezone(&Utc), - }) - } -} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 7a32895..9c40492 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -255,30 +255,7 @@ pub fn run() { commands::debug_commands::test_parse_draft_file, commands::debug_commands::validate_template_structure, // 便捷工具命令 - commands::tools_commands::clean_jsonl_data, - // 服装搭配命令 - commands::outfit_commands::create_outfit_analysis, - commands::outfit_commands::start_outfit_analysis, - commands::outfit_commands::list_outfit_analyses, - commands::outfit_commands::get_outfit_analysis_by_id, - commands::outfit_commands::delete_outfit_analysis, - commands::outfit_commands::get_outfit_analysis_stats, - commands::outfit_commands::create_outfit_item, - commands::outfit_commands::create_outfit_items_from_analysis, - commands::outfit_commands::list_outfit_items, - commands::outfit_commands::get_outfit_item_by_id, - commands::outfit_commands::update_outfit_item, - commands::outfit_commands::delete_outfit_item, - commands::outfit_commands::create_outfit_matching, - commands::outfit_commands::list_outfit_matchings, - commands::outfit_commands::get_outfit_matching_by_id, - commands::outfit_commands::update_outfit_matching, - commands::outfit_commands::delete_outfit_matching, - commands::outfit_commands::smart_outfit_matching, - commands::outfit_commands::increment_outfit_matching_wear_count, - commands::outfit_commands::save_outfit_image, - commands::outfit_commands::generate_outfit_recommendations, - commands::outfit_commands::debug_outfit_items_stats + commands::tools_commands::clean_jsonl_data ]) .setup(|app| { // 初始化日志系统 diff --git a/apps/desktop/src-tauri/src/presentation/commands/mod.rs b/apps/desktop/src-tauri/src/presentation/commands/mod.rs index cefec33..67618b5 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/mod.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/mod.rs @@ -17,4 +17,3 @@ pub mod template_matching_result_commands; pub mod export_record_commands; pub mod video_generation_commands; pub mod tools_commands; -pub mod outfit_commands; diff --git a/apps/desktop/src-tauri/src/presentation/commands/outfit_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/outfit_commands.rs deleted file mode 100644 index 208570f..0000000 --- a/apps/desktop/src-tauri/src/presentation/commands/outfit_commands.rs +++ /dev/null @@ -1,1097 +0,0 @@ -/** - * 服装搭配相关的 Tauri 命令 - * 遵循 Tauri 开发规范的 API 设计原则 - */ - -use tauri::{command, State}; -use std::sync::Arc; -use serde_json::Value; -use serde::{Serialize, Deserialize}; - -use crate::app_state::AppState; -use crate::business::services::outfit_analysis_service::OutfitAnalysisService; -use crate::business::services::outfit_item_service::OutfitItemService; -use crate::business::services::outfit_matching_service::OutfitMatchingService; -use crate::data::models::outfit_analysis::{ - OutfitAnalysis, CreateOutfitAnalysisRequest, - OutfitAnalysisQueryOptions, OutfitAnalysisStats -}; -use crate::data::models::outfit_item::{ - OutfitItem, CreateOutfitItemRequest, UpdateOutfitItemRequest, - OutfitItemQueryOptions -}; -use crate::data::models::outfit_matching::{ - OutfitMatching, CreateOutfitMatchingRequest, UpdateOutfitMatchingRequest, - OutfitMatchingQueryOptions, SmartMatchingRequest, SmartMatchingResult -}; -use crate::data::repositories::outfit_analysis_repository::OutfitAnalysisRepository; -use crate::data::repositories::outfit_item_repository::OutfitItemRepository; -use crate::data::repositories::outfit_matching_repository::OutfitMatchingRepository; -use crate::infrastructure::gemini_service::GeminiService; - -// 辅助函数:创建服务实例 -fn create_analysis_service(database: Arc) -> Result { - let analysis_repository = OutfitAnalysisRepository::new(database.clone()) - .map_err(|e| format!("创建分析仓库失败: {}", e))?; - let gemini_service = GeminiService::new(None); - Ok(OutfitAnalysisService::new( - Arc::new(analysis_repository), - Arc::new(gemini_service) - )) -} - -fn create_item_service(database: Arc) -> Result { - let item_repository = OutfitItemRepository::new(database.clone()) - .map_err(|e| format!("创建单品仓库失败: {}", e))?; - Ok(OutfitItemService::new(Arc::new(item_repository))) -} - -fn create_matching_service(database: Arc) -> Result { - let matching_repository = OutfitMatchingRepository::new(database.clone()) - .map_err(|e| format!("创建搭配仓库失败: {}", e))?; - let item_repository = OutfitItemRepository::new(database.clone()) - .map_err(|e| format!("创建单品仓库失败: {}", e))?; - Ok(OutfitMatchingService::new( - Arc::new(matching_repository), - Arc::new(item_repository) - )) -} - -/// 创建服装分析记录 -#[command] -pub async fn create_outfit_analysis( - state: State<'_, AppState>, - request: CreateOutfitAnalysisRequest, -) -> Result { - let database = state.get_database(); - let service = create_analysis_service(database)?; - service.create_analysis(request).await - .map_err(|e| format!("创建分析记录失败: {}", e)) -} - -/// 开始分析 -#[command] -pub async fn start_outfit_analysis( - state: State<'_, AppState>, - analysis_id: String, -) -> Result<(), String> { - let database = state.get_database(); - let service = create_analysis_service(database)?; - service.start_analysis(&analysis_id).await - .map_err(|e| format!("开始分析失败: {}", e)) -} - -/// 获取分析记录列表 -#[command] -pub async fn list_outfit_analyses( - state: State<'_, AppState>, - options: OutfitAnalysisQueryOptions, -) -> Result, String> { - let database = state.get_database(); - let service = create_analysis_service(database)?; - service.get_analyses(options).await - .map_err(|e| format!("获取分析记录列表失败: {}", e)) -} - -/// 根据ID获取服装分析记录 -#[command] -pub async fn get_outfit_analysis_by_id( - state: State<'_, AppState>, - id: String, -) -> Result, String> { - let database = state.get_database(); - let service = create_analysis_service(database)?; - service.get_analysis_by_id(&id).await - .map_err(|e| format!("获取分析记录失败: {}", e)) -} - -/// 删除分析记录 -#[command] -pub async fn delete_outfit_analysis( - state: State<'_, AppState>, - id: String, -) -> Result<(), String> { - let database = state.get_database(); - let service = create_analysis_service(database)?; - service.delete_analysis(&id).await - .map_err(|e| format!("删除分析记录失败: {}", e)) -} - -/// 获取分析统计信息 -#[command] -pub async fn get_outfit_analysis_stats( - state: State<'_, AppState>, - project_id: Option, -) -> Result { - let database = state.get_database(); - let service = create_analysis_service(database)?; - service.get_analysis_stats(project_id.as_deref()).await - .map_err(|e| format!("获取分析统计信息失败: {}", e)) -} - -/// 创建服装单品 -#[command] -pub async fn create_outfit_item( - state: State<'_, AppState>, - request: CreateOutfitItemRequest, -) -> Result { - let database = state.get_database(); - let service = create_item_service(database)?; - service.create_item(request).await - .map_err(|e| format!("创建服装单品失败: {}", e)) -} - -/// 从分析结果创建服装单品 -#[command] -pub async fn create_outfit_items_from_analysis( - state: State<'_, AppState>, - project_id: String, - analysis_id: String, -) -> Result, String> { - let database = state.get_database(); - let analysis_service = create_analysis_service(database.clone())?; - let item_service = create_item_service(database)?; - - // 1. 获取分析结果 - let analysis = analysis_service.get_analysis_by_id(&analysis_id).await - .map_err(|e| format!("获取分析结果失败: {}", e))? - .ok_or("分析结果不存在")?; - - // 2. 检查分析状态 - if analysis.analysis_status != crate::data::models::outfit_analysis::AnalysisStatus::Completed { - return Err("分析尚未完成,无法创建服装单品".to_string()); - } - - // 3. 解析分析结果 - let analysis_result = analysis.analysis_result - .ok_or("分析结果为空")?; - - // 4. 从分析结果中提取产品信息 - let products = extract_products_from_image_analysis(&analysis_result); - - if products.is_empty() { - return Err("分析结果中没有找到服装单品".to_string()); - } - - // 5. 为每个产品创建服装单品 - let mut created_items = Vec::new(); - for (index, product) in products.iter().enumerate() { - // 转换类别字符串为枚举 - let category = match product.category.as_str() { - "上衣" | "T恤" | "衬衫" | "毛衣" | "背心" => crate::data::models::outfit_item::OutfitCategory::Top, - "下装" | "裤子" | "短裤" | "裙子" => crate::data::models::outfit_item::OutfitCategory::Bottom, - "外套" | "夹克" | "大衣" => crate::data::models::outfit_item::OutfitCategory::Outerwear, - "连衣裙" => crate::data::models::outfit_item::OutfitCategory::Dress, - "鞋子" | "运动鞋" | "高跟鞋" | "靴子" => crate::data::models::outfit_item::OutfitCategory::Footwear, - "包包" | "手提包" | "背包" | "配饰" | "帽子" | "围巾" | "手表" => crate::data::models::outfit_item::OutfitCategory::Accessory, - _ => crate::data::models::outfit_item::OutfitCategory::Other, - }; - - // 转换颜色信息 - let color_primary = if let Some(color_value) = &product.color_pattern { - parse_color_from_value(color_value).unwrap_or_else(|| { - crate::data::models::outfit_analysis::ColorHSV::new(0.0, 0.0, 0.5) - }) - } else { - crate::data::models::outfit_analysis::ColorHSV::new(0.0, 0.0, 0.5) - }; - - let request = CreateOutfitItemRequest { - project_id: project_id.clone(), - analysis_id: Some(analysis_id.clone()), - name: product.name.clone(), - category, - brand: None, // AI分析通常不能识别品牌 - model: None, - color_primary, - color_secondary: None, - styles: vec![], // 暂时为空,可以后续扩展 - design_elements: product.design_styles.clone().unwrap_or_default(), - size: None, // AI分析通常不能识别尺寸 - material: None, - price: None, // AI分析不包含价格信息 - purchase_date: None, - image_urls: vec![analysis.image_path.clone()], - tags: product.tags.clone().unwrap_or_default(), - notes: Some(format!("从AI分析结果自动创建 (分析ID: {})", analysis_id)), - }; - - match item_service.create_item(request).await { - Ok(item) => { - created_items.push(item); - println!("✅ 成功创建服装单品 {}: {}", index + 1, product.name); - } - Err(e) => { - println!("❌ 创建服装单品失败 {}: {} - {}", index + 1, product.name, e); - // 继续创建其他单品,不因为一个失败而停止 - } - } - } - - if created_items.is_empty() { - return Err("没有成功创建任何服装单品".to_string()); - } - - println!("🎉 成功从分析结果创建了 {} 个服装单品", created_items.len()); - Ok(created_items) -} - -/// 生成智能搭配推荐 -#[command] -pub async fn generate_outfit_recommendations( - state: State<'_, AppState>, - request: GenerateRecommendationRequest, -) -> Result, String> { - let database = state.get_database(); - let item_service = create_item_service(database.clone())?; - let matching_service = create_matching_service(database)?; - - // 1. 获取项目中的所有服装单品 - let query_options = OutfitItemQueryOptions { - project_id: Some(request.project_id.clone()), - category: None, - styles: None, - color_similarity_threshold: None, - target_color: None, - brand: None, - tags: None, - limit: Some(100), - offset: Some(0), - }; - - let items = item_service.get_items(query_options).await - .map_err(|e| format!("获取服装单品失败: {}", e))?; - - println!("🔍 项目 {} 中找到 {} 件服装单品", request.project_id, items.len()); - for (i, item) in items.iter().enumerate() { - println!(" {}. {} - {} ({})", i + 1, item.name, item.category.to_chinese(), item.id); - } - - if items.len() < 2 { - return Err(format!("至少需要2件服装单品才能生成搭配推荐,当前项目 {} 中只有 {} 件", request.project_id, items.len())); - } - - // 2. 生成搭配组合 - let combinations = generate_outfit_combinations(&items, &request); - println!("🔄 生成了 {} 个搭配组合", combinations.len()); - - // 3. 评估每个组合的搭配效果 - let mut recommendations = Vec::new(); - for (i, combination) in combinations.iter().enumerate() { - println!(" 组合 {}: {} 件单品", i + 1, combination.len()); - for item in combination { - println!(" - {} ({})", item.name, item.category.to_chinese()); - } - - if let Some(recommendation) = evaluate_outfit_combination(combination.clone(), &request) { - println!(" ✅ 评分: {:.2}", recommendation.score); - recommendations.push(recommendation); - } else { - println!(" ❌ 评估失败或评分过低"); - } - } - - // 4. 按评分排序并限制数量 - recommendations.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal)); - recommendations.truncate(request.max_recommendations); - - // 5. 过滤低分推荐 - recommendations.retain(|rec| rec.score >= request.min_score_threshold); - - println!("🎯 生成了 {} 个搭配推荐", recommendations.len()); - Ok(recommendations) -} - -/// 调试:获取项目服装单品统计 -#[command] -pub async fn debug_outfit_items_stats( - state: State<'_, AppState>, - project_id: String, -) -> Result { - let database = state.get_database(); - let item_service = create_item_service(database)?; - - // 获取指定项目的所有服装单品 - let query_options = OutfitItemQueryOptions { - project_id: Some(project_id.clone()), - category: None, - styles: None, - color_similarity_threshold: None, - target_color: None, - brand: None, - tags: None, - limit: Some(1000), - offset: Some(0), - }; - - let items = item_service.get_items(query_options).await - .map_err(|e| format!("获取服装单品失败: {}", e))?; - - let mut result = format!("📊 项目 {} 的服装单品统计\n总计: {} 件\n\n", project_id, items.len()); - - if items.is_empty() { - result.push_str("❌ 该项目中没有任何服装单品\n"); - result.push_str("💡 建议:\n"); - result.push_str("1. 先上传服装图片进行AI分析\n"); - result.push_str("2. 从分析结果创建服装单品\n"); - result.push_str("3. 或手动添加服装单品\n"); - } else { - for (i, item) in items.iter().enumerate() { - result.push_str(&format!("{}. {} - {} (ID: {})\n", - i + 1, item.name, item.category.to_chinese(), item.id)); - } - - if items.len() < 2 { - result.push_str("\n⚠️ 需要至少2件服装单品才能生成搭配推荐\n"); - } else { - result.push_str("\n✅ 服装单品数量足够,可以生成搭配推荐\n"); - } - } - - println!("{}", result); - Ok(result) -} - - - -/// 获取服装单品列表 -#[command] -pub async fn list_outfit_items( - state: State<'_, AppState>, - options: OutfitItemQueryOptions, -) -> Result, String> { - let database = state.get_database(); - let service = create_item_service(database)?; - service.get_items(options).await - .map_err(|e| format!("获取服装单品列表失败: {}", e)) -} - -/// 根据ID获取服装单品 -#[command] -pub async fn get_outfit_item_by_id( - state: State<'_, AppState>, - id: String, -) -> Result, String> { - let database = state.get_database(); - let service = create_item_service(database)?; - service.get_item_by_id(&id).await - .map_err(|e| format!("获取服装单品失败: {}", e)) -} - -/// 更新服装单品 -#[command] -pub async fn update_outfit_item( - state: State<'_, AppState>, - id: String, - request: UpdateOutfitItemRequest, -) -> Result<(), String> { - let database = state.get_database(); - let service = create_item_service(database)?; - service.update_item(&id, request).await - .map_err(|e| format!("更新服装单品失败: {}", e)) -} - -/// 删除服装单品 -#[command] -pub async fn delete_outfit_item( - state: State<'_, AppState>, - id: String, -) -> Result<(), String> { - let database = state.get_database(); - let service = create_item_service(database)?; - service.delete_item(&id).await - .map_err(|e| format!("删除服装单品失败: {}", e)) -} - -/// 创建服装搭配 -#[command] -pub async fn create_outfit_matching( - state: State<'_, AppState>, - request: CreateOutfitMatchingRequest, -) -> Result { - let database = state.get_database(); - let service = create_matching_service(database)?; - service.create_matching(request).await - .map_err(|e| format!("创建服装搭配失败: {}", e)) -} - -/// 获取服装搭配列表 -#[command] -pub async fn list_outfit_matchings( - state: State<'_, AppState>, - options: OutfitMatchingQueryOptions, -) -> Result, String> { - let database = state.get_database(); - let service = create_matching_service(database)?; - service.get_matchings(options).await - .map_err(|e| format!("获取服装搭配列表失败: {}", e)) -} - -/// 根据ID获取服装搭配 -#[command] -pub async fn get_outfit_matching_by_id( - state: State<'_, AppState>, - id: String, -) -> Result, String> { - let database = state.get_database(); - let service = create_matching_service(database)?; - service.get_matching_by_id(&id).await - .map_err(|e| format!("获取服装搭配失败: {}", e)) -} - -/// 更新服装搭配 -#[command] -pub async fn update_outfit_matching( - state: State<'_, AppState>, - id: String, - request: UpdateOutfitMatchingRequest, -) -> Result<(), String> { - let database = state.get_database(); - let service = create_matching_service(database)?; - service.update_matching(&id, request).await - .map_err(|e| format!("更新服装搭配失败: {}", e)) -} - -/// 删除服装搭配 -#[command] -pub async fn delete_outfit_matching( - state: State<'_, AppState>, - id: String, -) -> Result<(), String> { - let database = state.get_database(); - let service = create_matching_service(database)?; - service.delete_matching(&id).await - .map_err(|e| format!("删除服装搭配失败: {}", e)) -} - -/// 智能搭配推荐 -#[command] -pub async fn smart_outfit_matching( - state: State<'_, AppState>, - request: SmartMatchingRequest, -) -> Result { - let database = state.get_database(); - let service = create_matching_service(database)?; - service.smart_matching(request).await - .map_err(|e| format!("智能搭配推荐失败: {}", e)) -} - -/// 增加搭配穿着次数 -#[command] -pub async fn increment_outfit_matching_wear_count( - state: State<'_, AppState>, - id: String, -) -> Result<(), String> { - let database = state.get_database(); - let service = create_matching_service(database)?; - service.increment_wear_count(&id).await - .map_err(|e| format!("增加搭配穿着次数失败: {}", e)) -} - -/// 保存上传的图片文件 -#[command] -pub async fn save_outfit_image( - state: State<'_, AppState>, - project_id: String, - file_name: String, - file_data: Vec, -) -> Result { - use std::fs; - use std::path::Path; - - // 获取项目路径 - let repository_guard = state.get_project_repository() - .map_err(|e| format!("获取项目仓库失败: {}", e))?; - - let repository = repository_guard.as_ref() - .ok_or("项目仓库未初始化")?; - - let project = repository.find_by_id(&project_id) - .map_err(|e| format!("获取项目失败: {}", e))? - .ok_or("项目不存在")?; - - // 创建保存目录 - let project_path = Path::new(&project.path); - let outfit_dir = project_path.join("outfit_images"); - - if !outfit_dir.exists() { - fs::create_dir_all(&outfit_dir) - .map_err(|e| format!("创建目录失败: {}", e))?; - } - - // 生成唯一文件名 - let timestamp = chrono::Utc::now().timestamp(); - let extension = Path::new(&file_name) - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or("jpg"); - let unique_name = format!("{}_{}.{}", timestamp, uuid::Uuid::new_v4(), extension); - let file_path = outfit_dir.join(&unique_name); - - // 保存文件 - fs::write(&file_path, file_data) - .map_err(|e| format!("保存文件失败: {}", e))?; - - // 返回文件路径 - Ok(file_path.to_string_lossy().to_string()) -} - -/// 从ImageAnalysisResult中提取产品信息 -fn extract_products_from_image_analysis(analysis_result: &crate::data::models::outfit_analysis::ImageAnalysisResult) -> Vec { - analysis_result.products.iter().map(|product| { - ProductInfo { - name: format!("{}", product.category), - category: product.category.clone(), - description: Some(product.description.clone()), - color_pattern: Some(serde_json::to_value(&product.color_pattern).unwrap_or(serde_json::Value::Null)), - design_styles: Some(product.design_styles.clone()), - tags: Some(vec![product.category.clone()]), - } - }).collect() -} - -/// 从分析结果中提取产品信息(备用方法) -fn extract_products_from_analysis(analysis_result: &Value) -> anyhow::Result> { - let mut products = Vec::new(); - - // 尝试从不同的结构中提取产品信息 - if let Some(products_array) = analysis_result.get("products").and_then(|v| v.as_array()) { - // 标准格式:{ "products": [...] } - for (index, product) in products_array.iter().enumerate() { - if let Some(product_info) = parse_product_from_json(product, index)? { - products.push(product_info); - } - } - } else if let Some(items_array) = analysis_result.get("items").and_then(|v| v.as_array()) { - // 备选格式:{ "items": [...] } - for (index, item) in items_array.iter().enumerate() { - if let Some(product_info) = parse_product_from_json(item, index)? { - products.push(product_info); - } - } - } else if analysis_result.is_array() { - // 直接是数组格式:[...] - if let Some(array) = analysis_result.as_array() { - for (index, item) in array.iter().enumerate() { - if let Some(product_info) = parse_product_from_json(item, index)? { - products.push(product_info); - } - } - } - } else { - // 尝试解析单个产品对象 - if let Some(product_info) = parse_product_from_json(analysis_result, 0)? { - products.push(product_info); - } - } - - Ok(products) -} - -/// 从JSON对象解析单个产品信息 -fn parse_product_from_json(json: &Value, index: usize) -> anyhow::Result> { - // 提取类别信息 - let category = json.get("category") - .and_then(|v| v.as_str()) - .or_else(|| json.get("type").and_then(|v| v.as_str())) - .or_else(|| json.get("clothing_type").and_then(|v| v.as_str())) - .unwrap_or("未分类") - .to_string(); - - // 如果没有有效的类别,跳过这个产品 - if category == "未分类" || category.is_empty() { - return Ok(None); - } - - // 提取名称 - let name = json.get("name") - .and_then(|v| v.as_str()) - .or_else(|| json.get("description").and_then(|v| v.as_str())) - .map(|s| s.to_string()) - .unwrap_or_else(|| format!("{} {}", category, index + 1)); - - // 提取描述 - let description = json.get("description") - .and_then(|v| v.as_str()) - .or_else(|| json.get("details").and_then(|v| v.as_str())) - .map(|s| s.to_string()); - - // 提取颜色信息 - let color_pattern = json.get("color_pattern") - .or_else(|| json.get("colors")) - .or_else(|| json.get("color")) - .cloned(); - - // 提取设计风格 - let design_styles = json.get("design_styles") - .or_else(|| json.get("styles")) - .or_else(|| json.get("style")) - .and_then(|v| { - if let Some(array) = v.as_array() { - Some(array.iter() - .filter_map(|s| s.as_str()) - .map(|s| s.to_string()) - .collect()) - } else if let Some(s) = v.as_str() { - Some(vec![s.to_string()]) - } else { - None - } - }); - - // 提取标签 - let tags = json.get("tags") - .and_then(|v| { - if let Some(array) = v.as_array() { - Some(array.iter() - .filter_map(|s| s.as_str()) - .map(|s| s.to_string()) - .collect()) - } else if let Some(s) = v.as_str() { - Some(vec![s.to_string()]) - } else { - None - } - }); - - Ok(Some(ProductInfo { - name, - category, - description, - color_pattern, - design_styles, - tags, - })) -} - -/// 产品信息结构体 -#[derive(Debug, Clone)] -struct ProductInfo { - name: String, - category: String, - description: Option, - color_pattern: Option, - design_styles: Option>, - tags: Option>, -} - -/// 从JSON值解析颜色信息 -fn parse_color_from_value(value: &Value) -> Option { - if let Some(obj) = value.as_object() { - let hue = obj.get("hue")?.as_f64().unwrap_or(0.0); - let saturation = obj.get("saturation")?.as_f64().unwrap_or(0.0); - let value = obj.get("value")?.as_f64().unwrap_or(0.5); - - Some(crate::data::models::outfit_analysis::ColorHSV::new( - hue.clamp(0.0, 1.0), - saturation.clamp(0.0, 1.0), - value.clamp(0.0, 1.0) - )) - } else { - None - } -} - -/// 生成推荐请求 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GenerateRecommendationRequest { - pub project_id: String, - pub max_recommendations: usize, - pub min_score_threshold: f64, - pub occasion_filter: Option, - pub season_filter: Option, - pub style_filter: Option, -} - -/// 搭配推荐结果 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OutfitRecommendation { - pub id: String, - pub items: Vec, - pub score: f64, - pub style_description: String, - pub occasion_tags: Vec, - pub season_tags: Vec, - pub color_harmony_score: f64, - pub style_consistency_score: f64, - pub created_at: String, -} - -/// 推荐中的服装单品 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RecommendationItem { - pub id: String, - pub name: String, - pub category: String, - pub brand: Option, - pub image_urls: Vec, - pub color_primary: crate::data::models::outfit_analysis::ColorHSV, - pub styles: Vec, - pub tags: Vec, -} - -/// 生成搭配组合 -fn generate_outfit_combinations(items: &[OutfitItem], request: &GenerateRecommendationRequest) -> Vec> { - let mut combinations = Vec::new(); - - // 按类别分组 - let mut tops = Vec::new(); - let mut bottoms = Vec::new(); - let mut dresses = Vec::new(); - let mut outerwear = Vec::new(); - let mut footwear = Vec::new(); - let mut accessories = Vec::new(); - - for item in items { - match item.category { - crate::data::models::outfit_item::OutfitCategory::Top => tops.push(item.clone()), - crate::data::models::outfit_item::OutfitCategory::Bottom => bottoms.push(item.clone()), - crate::data::models::outfit_item::OutfitCategory::Dress => dresses.push(item.clone()), - crate::data::models::outfit_item::OutfitCategory::Outerwear => outerwear.push(item.clone()), - crate::data::models::outfit_item::OutfitCategory::Footwear => footwear.push(item.clone()), - crate::data::models::outfit_item::OutfitCategory::Accessory => accessories.push(item.clone()), - _ => {} - } - } - - println!("📦 服装分类统计:"); - println!(" 上装: {} 件", tops.len()); - println!(" 下装: {} 件", bottoms.len()); - println!(" 连衣裙: {} 件", dresses.len()); - println!(" 外套: {} 件", outerwear.len()); - println!(" 鞋类: {} 件", footwear.len()); - println!(" 配饰: {} 件", accessories.len()); - - // 生成基本搭配组合 - // 1. 上装 + 下装 + 鞋子 (+ 可选外套/配饰) - for top in &tops { - for bottom in &bottoms { - for shoe in &footwear { - let mut combination = vec![top.clone(), bottom.clone(), shoe.clone()]; - - // 可选添加外套 - if !outerwear.is_empty() && combinations.len() < request.max_recommendations * 2 { - for outer in &outerwear { - let mut combo_with_outer = combination.clone(); - combo_with_outer.push(outer.clone()); - combinations.push(combo_with_outer); - } - } - - // 可选添加配饰 - if !accessories.is_empty() && combination.len() < 5 { - for accessory in accessories.iter().take(2) { - combination.push(accessory.clone()); - } - } - - combinations.push(combination); - - if combinations.len() >= request.max_recommendations * 3 { - break; - } - } - if combinations.len() >= request.max_recommendations * 3 { - break; - } - } - if combinations.len() >= request.max_recommendations * 3 { - break; - } - } - - // 2. 连衣裙 + 鞋子 (+ 可选外套/配饰) - for dress in &dresses { - for shoe in &footwear { - let mut combination = vec![dress.clone(), shoe.clone()]; - - // 可选添加外套 - if !outerwear.is_empty() { - for outer in outerwear.iter().take(1) { - combination.push(outer.clone()); - } - } - - // 可选添加配饰 - if !accessories.is_empty() && combination.len() < 4 { - for accessory in accessories.iter().take(2) { - combination.push(accessory.clone()); - } - } - - combinations.push(combination); - - if combinations.len() >= request.max_recommendations * 3 { - break; - } - } - if combinations.len() >= request.max_recommendations * 3 { - break; - } - } - - combinations -} - -/// 评估搭配组合 -fn evaluate_outfit_combination(combination: Vec, request: &GenerateRecommendationRequest) -> Option { - if combination.len() < 2 { - return None; - } - - // 计算色彩和谐度 - let color_harmony_score = calculate_color_harmony(&combination); - - // 计算风格一致性 - let style_consistency_score = calculate_style_consistency(&combination); - - // 计算综合评分 - let overall_score = (color_harmony_score * 0.4 + style_consistency_score * 0.6).clamp(0.0, 1.0); - - // 过滤低分组合 - if overall_score < request.min_score_threshold { - return None; - } - - // 生成风格描述 - let style_description = generate_style_description(&combination); - - // 生成场合标签 - let occasion_tags = generate_occasion_tags(&combination); - - // 生成季节标签 - let season_tags = generate_season_tags(&combination); - - // 应用筛选条件 - if let Some(occasion_filter) = &request.occasion_filter { - if !occasion_tags.iter().any(|tag| tag.contains(occasion_filter)) { - return None; - } - } - - if let Some(season_filter) = &request.season_filter { - if !season_tags.iter().any(|tag| tag.contains(season_filter)) { - return None; - } - } - - if let Some(style_filter) = &request.style_filter { - if !style_description.to_lowercase().contains(&style_filter.to_lowercase()) { - return None; - } - } - - // 转换为推荐格式 - let recommendation_items: Vec = combination.iter().map(|item| { - RecommendationItem { - id: item.id.clone(), - name: item.name.clone(), - category: item.category.to_chinese().to_string(), - brand: item.brand.clone(), - image_urls: item.image_urls.clone(), - color_primary: item.color_primary.clone(), - styles: item.styles.iter().map(|s| s.to_string()).collect(), - tags: item.tags.clone(), - } - }).collect(); - - Some(OutfitRecommendation { - id: uuid::Uuid::new_v4().to_string(), - items: recommendation_items, - score: overall_score, - style_description, - occasion_tags, - season_tags, - color_harmony_score, - style_consistency_score, - created_at: chrono::Utc::now().to_rfc3339(), - }) -} - -/// 计算色彩和谐度 -fn calculate_color_harmony(combination: &[OutfitItem]) -> f64 { - if combination.len() < 2 { - return 0.5; - } - - let mut harmony_scores = Vec::new(); - - // 计算每对颜色之间的和谐度 - for i in 0..combination.len() { - for j in (i + 1)..combination.len() { - let color1 = &combination[i].color_primary; - let color2 = &combination[j].color_primary; - - // 计算色相差异 - let hue_diff = (color1.hue - color2.hue).abs(); - let hue_diff = hue_diff.min(1.0 - hue_diff); // 处理环形色相 - - // 计算饱和度和明度的相似性 - let sat_similarity = 1.0 - (color1.saturation - color2.saturation).abs(); - let val_similarity = 1.0 - (color1.value - color2.value).abs(); - - // 和谐度评分规则 - let harmony_score = if hue_diff < 0.05 { - // 同色系:高和谐度 - 0.9 * sat_similarity * val_similarity - } else if hue_diff < 0.15 { - // 邻近色:中高和谐度 - 0.8 * sat_similarity * val_similarity - } else if (hue_diff - 0.5).abs() < 0.1 { - // 互补色:中等和谐度 - 0.7 * sat_similarity - } else if (hue_diff - 0.33).abs() < 0.1 || (hue_diff - 0.67).abs() < 0.1 { - // 三角色:中等和谐度 - 0.6 * sat_similarity - } else { - // 其他:较低和谐度 - 0.4 * sat_similarity * val_similarity - }; - - harmony_scores.push(harmony_score); - } - } - - // 返回平均和谐度 - if harmony_scores.is_empty() { - 0.5 - } else { - harmony_scores.iter().sum::() / harmony_scores.len() as f64 - } -} - -/// 计算风格一致性 -fn calculate_style_consistency(combination: &[OutfitItem]) -> f64 { - if combination.len() < 2 { - return 0.5; - } - - // 收集所有风格 - let mut all_styles = std::collections::HashSet::new(); - let mut style_counts = std::collections::HashMap::new(); - - for item in combination { - for style in &item.styles { - let style_str = style.to_string(); - all_styles.insert(style_str.clone()); - *style_counts.entry(style_str).or_insert(0) += 1; - } - } - - if all_styles.is_empty() { - return 0.5; - } - - // 计算风格重叠度 - let total_items = combination.len(); - let mut consistency_score = 0.0; - - for (_, count) in style_counts { - let overlap_ratio = count as f64 / total_items as f64; - consistency_score += overlap_ratio * overlap_ratio; // 平方加权 - } - - // 归一化到0-1范围 - consistency_score.clamp(0.0, 1.0) -} - -/// 生成风格描述 -fn generate_style_description(combination: &[OutfitItem]) -> String { - let mut style_counts = std::collections::HashMap::new(); - - for item in combination { - for style in &item.styles { - let style_str = style.to_string(); - *style_counts.entry(style_str).or_insert(0) += 1; - } - } - - if style_counts.is_empty() { - return "混搭风格".to_string(); - } - - // 找到最主要的风格 - let dominant_style = style_counts.iter() - .max_by_key(|(_, count)| *count) - .map(|(style, _)| style.clone()) - .unwrap_or_else(|| "混搭".to_string()); - - // 根据组合特点生成描述 - let categories: Vec = combination.iter() - .map(|item| item.category.to_chinese().to_string()) - .collect(); - - if categories.contains(&"连衣裙".to_string()) { - format!("{}连衣裙搭配", dominant_style) - } else if categories.contains(&"外套".to_string()) { - format!("{}外套搭配", dominant_style) - } else { - format!("{}搭配", dominant_style) - } -} - -/// 生成场合标签 -fn generate_occasion_tags(combination: &[OutfitItem]) -> Vec { - let mut tags = Vec::new(); - - // 根据风格推断场合 - let styles: Vec = combination.iter() - .flat_map(|item| item.styles.iter().map(|s| s.to_string())) - .collect(); - - if styles.iter().any(|s| s.contains("正式") || s.contains("商务")) { - tags.push("工作".to_string()); - tags.push("正式".to_string()); - } - - if styles.iter().any(|s| s.contains("休闲") || s.contains("街头")) { - tags.push("休闲".to_string()); - tags.push("日常".to_string()); - } - - if styles.iter().any(|s| s.contains("运动")) { - tags.push("运动".to_string()); - tags.push("健身".to_string()); - } - - if styles.iter().any(|s| s.contains("优雅") || s.contains("时尚")) { - tags.push("约会".to_string()); - tags.push("聚会".to_string()); - } - - // 如果没有明确的场合,添加通用标签 - if tags.is_empty() { - tags.push("日常".to_string()); - tags.push("休闲".to_string()); - } - - tags -} - -/// 生成季节标签 -fn generate_season_tags(combination: &[OutfitItem]) -> Vec { - let mut tags = Vec::new(); - - // 根据类别和材质推断季节 - let categories: Vec = combination.iter() - .map(|item| item.category.to_chinese().to_string()) - .collect(); - - let has_outerwear = categories.contains(&"外套".to_string()); - let has_dress = categories.contains(&"连衣裙".to_string()); - - // 简单的季节推断逻辑 - if has_outerwear { - tags.push("秋季".to_string()); - tags.push("冬季".to_string()); - } else if has_dress { - tags.push("春季".to_string()); - tags.push("夏季".to_string()); - } else { - // 通用搭配,适合多个季节 - tags.push("春季".to_string()); - tags.push("秋季".to_string()); - } - - tags -} diff --git a/apps/desktop/src-tauri/tests/outfit_integration_test.rs b/apps/desktop/src-tauri/tests/outfit_integration_test.rs deleted file mode 100644 index ad7a60f..0000000 --- a/apps/desktop/src-tauri/tests/outfit_integration_test.rs +++ /dev/null @@ -1,296 +0,0 @@ -/** - * 服装搭配功能集成测试 - * 测试从图像分析到搭配推荐的完整流程 - */ - -#[cfg(test)] -mod outfit_integration_tests { - use std::sync::Arc; - use chrono::Utc; - - use crate::data::models::outfit_analysis::{ - OutfitAnalysis, CreateOutfitAnalysisRequest, AnalysisStatus, ColorHSV - }; - use crate::data::models::outfit_item::{ - OutfitItem, CreateOutfitItemRequest, OutfitCategory, OutfitStyle - }; - use crate::data::models::outfit_matching::{ - OutfitMatching, CreateOutfitMatchingRequest, MatchingType - }; - use crate::data::repositories::outfit_analysis_repository::OutfitAnalysisRepository; - use crate::data::repositories::outfit_item_repository::OutfitItemRepository; - use crate::data::repositories::outfit_matching_repository::OutfitMatchingRepository; - use crate::business::services::outfit_analysis_service::OutfitAnalysisService; - use crate::business::services::outfit_item_service::OutfitItemService; - use crate::business::services::outfit_matching_service::OutfitMatchingService; - use crate::infrastructure::database::Database; - use crate::infrastructure::gemini_service::GeminiService; - - /// 测试完整的服装分析流程 - #[tokio::test] - async fn test_complete_outfit_analysis_workflow() { - // 创建内存数据库 - let database = Arc::new(Database::new_in_memory().unwrap()); - - // 创建仓库 - let analysis_repo = Arc::new(OutfitAnalysisRepository::new(database.clone()).unwrap()); - let item_repo = Arc::new(OutfitItemRepository::new(database.clone()).unwrap()); - let matching_repo = Arc::new(OutfitMatchingRepository::new(database.clone()).unwrap()); - - // 创建服务(注意:在实际测试中,应该使用模拟的GeminiService) - let gemini_service = Arc::new(GeminiService::new().unwrap()); - let analysis_service = OutfitAnalysisService::new(analysis_repo.clone(), gemini_service); - let item_service = OutfitItemService::new(item_repo.clone()); - let matching_service = OutfitMatchingService::new(matching_repo.clone(), item_repo.clone()); - - // 1. 创建分析记录 - let analysis_request = CreateOutfitAnalysisRequest { - project_id: "test_project".to_string(), - image_path: "/test/image.jpg".to_string(), - image_name: "test_image.jpg".to_string(), - }; - - let analysis = analysis_service.create_analysis(analysis_request).await.unwrap(); - assert_eq!(analysis.analysis_status, AnalysisStatus::Pending); - - // 2. 模拟分析完成,手动创建服装单品 - let item1_request = CreateOutfitItemRequest { - project_id: "test_project".to_string(), - analysis_id: Some(analysis.id.clone()), - name: "红色T恤".to_string(), - category: OutfitCategory::Top, - brand: Some("测试品牌".to_string()), - model: None, - color_primary: ColorHSV::new(0.0, 0.8, 0.9), // 红色 - color_secondary: None, - styles: vec![OutfitStyle::Casual], - design_elements: vec!["纯色".to_string()], - size: None, - material: None, - price: Some(99.0), - purchase_date: None, - image_urls: vec![], - tags: vec!["夏季".to_string(), "休闲".to_string()], - notes: Some("舒适的棉质T恤".to_string()), - }; - - let item1 = item_service.create_item(item1_request).await.unwrap(); - assert_eq!(item1.category, OutfitCategory::Top); - - let item2_request = CreateOutfitItemRequest { - project_id: "test_project".to_string(), - analysis_id: Some(analysis.id.clone()), - name: "蓝色牛仔裤".to_string(), - category: OutfitCategory::Bottom, - brand: Some("测试品牌".to_string()), - model: None, - color_primary: ColorHSV::new(0.6, 0.7, 0.8), // 蓝色 - color_secondary: None, - styles: vec![OutfitStyle::Casual], - design_elements: vec!["水洗".to_string()], - size: None, - material: None, - price: Some(199.0), - purchase_date: None, - image_urls: vec![], - tags: vec!["四季".to_string(), "百搭".to_string()], - notes: Some("经典款牛仔裤".to_string()), - }; - - let item2 = item_service.create_item(item2_request).await.unwrap(); - assert_eq!(item2.category, OutfitCategory::Bottom); - - // 3. 创建搭配 - let matching_request = CreateOutfitMatchingRequest { - project_id: "test_project".to_string(), - matching_name: "休闲夏日搭配".to_string(), - matching_type: MatchingType::StyleConsistent, - item_ids: vec![item1.id.clone(), item2.id.clone()], - occasion_tags: vec!["日常".to_string(), "休闲".to_string()], - season_tags: vec!["夏季".to_string()], - style_description: Some("轻松休闲的夏日搭配".to_string()), - }; - - let matching = matching_service.create_matching(matching_request).await.unwrap(); - assert_eq!(matching.matching_type, MatchingType::StyleConsistent); - assert_eq!(matching.items.len(), 2); - - // 4. 验证搭配评分 - assert!(matching.score_details.overall_score >= 0.0); - assert!(matching.score_details.overall_score <= 1.0); - - // 5. 测试搭配推荐 - let recommended_items = item_service.recommend_matching_items( - "test_project", - &item1.id, - Some(3) - ).await.unwrap(); - - // 应该推荐item2,因为它与item1风格匹配且类别互补 - assert!(recommended_items.iter().any(|item| item.id == item2.id)); - - println!("✅ 服装分析完整流程测试通过"); - } - - /// 测试颜色匹配功能 - #[tokio::test] - async fn test_color_matching_functionality() { - let database = Arc::new(Database::new_in_memory().unwrap()); - let item_repo = Arc::new(OutfitItemRepository::new(database.clone()).unwrap()); - let item_service = OutfitItemService::new(item_repo.clone()); - - // 创建不同颜色的服装单品 - let red_item = CreateOutfitItemRequest { - project_id: "test_project".to_string(), - analysis_id: None, - name: "红色上衣".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, - }; - - let blue_item = CreateOutfitItemRequest { - project_id: "test_project".to_string(), - analysis_id: None, - name: "蓝色上衣".to_string(), - category: OutfitCategory::Top, - brand: None, - model: None, - color_primary: ColorHSV::new(0.6, 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, - }; - - let similar_red_item = CreateOutfitItemRequest { - project_id: "test_project".to_string(), - analysis_id: None, - name: "深红色上衣".to_string(), - category: OutfitCategory::Top, - 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, - }; - - // 创建服装单品 - let red = item_service.create_item(red_item).await.unwrap(); - let blue = item_service.create_item(blue_item).await.unwrap(); - let similar_red = item_service.create_item(similar_red_item).await.unwrap(); - - // 测试颜色搜索 - let target_color = ColorHSV::new(0.0, 0.8, 0.9); // 红色 - let matching_items = item_service.search_by_color( - "test_project", - &target_color, - 0.7 // 70% 相似度阈值 - ).await.unwrap(); - - // 应该找到红色和相似红色的单品,但不包括蓝色 - assert!(matching_items.iter().any(|item| item.id == red.id)); - assert!(matching_items.iter().any(|item| item.id == similar_red.id)); - assert!(!matching_items.iter().any(|item| item.id == blue.id)); - - println!("✅ 颜色匹配功能测试通过"); - } - - /// 测试搭配评分算法 - #[tokio::test] - async fn test_outfit_scoring_algorithm() { - let database = Arc::new(Database::new_in_memory().unwrap()); - let matching_repo = Arc::new(OutfitMatchingRepository::new(database.clone()).unwrap()); - let item_repo = Arc::new(OutfitItemRepository::new(database.clone()).unwrap()); - let matching_service = OutfitMatchingService::new(matching_repo, item_repo); - - // 创建测试用的服装单品 - 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.0, 1.0), // 白色 - color_secondary: None, - styles: vec![OutfitStyle::Casual, OutfitStyle::Minimalist], - design_elements: vec![], - size: None, - material: None, - price: None, - purchase_date: None, - image_urls: vec![], - tags: vec![], - notes: None, - created_at: Utc::now(), - updated_at: 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.0, 0.0, 0.0), // 黑色 - color_secondary: None, - styles: vec![OutfitStyle::Casual, OutfitStyle::Minimalist], - design_elements: vec![], - size: None, - material: None, - price: None, - purchase_date: None, - image_urls: vec![], - tags: vec![], - notes: None, - created_at: Utc::now(), - updated_at: Utc::now(), - }, - ]; - - // 测试评分计算 - let (score_details, suggestions) = matching_service.calculate_matching_score_and_suggestions(&items); - - // 验证评分范围 - assert!(score_details.color_harmony_score >= 0.0 && score_details.color_harmony_score <= 1.0); - assert!(score_details.style_consistency_score >= 0.0 && score_details.style_consistency_score <= 1.0); - assert!(score_details.overall_score >= 0.0 && score_details.overall_score <= 1.0); - - // 黑白搭配应该有较高的风格一致性(都是极简风格) - assert!(score_details.style_consistency_score > 0.5); - - println!("✅ 搭配评分算法测试通过"); - println!(" 颜色和谐度: {:.2}", score_details.color_harmony_score); - println!(" 风格一致性: {:.2}", score_details.style_consistency_score); - println!(" 综合评分: {:.2}", score_details.overall_score); - } -} diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index c35d55b..9074617 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -9,7 +9,6 @@ import AiClassificationSettings from './pages/AiClassificationSettings'; import TemplateManagement from './pages/TemplateManagement'; import { MaterialModelBinding } from './pages/MaterialModelBinding'; import Tools from './pages/Tools'; -import OutfitMatch from './pages/OutfitMatch'; import Navigation from './components/Navigation'; import { NotificationSystem, useNotifications } from './components/NotificationSystem'; import { useProjectStore } from './store/projectStore'; @@ -81,7 +80,6 @@ function App() { } /> } /> } /> - } /> diff --git a/apps/desktop/src/components/outfit/ImageUploader.tsx b/apps/desktop/src/components/outfit/ImageUploader.tsx deleted file mode 100644 index db5cb8d..0000000 --- a/apps/desktop/src/components/outfit/ImageUploader.tsx +++ /dev/null @@ -1,340 +0,0 @@ -import React, { useState, useCallback, useRef } from 'react'; -import { - PhotoIcon, - CloudArrowUpIcon, - XMarkIcon, - EyeIcon -} from '@heroicons/react/24/outline'; -import { useNotifications } from '../NotificationSystem'; - -interface ImageFile { - file: File; - preview: string; - id: string; -} - -interface ImageUploaderProps { - onUpload: (files: File[]) => Promise; - isUploading?: boolean; - maxFiles?: number; - maxFileSize?: number; // in MB - acceptedFormats?: string[]; - className?: string; -} - -const ImageUploader: React.FC = ({ - onUpload, - isUploading = false, - maxFiles = 5, - maxFileSize = 10, - acceptedFormats = ['image/jpeg', 'image/png', 'image/webp'], - className = '' -}) => { - const [selectedFiles, setSelectedFiles] = useState([]); - const [isDragOver, setIsDragOver] = useState(false); - const [previewImage, setPreviewImage] = useState(null); - const fileInputRef = useRef(null); - const { addNotification } = useNotifications(); - - // 验证文件 - const validateFile = useCallback((file: File): string | null => { - if (!acceptedFormats.includes(file.type)) { - return `不支持的文件类型。请选择 ${acceptedFormats.join(', ')} 格式的图片。`; - } - - if (file.size > maxFileSize * 1024 * 1024) { - return `文件大小超过限制。请选择小于 ${maxFileSize}MB 的图片。`; - } - - return null; - }, [acceptedFormats, maxFileSize]); - - // 处理文件选择 - const handleFiles = useCallback((files: FileList) => { - const newFiles: ImageFile[] = []; - const errors: string[] = []; - - Array.from(files).forEach((file) => { - const error = validateFile(file); - if (error) { - errors.push(`${file.name}: ${error}`); - return; - } - - if (selectedFiles.length + newFiles.length >= maxFiles) { - errors.push(`最多只能选择 ${maxFiles} 个文件`); - return; - } - - const id = Math.random().toString(36).substr(2, 9); - const preview = URL.createObjectURL(file); - newFiles.push({ file, preview, id }); - }); - - if (errors.length > 0) { - addNotification({ - type: 'error', - title: '文件验证失败', - message: errors.join('\n') - }); - } - - if (newFiles.length > 0) { - setSelectedFiles(prev => [...prev, ...newFiles]); - } - }, [selectedFiles.length, maxFiles, validateFile, addNotification]); - - // 拖拽处理 - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(true); - }, []); - - const handleDragLeave = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(false); - }, []); - - const handleDrop = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(false); - - const files = e.dataTransfer.files; - if (files.length > 0) { - handleFiles(files); - } - }, [handleFiles]); - - // 文件选择 - const handleFileSelect = useCallback((e: React.ChangeEvent) => { - const files = e.target.files; - if (files && files.length > 0) { - handleFiles(files); - } - // 清空input值,允许重复选择同一文件 - e.target.value = ''; - }, [handleFiles]); - - // 移除文件 - const removeFile = useCallback((id: string) => { - setSelectedFiles(prev => { - const updated = prev.filter(f => f.id !== id); - // 清理预览URL - const removed = prev.find(f => f.id === id); - if (removed) { - URL.revokeObjectURL(removed.preview); - } - return updated; - }); - }, []); - - // 清空所有文件 - const clearAll = useCallback(() => { - selectedFiles.forEach(f => URL.revokeObjectURL(f.preview)); - setSelectedFiles([]); - }, [selectedFiles]); - - // 上传文件 - const handleUpload = useCallback(async () => { - if (selectedFiles.length === 0) { - addNotification({ - type: 'warning', - title: '请选择文件', - message: '请先选择要上传的图片文件' - }); - return; - } - - try { - const files = selectedFiles.map(f => f.file); - await onUpload(files); - - addNotification({ - type: 'success', - title: '上传成功', - message: `成功上传 ${files.length} 个文件` - }); - - clearAll(); - } catch (error) { - addNotification({ - type: 'error', - title: '上传失败', - message: error instanceof Error ? error.message : '上传过程中发生错误' - }); - } - }, [selectedFiles, onUpload, addNotification, clearAll]); - - // 预览图片 - const showPreview = useCallback((preview: string) => { - setPreviewImage(preview); - }, []); - - // 清理内存 - React.useEffect(() => { - return () => { - selectedFiles.forEach(f => URL.revokeObjectURL(f.preview)); - }; - }, []); - - return ( -
- {/* 拖拽上传区域 */} -
- - -
-
- -
- -
-

- 上传服装图片 -

-

- 拖拽图片到此处,或点击选择文件 -

- - -
- -
- 支持 {acceptedFormats.map(type => type.split('/')[1].toUpperCase()).join(', ')} 格式, - 最大 {maxFileSize}MB,最多 {maxFiles} 个文件 -
-
-
- - {/* 已选择的文件列表 */} - {selectedFiles.length > 0 && ( -
-
-

- 已选择 {selectedFiles.length} 个文件 -

- -
- -
- {selectedFiles.map((imageFile) => ( -
-
- {imageFile.file.name} -
- -
-
- - -
-
- -
-

- {imageFile.file.name} -

-

- {(imageFile.file.size / 1024 / 1024).toFixed(2)} MB -

-
-
- ))} -
- -
- -
-
- )} - - {/* 图片预览模态框 */} - {previewImage && ( -
-
- - 预览 -
-
- )} -
- ); -}; - -export default ImageUploader; \ No newline at end of file diff --git a/apps/desktop/src/components/outfit/MultiSelectExample.tsx b/apps/desktop/src/components/outfit/MultiSelectExample.tsx deleted file mode 100644 index 762a3fb..0000000 --- a/apps/desktop/src/components/outfit/MultiSelectExample.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React, { useState } from 'react'; -import { CustomSelect, CustomMultiSelect } from '../CustomSelect'; - -// 示例:如何使用单选和多选组件 -const MultiSelectExample: React.FC = () => { - const [singleValue, setSingleValue] = useState(''); - const [multiValue, setMultiValue] = useState([]); - const [searchableMultiValue, setSearchableMultiValue] = useState([]); - - // 示例选项 - const categoryOptions = [ - { value: 'top', label: '上装' }, - { value: 'bottom', label: '下装' }, - { value: 'dress', label: '连衣裙' }, - { value: 'outerwear', label: '外套' }, - { value: 'footwear', label: '鞋类' }, - { value: 'accessory', label: '配饰' }, - { value: 'other', label: '其他' } - ]; - - const styleOptions = [ - { value: 'casual', label: '休闲风格' }, - { value: 'formal', label: '正式风格' }, - { value: 'business', label: '商务风格' }, - { value: 'street', label: '街头风格' }, - { value: 'elegant', label: '优雅风格' }, - { value: 'sporty', label: '运动风格' }, - { value: 'vintage', label: '复古风格' }, - { value: 'minimalist', label: '简约风格' }, - { value: 'bohemian', label: '波西米亚风格' }, - { value: 'gothic', label: '哥特风格' } - ]; - - return ( -
-
-

- CustomSelect 组件使用示例 -

- -
- {/* 单选组件示例 */} -
-

单选组件 (CustomSelect)

- -
- - -

- 当前选择: {singleValue || '未选择'} -

-
-
- - {/* 多选组件示例 */} -
-

多选组件 (CustomMultiSelect)

- -
- - -

- 当前选择: {multiValue.length > 0 ? multiValue.join(', ') : '未选择'} -

-
-
-
- - {/* 带搜索的多选组件示例 */} -
-

带搜索的多选组件

- -
- - -

- 当前选择: {searchableMultiValue.length > 0 ? searchableMultiValue.join(', ') : '未选择'} -

-
-
- - {/* 使用说明 */} -
-

使用说明

-
-
- CustomSelect (单选): -
    -
  • 传统的下拉选择框,只能选择一个选项
  • -
  • 使用 value (string) 和 onChange ((value: string) => void)
  • -
-
-
- CustomMultiSelect (多选): -
    -
  • 支持选择多个选项,以标签形式显示
  • -
  • 使用 value (string[]) 和 onChange ((value: string[]) => void)
  • -
  • 支持搜索功能 (searchable=true)
  • -
  • 支持全选/取消全选
  • -
  • 可设置最大显示标签数 (maxDisplayItems)
  • -
  • 点击标签上的 × 可以移除单个选项
  • -
  • 点击右侧的 × 可以清空所有选择
  • -
-
-
-
- - {/* 代码示例 */} -
-

代码示例

-
-{`// 单选组件
-
-
-// 多选组件
-`}
-          
-
-
-
- ); -}; - -export default MultiSelectExample; diff --git a/apps/desktop/src/components/outfit/OutfitAnalysisResult.tsx b/apps/desktop/src/components/outfit/OutfitAnalysisResult.tsx deleted file mode 100644 index 826eaf2..0000000 --- a/apps/desktop/src/components/outfit/OutfitAnalysisResult.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { - SparklesIcon, - EyeIcon, - ClockIcon, - CheckCircleIcon, - ExclamationTriangleIcon, - ArrowPathIcon -} from '@heroicons/react/24/outline'; -import { invoke } from '@tauri-apps/api/core'; -import { useNotifications } from '../NotificationSystem'; -import { PROJECT_ID } from './const'; - -interface OutfitAnalysis { - id: string; - project_id: string; - image_path: string; - image_name: string; - analysis_status: 'Pending' | 'Processing' | 'Completed' | 'Failed'; - analysis_result?: any; - error_message?: string; - created_at: string; - updated_at: string; - analyzed_at?: string; -} - -interface OutfitAnalysisResultProps { - onCreateItems?: (analysisId: string) => void; -} - -const OutfitAnalysisResult: React.FC = ({ - onCreateItems -}) => { - const [analyses, setAnalyses] = useState([]); - const [loading, setLoading] = useState(true); - const [selectedAnalysis, setSelectedAnalysis] = useState(null); - const { addNotification } = useNotifications(); - - // 加载分析结果 - const loadAnalyses = async () => { - try { - setLoading(true); - const options = { - project_id: PROJECT_ID, - status: null, - limit: 50, - offset: 0 - }; - - const result = await invoke('list_outfit_analyses', { options }); - setAnalyses(result as OutfitAnalysis[]); - } catch (error) { - console.error('加载分析结果失败:', error); - addNotification({ - type: 'error', - title: '加载失败', - message: '无法加载分析结果' - }); - } finally { - setLoading(false); - } - }; - - // 创建服装单品 - const handleCreateItems = async (analysisId: string) => { - try { - await invoke('create_outfit_items_from_analysis', { - projectId: PROJECT_ID, - analysisId - }); - - addNotification({ - type: 'success', - title: '创建成功', - message: '成功从分析结果创建服装单品' - }); - - if (onCreateItems) { - onCreateItems(analysisId); - } - } catch (error) { - console.error('创建服装单品失败:', error); - addNotification({ - type: 'error', - title: '创建失败', - message: error instanceof Error ? error.message : '创建服装单品失败' - }); - } - }; - - // 获取状态图标 - const getStatusIcon = (status: string) => { - switch (status) { - case 'Pending': - return ; - case 'Processing': - return ; - case 'Completed': - return ; - case 'Failed': - return ; - default: - return ; - } - }; - - // 获取状态文本 - const getStatusText = (status: string) => { - switch (status) { - case 'Pending': - return '等待分析'; - case 'Processing': - return '分析中...'; - case 'Completed': - return '分析完成'; - case 'Failed': - return '分析失败'; - default: - return '未知状态'; - } - }; - - // 格式化时间 - const formatTime = (timeString: string) => { - return new Date(timeString).toLocaleString('zh-CN'); - }; - - useEffect(() => { - loadAnalyses(); - - // 设置定时刷新,检查分析状态 - const interval = setInterval(() => { - loadAnalyses(); - }, 5000); // 每5秒刷新一次 - - return () => clearInterval(interval); - }, []); - - if (loading) { - return ( -
-
- 加载中... -
- ); - } - - if (analyses.length === 0) { - return ( -
- -

暂无分析结果

-

请先上传图片进行分析

-
- ); - } - - return ( -
-
- {analyses.map((analysis) => ( -
- {/* 图片预览 */} -
- {analysis.image_path && ( - {analysis.image_name} { - (e.target as HTMLImageElement).style.display = 'none'; - }} - /> - )} -
- -
-
- - {/* 分析信息 */} -
-
-

- {analysis.image_name} -

-
- {getStatusIcon(analysis.analysis_status)} - - {getStatusText(analysis.analysis_status)} - -
-
- -

- 创建时间: {formatTime(analysis.created_at)} -

- - {/* 分析结果 */} - {analysis.analysis_status === 'Completed' && analysis.analysis_result && ( -
-
- 检测到: - - {analysis.analysis_result.products?.length || 0} 个服装单品 - -
- {analysis.analysis_result.style_description && ( -
- 风格: - - {analysis.analysis_result.style_description} - -
- )} -
- )} - - {/* 错误信息 */} - {analysis.analysis_status === 'Failed' && analysis.error_message && ( -
-

- {analysis.error_message} -

-
- )} - - {/* 操作按钮 */} -
- {analysis.analysis_status === 'Completed' && ( - - )} - -
-
-
- ))} -
- - {/* 详情模态框 */} - {selectedAnalysis && ( -
-
-
- -
-
-
-
-

- 分析详情 -

- -
-
- 文件名: - {selectedAnalysis.image_name} -
- -
- 状态: - {getStatusText(selectedAnalysis.analysis_status)} -
- -
- 创建时间: - {formatTime(selectedAnalysis.created_at)} -
- - {selectedAnalysis.analyzed_at && ( -
- 分析时间: - {formatTime(selectedAnalysis.analyzed_at)} -
- )} - - {selectedAnalysis.analysis_result && ( -
- 分析结果: -
-                            {JSON.stringify(selectedAnalysis.analysis_result, null, 2)}
-                          
-
- )} - - {selectedAnalysis.error_message && ( -
- 错误信息: -

{selectedAnalysis.error_message}

-
- )} -
-
-
-
- -
- -
-
-
-
- )} -
- ); -}; - -export default OutfitAnalysisResult; \ No newline at end of file diff --git a/apps/desktop/src/components/outfit/OutfitCard.tsx b/apps/desktop/src/components/outfit/OutfitCard.tsx deleted file mode 100644 index b7c01bf..0000000 --- a/apps/desktop/src/components/outfit/OutfitCard.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import React from 'react'; -import { - HeartIcon, - EyeIcon, - TrashIcon, - StarIcon, - ClockIcon, - SparklesIcon -} from '@heroicons/react/24/outline'; -import { HeartIcon as HeartSolidIcon } from '@heroicons/react/24/solid'; -import { format } from 'date-fns'; -import { zhCN } from 'date-fns/locale'; - -interface OutfitMatching { - id: string; - project_id: string; - matching_name: string; - matching_type: string; - items: Array<{ - item_id: string; - item_name: string; - category: string; - color_primary: any; - styles: string[]; - role_in_outfit: string; - image_url?: string; - }>; - score_details: { - color_harmony_score: number; - style_consistency_score: number; - proportion_score: number; - occasion_appropriateness: number; - trend_factor: number; - overall_score: number; - }; - suggestions: Array<{ - suggestion_type: string; - description: string; - priority: number; - }>; - occasion_tags: string[]; - season_tags: string[]; - style_description: string; - color_palette: any[]; - is_favorite: boolean; - wear_count: number; - last_worn_date?: string; - created_at: string; - updated_at: string; -} - -interface OutfitCardProps { - matching: OutfitMatching; - onToggleFavorite: () => void; - onDelete: () => void; - onView: () => void; -} - -const OutfitCard: React.FC = ({ - matching, - onToggleFavorite, - onDelete, - onView -}) => { - const formatColorHSV = (color: any) => { - if (!color) return '#000000'; - try { - const h = color.hue * 360; - const s = color.saturation * 100; - const v = color.value * 100; - return `hsl(${h}, ${s}%, ${v}%)`; - } catch { - return '#000000'; - } - }; - - const getScoreColor = (score: number) => { - if (score >= 0.8) return 'text-green-600 bg-green-100'; - if (score >= 0.6) return 'text-yellow-600 bg-yellow-100'; - return 'text-red-600 bg-red-100'; - }; - - const getMatchingTypeLabel = (type: string) => { - const typeMap: { [key: string]: string } = { - 'ColorHarmony': '颜色和谐', - 'StyleConsistent': '风格一致', - 'Complementary': '互补搭配', - 'Seasonal': '季节搭配', - 'Occasion': '场合搭配', - 'Trendy': '时尚搭配' - }; - return typeMap[type] || type; - }; - - const formatDate = (dateString: string) => { - try { - return format(new Date(dateString), 'MM-dd', { locale: zhCN }); - } catch { - return dateString; - } - }; - - const getCategoryIcon = (_category: string) => { - // 这里可以根据类别返回不同的图标 - return '👕'; // 简化处理,实际应用中可以使用更具体的图标 - }; - - return ( -
- {/* 头部 */} -
-
-
-

- {matching.matching_name} -

-

- {matching.style_description} -

-
- -
- - - - - -
-
- - {/* 搭配类型和评分 */} -
- - {getMatchingTypeLabel(matching.matching_type)} - - -
- - {(matching.score_details.overall_score * 100).toFixed(0)}分 -
-
-
- - {/* 服装单品预览 */} -
-

- 搭配单品 ({matching.items.length}) -

- -
- {matching.items.slice(0, 4).map((item) => ( -
-
-
- {getCategoryIcon(item.category)} -
-
-
-

- {item.item_name} -

-

- {item.role_in_outfit} -

-
-
- ))} - - {matching.items.length > 4 && ( -
- - +{matching.items.length - 4} 更多 - -
- )} -
-
- - {/* 色彩搭配方案 */} - {matching.color_palette.length > 0 && ( -
-

色彩方案

-
- {matching.color_palette.slice(0, 6).map((color, index) => ( -
- ))} - {matching.color_palette.length > 6 && ( -
- + -
- )} -
-
- )} - - {/* 标签 */} -
-
- {matching.occasion_tags.slice(0, 2).map((tag, index) => ( - - {tag} - - ))} - {matching.season_tags.slice(0, 2).map((tag, index) => ( - - {tag} - - ))} -
-
- - {/* 底部信息 */} -
-
-
- - - {formatDate(matching.created_at)} - - - {matching.wear_count > 0 && ( - - - 穿过 {matching.wear_count} 次 - - )} -
- - {matching.last_worn_date && ( - - 最后穿着: {formatDate(matching.last_worn_date)} - - )} -
-
-
- ); -}; - -export default OutfitCard; diff --git a/apps/desktop/src/components/outfit/OutfitItemCard.tsx b/apps/desktop/src/components/outfit/OutfitItemCard.tsx deleted file mode 100644 index cc13d27..0000000 --- a/apps/desktop/src/components/outfit/OutfitItemCard.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import React from 'react'; -import { - PhotoIcon, - TagIcon, - SparklesIcon -} from '@heroicons/react/24/outline'; -import { CheckCircleIcon as CheckCircleSolidIcon } from '@heroicons/react/24/solid'; - -interface OutfitItem { - id: string; - project_id: string; - analysis_id?: string; - name: string; - category: string; - brand?: string; - color_primary: any; - styles: string[]; - image_urls: string[]; - tags: string[]; - notes?: string; - created_at: string; - updated_at: string; -} - -interface OutfitItemCardProps { - item: OutfitItem; - isSelected: boolean; - onSelect: (selected: boolean) => void; -} - -const OutfitItemCard: React.FC = ({ - item, - isSelected, - onSelect -}) => { - const formatColorHSV = (color: any) => { - if (!color) return '#000000'; - try { - const h = color.hue * 360; - const s = color.saturation * 100; - const v = color.value * 100; - return `hsl(${h}, ${s}%, ${v}%)`; - } catch { - return '#000000'; - } - }; - - const getCategoryLabel = (category: string) => { - const categoryMap: { [key: string]: string } = { - 'Top': '上装', - 'Bottom': '下装', - 'Dress': '连衣裙', - 'Outerwear': '外套', - 'Footwear': '鞋类', - 'Accessory': '配饰', - 'Other': '其他' - }; - return categoryMap[category] || category; - }; - - const getStyleLabel = (style: string) => { - const styleMap: { [key: string]: string } = { - 'Casual': '休闲', - 'Formal': '正式', - 'Business': '商务', - 'Sporty': '运动', - 'Vintage': '复古', - 'Bohemian': '波西米亚', - 'Minimalist': '极简', - 'Streetwear': '街头', - 'Elegant': '优雅', - 'Trendy': '时尚' - }; - return styleMap[style] || style; - }; - - const getCategoryIcon = (category: string) => { - const iconMap: { [key: string]: string } = { - 'Top': '👕', - 'Bottom': '👖', - 'Dress': '👗', - 'Outerwear': '🧥', - 'Footwear': '👟', - 'Accessory': '👜', - 'Other': '👔' - }; - return iconMap[category] || '👔'; - }; - - const handleCardClick = () => { - onSelect(!isSelected); - }; - - return ( -
- {/* 选择状态指示器 */} -
- {isSelected ? ( - - ) : ( -
- )} -
- - {/* 图片区域 */} -
- {item.image_urls.length > 0 ? ( - {item.name} { - // 图片加载失败时显示默认图标 - e.currentTarget.style.display = 'none'; - e.currentTarget.nextElementSibling?.classList.remove('hidden'); - }} - /> - ) : null} - - {/* 默认图标(当没有图片或图片加载失败时显示) */} -
0 ? 'hidden' : ''}`}> -
-
- {getCategoryIcon(item.category)} -
- -
-
- - {/* 颜色指示器 */} -
-
-
- - {/* 类别标签 */} -
- - {getCategoryLabel(item.category)} - -
-
- - {/* 内容区域 */} -
- {/* 名称和品牌 */} -
-

- {item.name} -

- {item.brand && ( -

- {item.brand} -

- )} -
- - {/* 风格标签 */} - {item.styles.length > 0 && ( -
- {item.styles.slice(0, 2).map((style, index) => ( - - {getStyleLabel(style)} - - ))} - {item.styles.length > 2 && ( - - +{item.styles.length - 2} - - )} -
- )} - - {/* 自定义标签 */} - {item.tags.length > 0 && ( -
- -
- {item.tags.slice(0, 2).map((tag, index) => ( - - {tag} - - ))} - {item.tags.length > 2 && ( - - +{item.tags.length - 2} - - )} -
-
- )} - - {/* 备注 */} - {item.notes && ( -

- {item.notes} -

- )} - - {/* 来源标识 */} - {item.analysis_id && ( -
- - AI分析生成 -
- )} -
- - {/* 选中状态的覆盖层 */} - {isSelected && ( -
- )} -
- ); -}; - -export default OutfitItemCard; diff --git a/apps/desktop/src/components/outfit/OutfitItemForm.tsx b/apps/desktop/src/components/outfit/OutfitItemForm.tsx deleted file mode 100644 index cc5fd5d..0000000 --- a/apps/desktop/src/components/outfit/OutfitItemForm.tsx +++ /dev/null @@ -1,497 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { - XMarkIcon, - PlusIcon, - XCircleIcon -} from '@heroicons/react/24/outline'; -import { invoke } from '@tauri-apps/api/core'; -import { useNotifications } from '../NotificationSystem'; -import { PROJECT_ID } from './const'; - -interface OutfitItem { - id: string; - project_id: string; - analysis_id?: string; - name: string; - category: string; - description?: string; - color_pattern?: any; - design_styles?: string[]; - brand?: string; - size?: string; - price?: number; - purchase_date?: string; - image_path?: string; - tags?: string[]; - notes?: string; - created_at: string; - updated_at: string; -} - -interface OutfitItemFormData { - name: string; - category: string; - description: string; - brand: string; - size: string; - price: string; - purchase_date: string; - design_styles: string[]; - tags: string[]; - notes: string; -} - -interface OutfitItemFormProps { - projectId: string; - item?: OutfitItem; - isOpen: boolean; - onClose: () => void; - onSave: () => void; -} - -const OutfitItemForm: React.FC = ({ - item, - isOpen, - onClose, - onSave -}) => { - const [formData, setFormData] = useState({ - name: '', - category: '', - description: '', - brand: '', - size: '', - price: '', - purchase_date: '', - design_styles: [], - tags: [], - notes: '' - }); - const [loading, setLoading] = useState(false); - const [newStyle, setNewStyle] = useState(''); - const [newTag, setNewTag] = useState(''); - const { addNotification } = useNotifications(); - - // 常用类别选项 - const categoryOptions = [ - '上衣', '下装', '外套', '连衣裙', '鞋子', - '包包', '配饰', '内衣', '运动装', '正装' - ]; - - // 常用尺寸选项 - const sizeOptions = [ - 'XS', 'S', 'M', 'L', 'XL', 'XXL', - '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44' - ]; - - // 初始化表单数据 - useEffect(() => { - if (item) { - setFormData({ - name: item.name || '', - category: item.category || '', - description: item.description || '', - brand: item.brand || '', - size: item.size || '', - price: item.price ? item.price.toString() : '', - purchase_date: item.purchase_date || '', - design_styles: item.design_styles || [], - tags: item.tags || [], - notes: item.notes || '' - }); - } else { - setFormData({ - name: '', - category: '', - description: '', - brand: '', - size: '', - price: '', - purchase_date: '', - design_styles: [], - tags: [], - notes: '' - }); - } - }, [item, isOpen]); - - // 处理表单提交 - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!formData.name.trim()) { - addNotification({ - type: 'error', - title: '验证失败', - message: '请输入服装单品名称' - }); - return; - } - - if (!formData.category.trim()) { - addNotification({ - type: 'error', - title: '验证失败', - message: '请选择服装类别' - }); - return; - } - - setLoading(true); - - try { - const requestData = { - project_id: PROJECT_ID, - analysis_id: item?.analysis_id || null, - name: formData.name.trim(), - category: formData.category.trim(), - description: formData.description.trim() || null, - brand: formData.brand.trim() || null, - size: formData.size.trim() || null, - price: formData.price ? parseFloat(formData.price) : null, - purchase_date: formData.purchase_date || null, - design_styles: formData.design_styles.length > 0 ? formData.design_styles : null, - tags: formData.tags.length > 0 ? formData.tags : null, - notes: formData.notes.trim() || null - }; - - if (item) { - // 更新现有单品 - await invoke('update_outfit_item', { - id: item.id, - request: requestData - }); - - addNotification({ - type: 'success', - title: '更新成功', - message: '服装单品信息已更新' - }); - } else { - // 创建新单品 - await invoke('create_outfit_item', { - request: requestData - }); - - addNotification({ - type: 'success', - title: '创建成功', - message: '服装单品已创建' - }); - } - - onSave(); - onClose(); - } catch (error) { - console.error('保存服装单品失败:', error); - addNotification({ - type: 'error', - title: '保存失败', - message: error instanceof Error ? error.message : '保存服装单品失败' - }); - } finally { - setLoading(false); - } - }; - - // 添加设计风格 - const addDesignStyle = () => { - if (newStyle.trim() && !formData.design_styles.includes(newStyle.trim())) { - setFormData(prev => ({ - ...prev, - design_styles: [...prev.design_styles, newStyle.trim()] - })); - setNewStyle(''); - } - }; - - // 移除设计风格 - const removeDesignStyle = (style: string) => { - setFormData(prev => ({ - ...prev, - design_styles: prev.design_styles.filter(s => s !== style) - })); - }; - - // 添加标签 - const addTag = () => { - if (newTag.trim() && !formData.tags.includes(newTag.trim())) { - setFormData(prev => ({ - ...prev, - tags: [...prev.tags, newTag.trim()] - })); - setNewTag(''); - } - }; - - // 移除标签 - const removeTag = (tag: string) => { - setFormData(prev => ({ - ...prev, - tags: prev.tags.filter(t => t !== tag) - })); - }; - - if (!isOpen) return null; - - return ( -
-
-
- -
-
-
-
-

- {item ? '编辑服装单品' : '添加服装单品'} -

- -
- -
- {/* 基本信息 */} -
-
- - setFormData(prev => ({ ...prev, name: e.target.value }))} - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary-500 focus:border-primary-500" - placeholder="请输入服装名称" - required - /> -
- -
- - -
-
- -
- -