feat: 实现服装搭配功能的图像上传和AI分析

新功能:
- 实现完整的图像上传组件 (ImageUploader)
  - 支持拖拽上传、文件选择、预览和验证
  - 文件大小和格式限制
  - 实时预览和文件管理
- 实现AI图像分析结果展示 (OutfitAnalysisResult)
  - 实时状态更新和进度跟踪
  - 分析结果详情查看
  - 自动刷新机制
- 添加文件保存功能 (save_outfit_image 命令)
- 更新OutfitMatch页面为现代化标签页设计

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

 UI/UX优化:
- 现代化的标签页设计
- 响应式布局和优雅动画
- 统一的设计语言和交互体验
- 完善的加载状态和错误处理
This commit is contained in:
imeepos
2025-07-17 19:05:40 +08:00
parent 7fb1dfa95a
commit 7b1bb2fb0e
19 changed files with 4310 additions and 3 deletions

View File

@@ -166,6 +166,10 @@ pub enum BusinessError {
/// 操作不被允许
#[error("操作不被允许: {0}")]
OperationNotAllowed(String),
/// 状态无效
#[error("状态无效: {0}")]
InvalidState(String),
}
/// 错误结果类型别名
@@ -254,6 +258,9 @@ impl From<BusinessError> for AppError {
BusinessError::OperationNotAllowed(operation) => AppError::Permission {
operation,
},
BusinessError::InvalidState(state) => AppError::Internal {
message: format!("状态无效: {}", state),
},
}
}
}

View File

@@ -22,6 +22,9 @@ 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;

View File

