Files
mixvideo-v2/apps/desktop/src-tauri/src/infrastructure/database/migrations.rs
imeepos 7552761d9a feat: 模特详情页改版 - 实现个人看板、形象图片管理和穿搭图片生成功能
新功能:
- 个人看板统计信息展示 (照片数量、穿搭图片、生成记录等)
- 个人形象图片管理 (上传、删除、预览、收藏)
- 穿搭图片生成功能 (选择模特图片 + 上传商品图片 => AI生成穿搭效果)
- 穿搭图片管理界面 (生成记录展示、状态跟踪、结果预览)

 技术实现:
- 新增穿搭图片相关数据模型和数据库表
- 实现OutfitImageService服务层
- 创建多个UI组件 (ModelDashboardStats, ModelImageGallery, OutfitImageGenerator等)
- 优化模特详情页整体布局,采用响应式设计

 UI/UX优化:
- 遵循promptx/frontend-developer设计规范
- 统一的视觉风格和动画效果
- 支持拖拽上传、图片预览、状态指示等交互
- 响应式布局适配不同屏幕尺寸

 测试:
- Rust编译检查通过 (cargo check)
- 前端构建检查通过 (pnpm run -w tauri:web:build)
- 所有TypeScript类型检查通过
2025-07-30 12:51:34 +08:00