@@ -0,0 +1,314 @@
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<OutfitAnalysisRepository>,
gemini_service: Arc<GeminiService>,
}
impl OutfitAnalysisService {
/// 创建新的服装分析服务实例
pub fn new(repository: Arc<OutfitAnalysisRepository>, gemini_service: Arc<GeminiService>) -> Self {
Self { repository, gemini_service }
}
/// 创建服装分析记录
pub async fn create_analysis(&self, request: CreateOutfitAnalysisRequest) -> Result<OutfitAnalysis> {
// 验证输入数据
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<ImageAnalysisResult> {
// 读取图像文件
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<ImageAnalysisResult> {
// 尝试从响应中提取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<ColorHSV> {
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<OutfitProduct> {
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<Vec<OutfitAnalysis>> {
Ok(self.repository.list(&options)?)
}
/// 根据ID获取分析记录
pub async fn get_analysis_by_id(&self, id: &str) -> Result<Option<OutfitAnalysis>> {
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<OutfitAnalysisStats> {
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格式"));
}
}

View File

@@ -0,0 +1,516 @@
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<OutfitItemRepository>,
}
impl OutfitItemService {
/// 创建新的服装单品服务实例
pub fn new(repository: Arc<OutfitItemRepository>) -> Self {
Self { repository }
}
/// 创建服装单品
pub async fn create_item(&self, request: CreateOutfitItemRequest) -> Result<OutfitItem> {
// 验证输入数据
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<Vec<OutfitItem>> {
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<Vec<OutfitItem>> {
Ok(self.repository.list(&options)?)
}
/// 根据ID获取服装单品
pub async fn get_item_by_id(&self, id: &str) -> Result<Option<OutfitItem>> {
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<Vec<OutfitItem>> {
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<Vec<OutfitItem>> {
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<Vec<OutfitItem>> {
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<u32>
) -> Result<Vec<OutfitItem>> {
// 获取基础单品
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<OutfitItem> = 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<OutfitItem> = 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<OutfitItemStats> {
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());
}
}

View File

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

View File

@@ -10,3 +10,6 @@ pub mod project_template_binding;
pub mod template_matching_result;
pub mod export_record;
pub mod video_generation;
pub mod outfit_analysis;
pub mod outfit_item;
pub mod outfit_matching;

View File

@@ -0,0 +1,206 @@
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
/// HSV颜色模型
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ColorHSV {
pub hue: f64, // 色相 (0.0-1.0)
pub saturation: f64, // 饱和度 (0.0-1.0)
pub value: f64, // 明度 (0.0-1.0)
}
impl ColorHSV {
pub fn new(hue: f64, saturation: f64, value: f64) -> Self {
Self {
hue: hue.clamp(0.0, 1.0),
saturation: saturation.clamp(0.0, 1.0),
value: value.clamp(0.0, 1.0),
}
}
/// 转换为RGB十六进制字符串
pub fn to_rgb_hex(&self) -> String {
let (r, g, b) = hsv_to_rgb(self.hue, self.saturation, self.value);
format!("#{:02x}{:02x}{:02x}",
(r * 255.0) as u8,
(g * 255.0) as u8,
(b * 255.0) as u8)
}
/// 从RGB十六进制字符串创建
pub fn from_rgb_hex(hex: &str) -> Result<Self, String> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return Err("Invalid hex color format".to_string());
}
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| "Invalid red component")?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| "Invalid green component")?;
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| "Invalid blue component")?;
let (h, s, v) = rgb_to_hsv(r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0);
Ok(Self::new(h, s, v))
}
/// 计算与另一个颜色的相似度 (0.0-1.0)
pub fn similarity(&self, other: &ColorHSV) -> f64 {
let hue_diff = (self.hue - other.hue).abs().min(1.0 - (self.hue - other.hue).abs());
let sat_diff = (self.saturation - other.saturation).abs();
let val_diff = (self.value - other.value).abs();
// 加权计算相似度,色相权重最高
let similarity = 1.0 - (hue_diff * 0.5 + sat_diff * 0.3 + val_diff * 0.2);
similarity.clamp(0.0, 1.0)
}
}
/// 分析状态枚举
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AnalysisStatus {
Pending, // 待分析
Processing, // 分析中
Completed, // 已完成
Failed, // 分析失败
}
impl Default for AnalysisStatus {
fn default() -> Self {
Self::Pending
}
}
/// 服装项目信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitProduct {
pub category: String, // 服装类别 (如: "牛仔裤", "T恤")
pub description: String, // 描述
pub color_pattern: ColorHSV, // 主色调
pub design_styles: Vec<String>, // 设计风格标签
pub color_pattern_match_dress: f64, // 与整体搭配的颜色匹配度 (0.0-1.0)
pub color_pattern_match_environment: f64, // 与环境的颜色匹配度 (0.0-1.0)
}
/// 图像分析结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageAnalysisResult {
pub environment_tags: Vec<String>, // 环境标签
pub environment_color_pattern: ColorHSV, // 环境色调
pub dress_color_pattern: ColorHSV, // 整体搭配色调
pub style_description: String, // 风格描述
pub products: Vec<OutfitProduct>, // 识别到的服装项目
}
/// 服装分析记录
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitAnalysis {
pub id: String,
pub project_id: String,
pub image_path: String, // 原始图像路径
pub image_name: String, // 图像名称
pub analysis_status: AnalysisStatus, // 分析状态
pub analysis_result: Option<ImageAnalysisResult>, // 分析结果
pub error_message: Option<String>, // 错误信息
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub analyzed_at: Option<DateTime<Utc>>,
}
/// 创建分析请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateOutfitAnalysisRequest {
pub project_id: String,
pub image_path: String,
pub image_name: String,
}
/// 更新分析请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateOutfitAnalysisRequest {
pub analysis_status: Option<AnalysisStatus>,
pub analysis_result: Option<ImageAnalysisResult>,
pub error_message: Option<String>,
}
/// 分析查询选项
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitAnalysisQueryOptions {
pub project_id: Option<String>,
pub status: Option<AnalysisStatus>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
/// 分析统计信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitAnalysisStats {
pub total_count: u32,
pub pending_count: u32,
pub processing_count: u32,
pub completed_count: u32,
pub failed_count: u32,
}
// HSV和RGB转换函数
fn hsv_to_rgb(h: f64, s: f64, v: f64) -> (f64, f64, f64) {
let c = v * s;
let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
let m = v - c;
let (r_prime, g_prime, b_prime) = match (h * 6.0) as i32 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
5 => (c, 0.0, x),
_ => (0.0, 0.0, 0.0),
};
(r_prime + m, g_prime + m, b_prime + m)
}
fn rgb_to_hsv(r: f64, g: f64, b: f64) -> (f64, f64, f64) {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let delta = max - min;
let h = if delta == 0.0 {
0.0
} else if max == r {
((g - b) / delta) % 6.0
} else if max == g {
(b - r) / delta + 2.0
} else {
(r - g) / delta + 4.0
} / 6.0;
let s = if max == 0.0 { 0.0 } else { delta / max };
let v = max;
(h.abs(), s, v)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_hsv_conversion() {
let color = ColorHSV::new(0.5, 0.8, 0.9);
let hex = color.to_rgb_hex();
let converted = ColorHSV::from_rgb_hex(&hex).unwrap();
assert!((color.hue - converted.hue).abs() < 0.01);
assert!((color.saturation - converted.saturation).abs() < 0.01);
assert!((color.value - converted.value).abs() < 0.01);
}
#[test]
fn test_color_similarity() {
let color1 = ColorHSV::new(0.5, 0.8, 0.9);
let color2 = ColorHSV::new(0.52, 0.82, 0.88);
let similarity = color1.similarity(&color2);
assert!(similarity > 0.8); // 应该很相似
}
}

View File

@@ -0,0 +1,237 @@
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use super::outfit_analysis::ColorHSV;
/// 服装类别枚举
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum OutfitCategory {
Top, // 上装 (T恤、衬衫、毛衣等)
Bottom, // 下装 (裤子、裙子等)
Dress, // 连衣裙
Outerwear, // 外套
Footwear, // 鞋类
Accessory, // 配饰
Other, // 其他
}
impl OutfitCategory {
pub fn from_string(category: &str) -> Self {
match category.to_lowercase().as_str() {
"上装" | "t恤" | "衬衫" | "毛衣" | "背心" | "top" => Self::Top,
"下装" | "裤子" | "牛仔裤" | "短裤" | "裙子" | "bottom" => Self::Bottom,
"连衣裙" | "长裙" | "dress" => Self::Dress,
"外套" | "夹克" | "大衣" | "outerwear" => Self::Outerwear,
"鞋子" | "运动鞋" | "高跟鞋" | "footwear" => Self::Footwear,
"配饰" | "帽子" | "包包" | "首饰" | "accessory" => Self::Accessory,
_ => Self::Other,
}
}
pub fn to_chinese(&self) -> &'static str {
match self {
Self::Top => "上装",
Self::Bottom => "下装",
Self::Dress => "连衣裙",
Self::Outerwear => "外套",
Self::Footwear => "鞋类",
Self::Accessory => "配饰",
Self::Other => "其他",
}
}
}
/// 服装风格枚举
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum OutfitStyle {
Casual, // 休闲
Formal, // 正式
Business, // 商务
Sporty, // 运动
Vintage, // 复古
Bohemian, // 波西米亚
Minimalist, // 极简
Streetwear, // 街头
Elegant, // 优雅
Trendy, // 时尚
Other(String), // 其他自定义风格
}
impl OutfitStyle {
pub fn from_string(style: &str) -> Self {
match style.to_lowercase().as_str() {
"casual" | "休闲" => Self::Casual,
"formal" | "正式" => Self::Formal,
"business" | "商务" => Self::Business,
"sporty" | "运动" => Self::Sporty,
"vintage" | "复古" => Self::Vintage,
"bohemian" | "波西米亚" => Self::Bohemian,
"minimalist" | "极简" => Self::Minimalist,
"streetwear" | "街头" => Self::Streetwear,
"elegant" | "优雅" => Self::Elegant,
"trendy" | "时尚" => Self::Trendy,
_ => Self::Other(style.to_string()),
}
}
pub fn to_string(&self) -> String {
match self {
Self::Casual => "休闲".to_string(),
Self::Formal => "正式".to_string(),
Self::Business => "商务".to_string(),
Self::Sporty => "运动".to_string(),
Self::Vintage => "复古".to_string(),
Self::Bohemian => "波西米亚".to_string(),
Self::Minimalist => "极简".to_string(),
Self::Streetwear => "街头".to_string(),
Self::Elegant => "优雅".to_string(),
Self::Trendy => "时尚".to_string(),
Self::Other(s) => s.clone(),
}
}
}
/// 服装尺寸信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitSize {
pub size_label: String, // 尺码标签 (如: "M", "L", "XL")
pub measurements: Option<SizeMeasurements>, // 具体尺寸
}
/// 具体尺寸测量
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SizeMeasurements {
pub chest: Option<f64>, // 胸围 (cm)
pub waist: Option<f64>, // 腰围 (cm)
pub hip: Option<f64>, // 臀围 (cm)
pub length: Option<f64>, // 长度 (cm)
pub sleeve: Option<f64>, // 袖长 (cm)
}
/// 服装材质信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitMaterial {
pub fabric_type: String, // 面料类型 (如: "棉", "聚酯纤维")
pub fabric_composition: Vec<FabricComponent>, // 面料成分
pub care_instructions: Vec<String>, // 护理说明
}
/// 面料成分
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FabricComponent {
pub material: String, // 材料名称
pub percentage: f64, // 百分比
}
/// 服装单品实体
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitItem {
pub id: String,
pub project_id: String,
pub analysis_id: Option<String>, // 关联的分析记录ID
pub name: String, // 服装名称
pub category: OutfitCategory, // 服装类别
pub brand: Option<String>, // 品牌
pub model: Option<String>, // 型号
pub color_primary: ColorHSV, // 主色调
pub color_secondary: Option<ColorHSV>, // 次要色调
pub styles: Vec<OutfitStyle>, // 风格标签
pub design_elements: Vec<String>, // 设计元素 (如: "条纹", "印花", "刺绣")
pub size: Option<OutfitSize>, // 尺寸信息
pub material: Option<OutfitMaterial>, // 材质信息
pub price: Option<f64>, // 价格
pub purchase_date: Option<DateTime<Utc>>, // 购买日期
pub image_urls: Vec<String>, // 图片URL列表
pub tags: Vec<String>, // 自定义标签
pub notes: Option<String>, // 备注
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// 创建服装单品请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateOutfitItemRequest {
pub project_id: String,
pub analysis_id: Option<String>,
pub name: String,
pub category: OutfitCategory,
pub brand: Option<String>,
pub model: Option<String>,
pub color_primary: ColorHSV,
pub color_secondary: Option<ColorHSV>,
pub styles: Vec<OutfitStyle>,
pub design_elements: Vec<String>,
pub size: Option<OutfitSize>,
pub material: Option<OutfitMaterial>,
pub price: Option<f64>,
pub purchase_date: Option<DateTime<Utc>>,
pub image_urls: Vec<String>,
pub tags: Vec<String>,
pub notes: Option<String>,
}
/// 更新服装单品请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateOutfitItemRequest {
pub name: Option<String>,
pub category: Option<OutfitCategory>,
pub brand: Option<String>,
pub model: Option<String>,
pub color_primary: Option<ColorHSV>,
pub color_secondary: Option<ColorHSV>,
pub styles: Option<Vec<OutfitStyle>>,
pub design_elements: Option<Vec<String>>,
pub size: Option<OutfitSize>,
pub material: Option<OutfitMaterial>,
pub price: Option<f64>,
pub purchase_date: Option<DateTime<Utc>>,
pub image_urls: Option<Vec<String>>,
pub tags: Option<Vec<String>>,
pub notes: Option<String>,
}
/// 服装单品查询选项
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitItemQueryOptions {
pub project_id: Option<String>,
pub category: Option<OutfitCategory>,
pub styles: Option<Vec<OutfitStyle>>,
pub color_similarity_threshold: Option<f64>, // 颜色相似度阈值
pub target_color: Option<ColorHSV>, // 目标颜色
pub brand: Option<String>,
pub tags: Option<Vec<String>>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
/// 服装单品统计信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitItemStats {
pub total_count: u32,
pub category_counts: std::collections::HashMap<String, u32>,
pub style_counts: std::collections::HashMap<String, u32>,
pub brand_counts: std::collections::HashMap<String, u32>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_outfit_category_conversion() {
assert_eq!(OutfitCategory::from_string("T恤"), OutfitCategory::Top);
assert_eq!(OutfitCategory::from_string("牛仔裤"), OutfitCategory::Bottom);
assert_eq!(OutfitCategory::from_string("连衣裙"), OutfitCategory::Dress);
}
#[test]
fn test_outfit_style_conversion() {
assert_eq!(OutfitStyle::from_string("休闲"), OutfitStyle::Casual);
assert_eq!(OutfitStyle::from_string("正式"), OutfitStyle::Formal);
let custom_style = OutfitStyle::from_string("自定义风格");
match custom_style {
OutfitStyle::Other(s) => assert_eq!(s, "自定义风格"),
_ => panic!("Expected Other variant"),
}
}
}