595 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use rusqlite::Connection;
use anyhow::{Result, anyhow};
use std::collections::HashMap;
/// 数据库迁移版本号
pub type MigrationVersion = u32;
/// 单个迁移脚本
#[derive(Debug, Clone)]
pub struct Migration {
/// 迁移版本号
pub version: MigrationVersion,
/// 迁移描述
pub description: String,
/// 向前迁移SQL
pub up_sql: String,
/// 回滚迁移SQL可选
pub down_sql: Option<String>,
}
/// 迁移管理器
pub struct MigrationManager {
migrations: HashMap<MigrationVersion, Migration>,
}
impl MigrationManager {
/// 创建新的迁移管理器
pub fn new() -> Self {
let mut manager = Self {
migrations: HashMap::new(),
};
// 注册所有迁移
manager.register_migrations();
manager
}
/// 注册所有迁移脚本
fn register_migrations(&mut self) {
// 迁移 1: 初始化数据库表结构
self.add_migration(Migration {
version: 1,
description: "初始化数据库表结构".to_string(),
up_sql: include_str!("migrations/001_initial_schema.sql").to_string(),
down_sql: Some(include_str!("migrations/001_initial_schema_down.sql").to_string()),
});
// 迁移 2: 添加模特ID字段到素材表
self.add_migration(Migration {
version: 2,
description: "添加模特ID字段到素材表".to_string(),
up_sql: include_str!("migrations/002_add_model_id_to_materials.sql").to_string(),
down_sql: Some(include_str!("migrations/002_add_model_id_to_materials_down.sql").to_string()),
});
// 迁移 3: 添加文件存在状态字段到模板素材表
self.add_migration(Migration {
version: 3,
description: "添加文件存在状态字段到模板素材表".to_string(),
up_sql: include_str!("migrations/003_add_file_status_to_template_materials.sql").to_string(),
down_sql: Some(include_str!("migrations/003_add_file_status_to_template_materials_down.sql").to_string()),
});
// 迁移 4: 移除模板表的project_id字段
self.add_migration(Migration {
version: 4,
description: "移除模板表的project_id字段".to_string(),
up_sql: include_str!("migrations/004_remove_project_id_from_templates.sql").to_string(),
down_sql: None, // 这个迁移不支持回滚,因为涉及数据丢失
});
// 迁移 5: 修复AI分类表的is_active字段类型
self.add_migration(Migration {
version: 5,
description: "修复AI分类表的is_active字段类型".to_string(),
up_sql: include_str!("migrations/005_fix_ai_classifications_is_active.sql").to_string(),
down_sql: None, // 类型修复不支持回滚
});
// 迁移 6: 添加导出状态字段到模板匹配结果表
self.add_migration(Migration {
version: 6,
description: "添加导出状态字段到模板匹配结果表".to_string(),
up_sql: include_str!("migrations/006_add_export_status_to_template_matching.sql").to_string(),
down_sql: Some(include_str!("migrations/006_add_export_status_to_template_matching_down.sql").to_string()),
});
// 迁移 7: 修复项目表的is_active字段数据
self.add_migration(Migration {
version: 7,
description: "修复项目表的is_active字段数据".to_string(),
up_sql: include_str!("migrations/007_fix_projects_is_active_data.sql").to_string(),
down_sql: None, // 数据修复不支持回滚
});
// 迁移 8: 添加缩略图和使用跟踪字段
self.add_migration(Migration {
version: 8,
description: "添加缩略图和使用跟踪字段".to_string(),
up_sql: include_str!("migrations/008_add_thumbnail_and_usage_tracking.sql").to_string(),
down_sql: Some(include_str!("migrations/008_add_thumbnail_and_usage_tracking_down.sql").to_string()),
});
// 迁移 9: 修复template_materials表的file_size字段允许为NULL
self.add_migration(Migration {
version: 9,
description: "修复template_materials表的file_size字段允许为NULL".to_string(),
up_sql: include_str!("migrations/009_fix_template_materials_file_size_nullable.sql").to_string(),
down_sql: Some(include_str!("migrations/009_fix_template_materials_file_size_nullable_down.sql").to_string()),
});
// 迁移 10: 修复video_classification_records表结构添加缺失字段
self.add_migration(Migration {
version: 10,
description: "修复video_classification_records表结构添加缺失字段".to_string(),
up_sql: include_str!("migrations/010_fix_video_classification_records_schema.sql").to_string(),
down_sql: Some(include_str!("migrations/010_fix_video_classification_records_schema_down.sql").to_string()),
});
// 迁移 11: 修复template_matching_results表结构添加缺失字段
self.add_migration(Migration {
version: 11,
description: "修复template_matching_results表结构添加缺失字段".to_string(),
up_sql: include_str!("migrations/011_fix_template_matching_results_schema.sql").to_string(),
down_sql: Some(include_str!("migrations/011_fix_template_matching_results_schema_down.sql").to_string()),
});
// 迁移 12: 创建video_classification_tasks表
self.add_migration(Migration {
version: 12,
description: "创建video_classification_tasks表".to_string(),
up_sql: include_str!("migrations/012_create_video_classification_tasks_table.sql").to_string(),
down_sql: Some(include_str!("migrations/012_create_video_classification_tasks_table_down.sql").to_string()),
});
// 迁移 13: 创建export_records表
self.add_migration(Migration {
version: 13,
description: "创建export_records表".to_string(),
up_sql: include_str!("migrations/013_create_export_records_table.sql").to_string(),
down_sql: Some(include_str!("migrations/013_create_export_records_table_down.sql").to_string()),
});
// 迁移 14: 创建project_template_bindings表
self.add_migration(Migration {
version: 14,
description: "创建project_template_bindings表".to_string(),
up_sql: include_str!("migrations/014_create_project_template_bindings_table.sql").to_string(),
down_sql: Some(include_str!("migrations/014_create_project_template_bindings_table_down.sql").to_string()),
});
// 迁移 15: 添加默认的AI分类数据
self.add_migration(Migration {
version: 15,
description: "添加默认的AI分类数据".to_string(),
up_sql: include_str!("migrations/015_add_default_ai_classifications.sql").to_string(),
down_sql: Some(include_str!("migrations/015_add_default_ai_classifications_down.sql").to_string()),
});
// 迁移 16: 修复AI分类表中的日期时间格式
self.add_migration(Migration {
version: 16,
description: "修复AI分类表中的日期时间格式".to_string(),
up_sql: include_str!("migrations/016_fix_ai_classifications_datetime_format.sql").to_string(),
down_sql: Some(include_str!("migrations/016_fix_ai_classifications_datetime_format_down.sql").to_string()),
});
// 迁移 17: 创建matching_segment_results和matching_failed_segment_results表
self.add_migration(Migration {
version: 17,
description: "创建matching_segment_results和matching_failed_segment_results表".to_string(),
up_sql: include_str!("migrations/017_create_matching_segment_results_tables.sql").to_string(),
down_sql: Some(include_str!("migrations/017_create_matching_segment_results_tables_down.sql").to_string()),
});
// 迁移 18: 创建material_usage_records表
self.add_migration(Migration {
version: 18,
description: "创建material_usage_records表".to_string(),
up_sql: include_str!("migrations/018_create_material_usage_records_table.sql").to_string(),
down_sql: Some(include_str!("migrations/018_create_material_usage_records_table_down.sql").to_string()),
});
// 迁移 19: 为ai_classifications表添加weight字段
self.add_migration(Migration {
version: 19,
description: "为ai_classifications表添加weight字段".to_string(),
up_sql: include_str!("migrations/019_add_weight_to_ai_classifications.sql").to_string(),
down_sql: Some(include_str!("migrations/019_add_weight_to_ai_classifications_down.sql").to_string()),
});
// 迁移 20: 创建模板片段权重配置表
self.add_migration(Migration {
version: 20,
description: "创建模板片段权重配置表".to_string(),
up_sql: include_str!("migrations/020_create_template_segment_weights_table.sql").to_string(),
down_sql: Some(include_str!("migrations/020_create_template_segment_weights_table_down.sql").to_string()),
});
// 迁移 21: 创建穿搭方案收藏表
self.add_migration(Migration {
version: 21,
description: "创建穿搭方案收藏表".to_string(),
up_sql: include_str!("migrations/021_outfit_favorites.sql").to_string(),
down_sql: Some(include_str!("migrations/021_outfit_favorites_down.sql").to_string()),
});
// 迁移 22: 创建图片生成记录表
self.add_migration(Migration {
version: 22,
description: "创建图片生成记录表".to_string(),
up_sql: include_str!("migrations/022_create_image_generation_records_table.sql").to_string(),
down_sql: Some(include_str!("migrations/022_create_image_generation_records_table_down.sql").to_string()),
});
// 迁移 23: 创建穿搭图片相关表
self.add_migration(Migration {
version: 23,
description: "创建穿搭图片相关表".to_string(),
up_sql: include_str!("migrations/023_create_outfit_images_tables.sql").to_string(),
down_sql: Some(include_str!("migrations/023_create_outfit_images_tables_down.sql").to_string()),
});
// 迁移 23: 创建声音克隆记录表
self.add_migration(Migration {
version: 23,
description: "创建声音克隆记录表".to_string(),
up_sql: include_str!("migrations/023_create_voice_clone_table.sql").to_string(),
down_sql: Some(include_str!("migrations/023_create_voice_clone_table_down.sql").to_string()),
});
// 迁移 24: 创建语音生成记录表
self.add_migration(Migration {
version: 24,
description: "创建语音生成记录表".to_string(),
up_sql: include_str!("migrations/024_create_speech_generation_records_table.sql").to_string(),
down_sql: None, // 暂时不提供回滚脚本
});
}
/// 添加迁移
fn add_migration(&mut self, migration: Migration) {
self.migrations.insert(migration.version, migration);
}
/// 获取当前数据库版本
pub fn get_current_version(&self, conn: &Connection) -> Result<MigrationVersion> {
// 确保迁移历史表存在
self.ensure_migration_table(conn)?;
let version = conn.query_row(
"SELECT MAX(version) FROM schema_migrations WHERE success = 1",
[],
|row| row.get::<_, Option<u32>>(0)
).unwrap_or(Some(0)).unwrap_or(0);
Ok(version)
}
/// 获取最新版本号
pub fn get_latest_version(&self) -> MigrationVersion {
self.migrations.keys().max().copied().unwrap_or(0)
}
/// 确保迁移历史表存在
fn ensure_migration_table(&self, conn: &Connection) -> Result<()> {
conn.execute(
"CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
description TEXT NOT NULL,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP,
success INTEGER NOT NULL DEFAULT 0,
error_message TEXT
)",
[],
)?;
Ok(())
}
/// 运行所有待执行的迁移
pub fn migrate(&self, conn: &Connection) -> Result<()> {
let current_version = self.get_current_version(conn)?;
let latest_version = self.get_latest_version();
if current_version >= latest_version {
println!("数据库已是最新版本 v{}", current_version);
return Ok(());
}
println!("开始数据库迁移: v{} -> v{}", current_version, latest_version);
// 按版本号排序执行迁移
let mut versions: Vec<_> = self.migrations.keys().collect();
versions.sort();
for &version in versions {
if version > current_version {
self.apply_migration(conn, version)?;
}
}
println!("数据库迁移完成");
Ok(())
}
/// 应用单个迁移
fn apply_migration(&self, conn: &Connection, version: MigrationVersion) -> Result<()> {
let migration = self.migrations.get(&version)
.ok_or_else(|| anyhow!("迁移版本 {} 不存在", version))?;
println!("应用迁移 v{}: {}", version, migration.description);
// 开始事务
let tx = conn.unchecked_transaction()?;
// 记录迁移开始
tx.execute(
"INSERT OR REPLACE INTO schema_migrations (version, description, success) VALUES (?1, ?2, 0)",
[&version.to_string(), &migration.description],
)?;
// 执行迁移SQL
match self.execute_migration_sql(&tx, &migration.up_sql) {
Ok(_) => {
// 标记迁移成功
tx.execute(
"UPDATE schema_migrations SET success = 1, applied_at = CURRENT_TIMESTAMP WHERE version = ?1",
[&version.to_string()],
)?;
tx.commit()?;
println!("迁移 v{} 应用成功", version);
Ok(())
}
Err(e) => {
// 记录错误信息
let _ = tx.execute(
"UPDATE schema_migrations SET error_message = ?1 WHERE version = ?2",
[&e.to_string(), &version.to_string()],
);
tx.rollback()?;
Err(anyhow!("迁移 v{} 失败: {}", version, e))
}
}
}
/// 执行迁移SQL支持多条语句
fn execute_migration_sql(&self, conn: &Connection, sql: &str) -> Result<()> {
// 使用SQLite的execute_batch方法来执行多条SQL语句
// 这个方法能正确处理SQL语句的分割包括注释和字符串中的分号
conn.execute_batch(sql)?;
Ok(())
}
/// 回滚到指定版本
pub fn rollback_to(&self, conn: &Connection, target_version: MigrationVersion) -> Result<()> {
let current_version = self.get_current_version(conn)?;
if target_version >= current_version {
return Err(anyhow!("目标版本 {} 不能大于等于当前版本 {}", target_version, current_version));
}
println!("开始回滚数据库: v{} -> v{}", current_version, target_version);
// 按版本号倒序回滚
let mut versions: Vec<_> = self.migrations.keys().collect();
versions.sort_by(|a, b| b.cmp(a)); // 倒序
for &version in versions {
if version > target_version && version <= current_version {
self.rollback_migration(conn, version)?;
}
}
println!("数据库回滚完成");
Ok(())
}
/// 回滚单个迁移
fn rollback_migration(&self, conn: &Connection, version: MigrationVersion) -> Result<()> {
let migration = self.migrations.get(&version)
.ok_or_else(|| anyhow!("迁移版本 {} 不存在", version))?;
let down_sql = migration.down_sql.as_ref()
.ok_or_else(|| anyhow!("迁移 v{} 不支持回滚", version))?;
println!("回滚迁移 v{}: {}", version, migration.description);
// 开始事务
let tx = conn.unchecked_transaction()?;
// 执行回滚SQL
match self.execute_migration_sql(&tx, down_sql) {
Ok(_) => {
// 删除迁移记录
tx.execute(
"DELETE FROM schema_migrations WHERE version = ?1",
[&version.to_string()],
)?;
tx.commit()?;
println!("迁移 v{} 回滚成功", version);
Ok(())
}
Err(e) => {
tx.rollback()?;
Err(anyhow!("迁移 v{} 回滚失败: {}", version, e))
}
}
}
/// 获取迁移历史
pub fn get_migration_history(&self, conn: &Connection) -> Result<Vec<(MigrationVersion, String, String, bool)>> {
self.ensure_migration_table(conn)?;
let mut stmt = conn.prepare(
"SELECT version, description, applied_at, success FROM schema_migrations ORDER BY version"
)?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, u32>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, i32>(3)? == 1,
))
})?;
let mut history = Vec::new();
for row in rows {
history.push(row?);
}
Ok(history)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rusqlite::Connection;
use tempfile::NamedTempFile;
fn create_test_connection() -> Connection {
let temp_file = NamedTempFile::new().unwrap();
let conn = Connection::open(temp_file.path()).unwrap();
// 配置数据库设置
conn.pragma_update(None, "foreign_keys", "ON").unwrap();
conn.pragma_update(None, "journal_mode", "WAL").unwrap();
conn
}
#[test]
fn test_migration_manager_creation() {
let manager = MigrationManager::new();
let latest_version = manager.get_latest_version();
// 应该有至少8个迁移版本
assert!(latest_version >= 8, "应该有至少8个迁移版本实际: {}", latest_version);
}
#[test]
fn test_initial_version_is_zero() {
let conn = create_test_connection();
let manager = MigrationManager::new();
let current_version = manager.get_current_version(&conn).unwrap();
assert_eq!(current_version, 0, "新数据库的版本应该是0");
}
#[test]
fn test_migration_table_creation() {
let conn = create_test_connection();
let manager = MigrationManager::new();
// 确保迁移表存在
manager.ensure_migration_table(&conn).unwrap();
// 验证表是否存在
let table_exists: bool = conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='schema_migrations'",
[],
|row| row.get::<_, i32>(0)
).unwrap() > 0;
assert!(table_exists, "schema_migrations表应该存在");
}
#[test]
fn test_full_migration() {
let conn = create_test_connection();
let manager = MigrationManager::new();
// 执行所有迁移
manager.migrate(&conn).unwrap();
// 验证版本已更新
let current_version = manager.get_current_version(&conn).unwrap();
let latest_version = manager.get_latest_version();
assert_eq!(current_version, latest_version, "迁移后版本应该是最新版本");
// 验证一些关键表是否存在
let tables = vec![
"projects", "materials", "material_segments", "models",
"ai_classifications", "templates", "template_materials",
"tracks", "track_segments", "video_classification_records"
];
for table in tables {
let table_exists: bool = conn.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1",
[table],
|row| row.get::<_, i32>(0)
).unwrap() > 0;
assert!(table_exists, "表 {} 应该存在", table);
}
}
#[test]
fn test_migration_history() {
let conn = create_test_connection();
let manager = MigrationManager::new();
// 执行迁移
manager.migrate(&conn).unwrap();
// 获取迁移历史
let history = manager.get_migration_history(&conn).unwrap();
// 应该有迁移记录
assert!(!history.is_empty(), "应该有迁移历史记录");
// 所有迁移都应该成功
for (version, description, _applied_at, success) in history {
assert!(success, "迁移 v{}: {} 应该成功", version, description);
}
}
#[test]
fn test_idempotent_migration() {
let conn = create_test_connection();
let manager = MigrationManager::new();
// 第一次迁移
manager.migrate(&conn).unwrap();
let version_after_first = manager.get_current_version(&conn).unwrap();
// 第二次迁移(应该是幂等的)
manager.migrate(&conn).unwrap();
let version_after_second = manager.get_current_version(&conn).unwrap();
assert_eq!(version_after_first, version_after_second, "重复迁移应该是幂等的");
}
#[test]
fn test_specific_migration_features() {
let conn = create_test_connection();
let manager = MigrationManager::new();
// 执行迁移
manager.migrate(&conn).unwrap();
// 测试特定的迁移功能
// 1. 检查materials表是否有model_id字段迁移2
let has_model_id = conn.prepare("SELECT model_id FROM materials LIMIT 1").is_ok();
assert!(has_model_id, "materials表应该有model_id字段");
// 2. 检查template_materials表是否有file_exists字段迁移3
let has_file_exists = conn.prepare("SELECT file_exists FROM template_materials LIMIT 1").is_ok();
assert!(has_file_exists, "template_materials表应该有file_exists字段");
// 3. 检查materials表是否有thumbnail_path字段迁移8
let has_materials_thumbnail_path = conn.prepare("SELECT thumbnail_path FROM materials LIMIT 1").is_ok();
assert!(has_materials_thumbnail_path, "materials表应该有thumbnail_path字段");
// 4. 检查material_segments表是否有usage_count字段迁移8
let has_usage_count = conn.prepare("SELECT usage_count FROM material_segments LIMIT 1").is_ok();
assert!(has_usage_count, "material_segments表应该有usage_count字段");
// 5. 检查template_matching_results表是否有export_count字段迁移8
let has_export_count = conn.prepare("SELECT export_count FROM template_matching_results LIMIT 1").is_ok();
assert!(has_export_count, "template_matching_results表应该有export_count字段");
// 6. 验证初始化时就存在的字段
let has_matching_rule = conn.prepare("SELECT matching_rule FROM track_segments LIMIT 1").is_ok();
assert!(has_matching_rule, "track_segments表应该有matching_rule字段初始化时创建");
let has_segments_thumbnail_path = conn.prepare("SELECT thumbnail_path FROM material_segments LIMIT 1").is_ok();
assert!(has_segments_thumbnail_path, "material_segments表应该有thumbnail_path字段初始化时创建");
}
}