View File

@@ -0,0 +1,237 @@
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use super::outfit_analysis::ColorHSV;
use super::outfit_item::{OutfitCategory, OutfitStyle};
/// 匹配类型枚举
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum MatchingType {
ColorHarmony, // 颜色和谐搭配
StyleConsistent, // 风格一致搭配
Complementary, // 互补搭配
Seasonal, // 季节性搭配
Occasion, // 场合搭配
Trendy, // 时尚搭配
}
impl MatchingType {
pub fn to_chinese(&self) -> &'static str {
match self {
Self::ColorHarmony => "颜色和谐",
Self::StyleConsistent => "风格一致",
Self::Complementary => "互补搭配",
Self::Seasonal => "季节搭配",
Self::Occasion => "场合搭配",
Self::Trendy => "时尚搭配",
}
}
}
/// 匹配置信度等级
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ConfidenceLevel {
Low, // 低 (0.0-0.4)
Medium, // 中 (0.4-0.7)
High, // 高 (0.7-0.9)
Excellent, // 极佳 (0.9-1.0)
}
impl ConfidenceLevel {
pub fn from_score(score: f64) -> Self {
match score {
s if s >= 0.9 => Self::Excellent,
s if s >= 0.7 => Self::High,
s if s >= 0.4 => Self::Medium,
_ => Self::Low,
}
}
pub fn to_chinese(&self) -> &'static str {
match self {
Self::Low => "",
Self::Medium => "",
Self::High => "",
Self::Excellent => "极佳",
}
}
}
/// 搭配评分详情
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatchingScoreDetails {
pub color_harmony_score: f64, // 颜色和谐度评分 (0.0-1.0)
pub style_consistency_score: f64, // 风格一致性评分 (0.0-1.0)
pub proportion_score: f64, // 比例协调度评分 (0.0-1.0)
pub occasion_appropriateness: f64, // 场合适宜度评分 (0.0-1.0)
pub trend_factor: f64, // 时尚度评分 (0.0-1.0)
pub overall_score: f64, // 综合评分 (0.0-1.0)
}
impl MatchingScoreDetails {
pub fn calculate_overall_score(&mut self) {
// 加权计算综合评分
self.overall_score = (
self.color_harmony_score * 0.3 +
self.style_consistency_score * 0.25 +
self.proportion_score * 0.2 +
self.occasion_appropriateness * 0.15 +
self.trend_factor * 0.1
).clamp(0.0, 1.0);
}
pub fn get_confidence_level(&self) -> ConfidenceLevel {
ConfidenceLevel::from_score(self.overall_score)
}
}
/// 搭配建议
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatchingSuggestion {
pub suggestion_type: String, // 建议类型 (如: "颜色调整", "风格优化")
pub description: String, // 建议描述
pub priority: u8, // 优先级 (1-5, 5最高)
}
/// 搭配组合中的单品
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MatchingOutfitItem {
pub item_id: String, // 服装单品ID
pub item_name: String, // 服装名称
pub category: OutfitCategory, // 类别
pub color_primary: ColorHSV, // 主色调
pub styles: Vec<OutfitStyle>, // 风格
pub role_in_outfit: String, // 在搭配中的角色 (如: "主角", "配角", "点缀")
pub image_url: Option<String>, // 图片URL
}
/// 搭配匹配结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitMatching {
pub id: String,
pub project_id: String,
pub matching_name: String, // 搭配名称
pub matching_type: MatchingType, // 匹配类型
pub items: Vec<MatchingOutfitItem>, // 搭配的服装单品
pub score_details: MatchingScoreDetails, // 评分详情
pub suggestions: Vec<MatchingSuggestion>, // 搭配建议
pub occasion_tags: Vec<String>, // 适用场合标签
pub season_tags: Vec<String>, // 适用季节标签
pub style_description: String, // 风格描述
pub color_palette: Vec<ColorHSV>, // 色彩搭配方案
pub is_favorite: bool, // 是否收藏
pub wear_count: u32, // 穿着次数
pub last_worn_date: Option<DateTime<Utc>>, // 最后穿着日期
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// 创建搭配匹配请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateOutfitMatchingRequest {
pub project_id: String,
pub matching_name: String,
pub matching_type: MatchingType,
pub item_ids: Vec<String>, // 服装单品ID列表
pub occasion_tags: Vec<String>,
pub season_tags: Vec<String>,
pub style_description: Option<String>,
}
/// 更新搭配匹配请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateOutfitMatchingRequest {
pub matching_name: Option<String>,
pub matching_type: Option<MatchingType>,
pub item_ids: Option<Vec<String>>,
pub occasion_tags: Option<Vec<String>>,
pub season_tags: Option<Vec<String>>,
pub style_description: Option<String>,
pub is_favorite: Option<bool>,
}
/// 搭配搜索过滤条件
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitMatchingSearchFilters {
pub categories: Option<Vec<OutfitCategory>>, // 包含的服装类别
pub styles: Option<Vec<OutfitStyle>>, // 风格过滤
pub colors: Option<Vec<ColorHSV>>, // 颜色过滤
pub color_similarity_threshold: Option<f64>, // 颜色相似度阈值
pub occasions: Option<Vec<String>>, // 场合过滤
pub seasons: Option<Vec<String>>, // 季节过滤
pub min_score: Option<f64>, // 最低评分
pub confidence_level: Option<ConfidenceLevel>, // 置信度等级
pub matching_types: Option<Vec<MatchingType>>, // 匹配类型
}
/// 搭配匹配查询选项
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitMatchingQueryOptions {
pub project_id: Option<String>,
pub filters: Option<OutfitMatchingSearchFilters>,
pub sort_by: Option<String>, // 排序字段 ("score", "created_at", "wear_count")
pub sort_order: Option<String>, // 排序方向 ("asc", "desc")
pub limit: Option<u32>,
pub offset: Option<u32>,
}
/// 智能搭配推荐请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmartMatchingRequest {
pub project_id: String,
pub base_item_id: Option<String>, // 基础单品ID (可选)
pub target_occasion: Option<String>, // 目标场合
pub target_season: Option<String>, // 目标季节
pub preferred_styles: Option<Vec<OutfitStyle>>, // 偏好风格
pub color_preferences: Option<Vec<ColorHSV>>, // 颜色偏好
pub exclude_item_ids: Option<Vec<String>>, // 排除的单品ID
pub max_results: Option<u32>, // 最大结果数
}
/// 智能搭配推荐结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmartMatchingResult {
pub recommended_matchings: Vec<OutfitMatching>, // 推荐的搭配
pub reasoning: String, // 推荐理由
pub confidence_score: f64, // 推荐置信度
}
/// 搭配匹配统计信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutfitMatchingStats {
pub total_matchings: u32,
pub favorite_count: u32,
pub average_score: f64,
pub most_worn_matching_id: Option<String>,
pub style_distribution: std::collections::HashMap<String, u32>,
pub occasion_distribution: std::collections::HashMap<String, u32>,
pub season_distribution: std::collections::HashMap<String, u32>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_confidence_level_from_score() {
assert_eq!(ConfidenceLevel::from_score(0.95), ConfidenceLevel::Excellent);
assert_eq!(ConfidenceLevel::from_score(0.8), ConfidenceLevel::High);
assert_eq!(ConfidenceLevel::from_score(0.5), ConfidenceLevel::Medium);
assert_eq!(ConfidenceLevel::from_score(0.2), ConfidenceLevel::Low);
}
#[test]
fn test_matching_score_calculation() {
let mut score_details = MatchingScoreDetails {
color_harmony_score: 0.8,
style_consistency_score: 0.9,
proportion_score: 0.7,
occasion_appropriateness: 0.8,
trend_factor: 0.6,
overall_score: 0.0,
};
score_details.calculate_overall_score();
assert!(score_details.overall_score > 0.7);
assert!(score_details.overall_score < 0.9);
}
}

View File

@@ -8,3 +8,6 @@ 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;

View File

@@ -0,0 +1,290 @@
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<Database>,
}
impl OutfitAnalysisRepository {
/// 创建新的服装分析仓库实例
pub fn new(database: Arc<Database>) -> Result<Self> {
Ok(OutfitAnalysisRepository { database })
}
/// 创建服装分析记录
pub fn create(&self, request: &CreateOutfitAnalysisRequest) -> Result<OutfitAnalysis> {
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<Option<OutfitAnalysis>> {
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<Box<dyn rusqlite::ToSql>> = 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<Vec<OutfitAnalysis>> {
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<Box<dyn rusqlite::ToSql>> = 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<OutfitAnalysisStats> {
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<Box<dyn rusqlite::ToSql>> = 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::<AnalysisStatus>(&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<OutfitAnalysis> {
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<String> = row.get(5)?;
let analysis_result = analysis_result
.and_then(|s| serde_json::from_str::<ImageAnalysisResult>(&s).ok());
let created_at_str: String = row.get(7)?;
let updated_at_str: String = row.get(8)?;
let analyzed_at_str: Option<String> = 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)),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::infrastructure::database::Database;
#[test]
fn test_create_and_get_outfit_analysis() {
let database = Arc::new(Database::new_in_memory().unwrap());
let repo = OutfitAnalysisRepository::new(database).unwrap();
let request = CreateOutfitAnalysisRequest {
project_id: "test_project".to_string(),
image_path: "/path/to/image.jpg".to_string(),
image_name: "test_image.jpg".to_string(),
};
let analysis = repo.create(&request).unwrap();
assert_eq!(analysis.project_id, "test_project");
assert_eq!(analysis.image_name, "test_image.jpg");
assert_eq!(analysis.analysis_status, AnalysisStatus::Pending);
let retrieved = repo.get_by_id(&analysis.id).unwrap().unwrap();
assert_eq!(retrieved.id, analysis.id);
assert_eq!(retrieved.project_id, analysis.project_id);
}
}

View File

@@ -0,0 +1,331 @@
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<Database>,
}
impl OutfitItemRepository {
/// 创建新的服装单品仓库实例
pub fn new(database: Arc<Database>) -> Result<Self> {
Ok(OutfitItemRepository { database })
}
/// 创建服装单品
pub fn create(&self, request: &CreateOutfitItemRequest) -> Result<OutfitItem> {
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<Option<OutfitItem>> {
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<Box<dyn rusqlite::ToSql>> = 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<Vec<OutfitItem>> {
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<Box<dyn rusqlite::ToSql>> = 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<Vec<OutfitItem>> {
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<OutfitItemStats> {
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<Box<dyn rusqlite::ToSql>> = 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<OutfitItem> {
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<String> = 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<String>>(13)?.and_then(|s| s.parse().ok()),
purchase_date: row.get::<_, Option<String>>(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),
})
}
}

View File

@@ -0,0 +1,347 @@
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<Database>,
}
impl OutfitMatchingRepository {
/// 创建新的服装搭配仓库实例
pub fn new(database: Arc<Database>) -> Result<Self> {
Ok(OutfitMatchingRepository { database })
}
/// 创建服装搭配
pub fn create(&self, request: &CreateOutfitMatchingRequest) -> Result<OutfitMatching> {
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<Option<OutfitMatching>> {
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<Box<dyn rusqlite::ToSql>> = 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<Vec<OutfitMatching>> {
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<Box<dyn rusqlite::ToSql>> = 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<OutfitMatchingStats> {
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<Box<dyn rusqlite::ToSql>> = 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<f64>>(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<OutfitMatching> {
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<String> = 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),
})
}
}

View File

@@ -808,6 +808,77 @@ impl Database {
[],
)?;
// 创建服装分析表
conn.execute(
"CREATE TABLE IF NOT EXISTS outfit_analyses (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
image_path TEXT NOT NULL,
image_name TEXT NOT NULL,
analysis_status TEXT NOT NULL DEFAULT 'Pending',
analysis_result TEXT,
error_message TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
analyzed_at DATETIME,
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
)",
[],
)?;
// 创建服装单品表
conn.execute(
"CREATE TABLE IF NOT EXISTS outfit_items (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
analysis_id TEXT,
name TEXT NOT NULL,
category TEXT NOT NULL,
brand TEXT,
model TEXT,
color_primary TEXT NOT NULL,
color_secondary TEXT,
styles TEXT NOT NULL,
design_elements TEXT,
size_info TEXT,
material_info TEXT,
price REAL,
purchase_date DATETIME,
image_urls TEXT,
tags TEXT,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
FOREIGN KEY (analysis_id) REFERENCES outfit_analyses (id) ON DELETE SET NULL
)",
[],
)?;
// 创建服装搭配表
conn.execute(
"CREATE TABLE IF NOT EXISTS outfit_matchings (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
matching_name TEXT NOT NULL,
matching_type TEXT NOT NULL,
items TEXT NOT NULL,
score_details TEXT NOT NULL,
suggestions TEXT,
occasion_tags TEXT,
season_tags TEXT,
style_description TEXT NOT NULL,
color_palette TEXT,
is_favorite BOOLEAN DEFAULT 0,
wear_count INTEGER DEFAULT 0,
last_worn_date DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
)",
[],
)?;
// 创建索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_projects_name ON projects (name)",
@@ -951,6 +1022,64 @@ impl Database {
[],
)?;
// 创建服装分析表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_outfit_analyses_project_id ON outfit_analyses (project_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_outfit_analyses_status ON outfit_analyses (analysis_status)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_outfit_analyses_created_at ON outfit_analyses (created_at)",
[],
)?;
// 创建服装单品表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_outfit_items_project_id ON outfit_items (project_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_outfit_items_analysis_id ON outfit_items (analysis_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_outfit_items_category ON outfit_items (category)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_outfit_items_brand ON outfit_items (brand)",
[],
)?;
// 创建服装搭配表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_outfit_matchings_project_id ON outfit_matchings (project_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_outfit_matchings_type ON outfit_matchings (matching_type)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_outfit_matchings_favorite ON outfit_matchings (is_favorite)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_outfit_matchings_created_at ON outfit_matchings (created_at)",
[],
)?;
// 添加新字段(如果不存在)- 数据库迁移
let _ = conn.execute(
"ALTER TABLE template_materials ADD COLUMN file_exists BOOLEAN DEFAULT FALSE",

View File

@@ -86,9 +86,13 @@ struct ContentPart {
#[serde(untagged)]
enum Part {
Text { text: String },
FileData {
FileData {
#[serde(rename = "fileData")]
file_data: FileData
file_data: FileData
},
InlineData {
#[serde(rename = "inlineData")]
inline_data: InlineData
},
}
@@ -100,6 +104,13 @@ struct FileData {
file_uri: String,
}
#[derive(Debug, Serialize)]
struct InlineData {
#[serde(rename = "mimeType")]
mime_type: String,
data: String,
}
#[derive(Debug, Serialize)]
struct GenerationConfig {
temperature: f32,
@@ -134,6 +145,7 @@ struct ResponsePart {
/// Gemini API服务
/// 遵循 Tauri 开发规范的基础设施层设计模式
#[derive(Clone)]
pub struct GeminiService {
config: GeminiConfig,
client: reqwest::Client,
@@ -409,6 +421,64 @@ impl GeminiService {
Ok((file_uri, analysis_result))
}
/// 分析图像内容(用于服装搭配分析)
pub async fn analyze_image_with_prompt(&mut self, image_base64: &str, prompt: &str) -> Result<String> {
println!("🧠 正在进行图像分析...");
// 获取访问令牌
let access_token = self.get_access_token().await?;
// 创建客户端配置
let client_config = self.create_gemini_client(&access_token);
// 准备请求数据
let request_data = GenerateContentRequest {
contents: vec![ContentPart {
role: "user".to_string(),
parts: vec![
Part::Text { text: prompt.to_string() },
Part::InlineData {
inline_data: InlineData {
mime_type: "image/jpeg".to_string(),
data: image_base64.to_string(),
}
}
],
}],
generation_config: GenerationConfig {
temperature: self.config.temperature,
top_k: 32,
top_p: 1.0,
max_output_tokens: self.config.max_tokens,
},
};
// 发送请求到Cloudflare Gateway
let generate_url = format!("{}/{}:generateContent", client_config.gateway_url, self.config.model_name);
// 重试机制
let mut last_error = None;
for attempt in 0..self.config.max_retries {
match self.send_generate_request(&generate_url, &client_config, &request_data).await {
Ok(result) => {
let content = self.parse_gemini_response_content(&result)?;
println!("✅ 图像分析完成");
return Ok(content);
}
Err(e) => {
last_error = Some(e);
if attempt < self.config.max_retries - 1 {
tokio::time::sleep(tokio::time::Duration::from_secs(self.config.retry_delay)).await;
}
}
}
}
Err(anyhow!("图像分析失败,已重试{}次: {}", self.config.max_retries, last_error.unwrap()))
}
}
#[cfg(test)]

View File

@@ -255,7 +255,28 @@ pub fn run() {
commands::debug_commands::test_parse_draft_file,
commands::debug_commands::validate_template_structure,
// 便捷工具命令
commands::tools_commands::clean_jsonl_data
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
])
.setup(|app| {
// 初始化日志系统

View File

@@ -17,3 +17,4 @@ pub mod template_matching_result_commands;
pub mod export_record_commands;
pub mod video_generation_commands;
pub mod tools_commands;
pub mod outfit_commands;

View File

@@ -0,0 +1,333 @@
/**
* 服装搭配相关的 Tauri 命令
* 遵循 Tauri 开发规范的 API 设计原则
*/
use tauri::{command, State};
use std::sync::Arc;
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<crate::infrastructure::database::Database>) -> Result<OutfitAnalysisService, String> {
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<crate::infrastructure::database::Database>) -> Result<OutfitItemService, String> {
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<crate::infrastructure::database::Database>) -> Result<OutfitMatchingService, String> {
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<OutfitAnalysis, String> {
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<Vec<OutfitAnalysis>, 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<Option<OutfitAnalysis>, 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<String>,
) -> Result<OutfitAnalysisStats, String> {
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<OutfitItem, String> {
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<Vec<OutfitItem>, String> {
// 这是一个简化的实现,实际应该从分析结果中提取产品信息
Err("功能开发中".to_string())
}
/// 获取服装单品列表
#[command]
pub async fn list_outfit_items(
state: State<'_, AppState>,
options: OutfitItemQueryOptions,
) -> Result<Vec<OutfitItem>, 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<Option<OutfitItem>, 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<OutfitMatching, String> {
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<Vec<OutfitMatching>, 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<Option<OutfitMatching>, 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<SmartMatchingResult, String> {
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<u8>,
) -> Result<String, String> {
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())
}

View File

@@ -0,0 +1,296 @@
/**
* 服装搭配功能集成测试
* 测试从图像分析到搭配推荐的完整流程
*/
#[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);
}
}