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

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

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

1642 lines
62 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 std::path::PathBuf;
use std::sync::{Arc, Mutex};
use anyhow::{Result, anyhow};
use std::ops::{Deref, DerefMut};
use crate::infrastructure::connection_pool::{ConnectionPool, ConnectionPoolConfig, PooledConnectionHandle};
/// 统一的数据库连接句柄
/// 可以是单连接模式的 MutexGuard 或连接池模式的 PooledConnectionHandle
pub enum ConnectionHandle<'a> {
/// 单连接模式(兼容模式)
Single(std::sync::MutexGuard<'a, Connection>),
/// 连接池模式(推荐)
Pooled(PooledConnectionHandle),
}
impl<'a> Deref for ConnectionHandle<'a> {
type Target = Connection;
fn deref(&self) -> &Self::Target {
match self {
ConnectionHandle::Single(conn) => conn,
ConnectionHandle::Pooled(conn) => conn.as_ref(),
}
}
}
impl<'a> DerefMut for ConnectionHandle<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
ConnectionHandle::Single(conn) => conn,
ConnectionHandle::Pooled(conn) => conn.as_mut(),
}
}
}
/// 数据库管理器
/// 遵循 Tauri 开发规范的数据库设计模式
/// 支持连接池以提高并发性能,实现读写分离
pub struct Database {
// 主连接(用于写操作和复杂事务)
connection: Arc<Mutex<Connection>>,
// 只读连接(专门用于读操作,避免锁竞争)
read_connection: Arc<Mutex<Connection>>,
// 连接池支持
pool: Option<Arc<ConnectionPool>>,
}
/// 数据库连接模式
#[derive(Debug, Clone)]
pub enum ConnectionMode {
/// 单连接模式(兼容模式)
Single,
/// 连接池模式(推荐)
Pool(ConnectionPoolConfig),
}
impl Database {
/// 创建新的数据库实例
/// 遵循安全第一原则,确保数据库文件的安全存储
pub fn new() -> Result<Self> {
let app_data_dir = dirs::data_dir()
.ok_or_else(|| anyhow!("无法获取应用数据目录"))?
.join("mixvideo");
std::fs::create_dir_all(&app_data_dir)?;
let db_path = app_data_dir.join("mixvideo.db");
Self::new_with_path(db_path.to_str().unwrap())
}
/// 创建带连接池的数据库实例(推荐用于生产环境)
pub fn new_with_pool() -> Result<Self> {
let app_data_dir = dirs::data_dir()
.ok_or_else(|| anyhow!("无法获取应用数据目录"))?
.join("mixvideo");
std::fs::create_dir_all(&app_data_dir)?;
let db_path = app_data_dir.join("mixvideo.db");
// 检查数据库是否被锁定
if let Err(e) = Self::check_database_lock(&db_path) {
eprintln!("数据库锁检查失败: {}", e);
// 尝试清理锁文件
Self::cleanup_database_locks(&db_path)?;
}
Self::new_with_path_and_pool(db_path.to_str().unwrap(), Some(ConnectionPoolConfig::default()))
}
/// 检查数据库锁状态
fn check_database_lock(db_path: &PathBuf) -> Result<()> {
// 检查 WAL 和 SHM 文件
let wal_path = db_path.with_extension("db-wal");
let shm_path = db_path.with_extension("db-shm");
if wal_path.exists() || shm_path.exists() {
println!("检测到 WAL/SHM 文件,数据库可能正在被其他进程使用");
}
// 尝试打开数据库进行快速检查
match Connection::open(db_path) {
Ok(conn) => {
// 尝试执行一个简单的查询
match conn.execute("SELECT 1", []) {
Ok(_) => {
println!("数据库连接检查通过");
Ok(())
},
Err(e) => Err(anyhow!("数据库查询失败: {}", e))
}
},
Err(e) => Err(anyhow!("无法打开数据库: {}", e))
}
}
/// 清理数据库锁文件
fn cleanup_database_locks(db_path: &PathBuf) -> Result<()> {
let wal_path = db_path.with_extension("db-wal");
let shm_path = db_path.with_extension("db-shm");
if wal_path.exists() {
println!("清理 WAL 文件: {:?}", wal_path);
std::fs::remove_file(&wal_path).ok(); // 忽略错误
}
if shm_path.exists() {
println!("清理 SHM 文件: {:?}", shm_path);
std::fs::remove_file(&shm_path).ok(); // 忽略错误
}
Ok(())
}
/// 使用指定路径创建数据库实例(主要用于测试)
pub fn new_with_path(db_path: &str) -> Result<Self> {
Self::new_with_path_and_pool(db_path, None)
}
/// 使用指定路径和连接池配置创建数据库实例
pub fn new_with_path_and_pool(db_path: &str, pool_config: Option<ConnectionPoolConfig>) -> Result<Self> {
let db_path = std::path::PathBuf::from(db_path);
// 打印数据库路径用于调试
println!("Initializing database at: {}", db_path.display());
// 确保数据库目录存在
if let Some(parent) = db_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
eprintln!("Failed to create database directory: {}", e);
rusqlite::Error::SqliteFailure(
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CANTOPEN),
Some(format!("Failed to create database directory: {}", e)),
)
})?;
}
let connection = Connection::open(&db_path)?;
println!("Database connection established successfully");
// 遵循 Tauri 开发规范的安全第一原则
// 设置数据库安全配置
connection.pragma_update(None, "secure_delete", "ON")?;
connection.pragma_update(None, "temp_store", "MEMORY")?;
// 配置数据库设置
connection.pragma_update(None, "foreign_keys", "ON")?;
connection.pragma_update(None, "journal_mode", "WAL")?; // 使用 WAL 模式以支持并发访问
connection.pragma_update(None, "synchronous", "NORMAL")?; // 使用 NORMAL 以提高性能
connection.pragma_update(None, "cache_size", "10000")?; // 增加缓存大小
connection.pragma_update(None, "busy_timeout", "5000")?; // 设置忙等待超时为5秒
println!("Database pragmas configured");
// 创建连接池(如果配置了)
let pool = if let Some(config) = pool_config {
println!("Initializing connection pool with min={}, max={} connections",
config.min_connections, config.max_connections);
let pool = ConnectionPool::new(db_path.to_string_lossy().to_string(), config)?;
println!("Connection pool initialized successfully");
Some(pool)
} else {
println!("Using single connection mode (no pool)");
None
};
// 创建专用的只读连接
let read_connection = Connection::open(&db_path)?;
read_connection.pragma_update(None, "secure_delete", "ON")?;
read_connection.pragma_update(None, "temp_store", "MEMORY")?;
read_connection.pragma_update(None, "foreign_keys", "ON")?;
read_connection.pragma_update(None, "journal_mode", "WAL")?;
read_connection.pragma_update(None, "synchronous", "NORMAL")?;
read_connection.pragma_update(None, "cache_size", "10000")?;
read_connection.pragma_update(None, "busy_timeout", "5000")?;
// 只读连接设置为只读模式
read_connection.pragma_update(None, "query_only", "ON")?;
println!("只读数据库连接创建成功");
let database = Database {
connection: Arc::new(Mutex::new(connection)),
read_connection: Arc::new(Mutex::new(read_connection)),
pool,
};
// 初始化数据库表
database.initialize_tables()?;
// 运行数据库迁移
database.run_migrations()?;
Ok(database)
}
/// 获取数据库连接(用于写操作)
pub fn get_connection(&self) -> Arc<Mutex<Connection>> {
Arc::clone(&self.connection)
}
/// 获取只读数据库连接(用于读操作)
/// 这个连接专门用于查询操作,避免与写操作竞争锁
pub fn get_read_connection(&self) -> Arc<Mutex<Connection>> {
Arc::clone(&self.read_connection)
}
/// 检查数据库连接状态
pub fn check_connection_status(&self) -> String {
match self.connection.try_lock() {
Ok(_) => "连接可用".to_string(),
Err(std::sync::TryLockError::Poisoned(_)) => "连接已损坏".to_string(),
Err(std::sync::TryLockError::WouldBlock) => "连接被占用".to_string(),
}
}
/// 尝试非阻塞获取数据库连接(写连接)
/// 如果连接被占用,立即返回 None
pub fn try_get_connection(&self) -> Option<std::sync::MutexGuard<Connection>> {
match self.connection.try_lock() {
Ok(conn) => Some(conn),
Err(_) => None,
}
}
/// 尝试非阻塞获取只读数据库连接
/// 如果连接被占用,立即返回 None
pub fn try_get_read_connection(&self) -> Option<std::sync::MutexGuard<Connection>> {
match self.read_connection.try_lock() {
Ok(conn) => Some(conn),
Err(_) => None,
}
}
/// 获取最佳的只读连接
/// 优先使用专用只读连接,如果不可用则尝试主连接
pub fn get_best_read_connection(&self) -> Result<ConnectionHandle> {
// 首先尝试专用只读连接
match self.read_connection.try_lock() {
Ok(conn) => {
println!("使用专用只读连接");
return Ok(ConnectionHandle::Single(conn));
},
Err(_) => {
println!("只读连接被占用,尝试连接池");
}
}
// 如果只读连接被占用,尝试连接池
if let Some(pool) = &self.pool {
match pool.try_acquire()? {
Some(conn) => {
println!("使用连接池连接进行读操作");
return Ok(ConnectionHandle::Pooled(conn));
},
None => {
println!("连接池无可用连接,尝试主连接");
}
}
}
// 最后尝试主连接(非阻塞)
match self.connection.try_lock() {
Ok(conn) => {
println!("使用主连接进行读操作");
Ok(ConnectionHandle::Single(conn))
},
Err(_) => {
println!("所有连接都被占用,读操作失败");
Err(anyhow!("所有数据库连接都被占用"))
}
}
}
/// 检查是否启用了连接池
pub fn has_pool(&self) -> bool {
self.pool.is_some()
}
/// 从连接池获取连接(推荐)
/// 如果连接池未启用,返回错误
pub fn acquire_from_pool(&self) -> Result<PooledConnectionHandle> {
match &self.pool {
Some(pool) => pool.acquire().map_err(|e| anyhow!("获取连接池连接失败: {}", e)),
None => Err(anyhow!("连接池未启用")),
}
}
/// 尝试从连接池获取连接(非阻塞)
/// 如果连接池未启用或无可用连接,返回 None
pub fn try_acquire_from_pool(&self) -> Result<Option<PooledConnectionHandle>> {
match &self.pool {
Some(pool) => pool.try_acquire().map_err(|e| anyhow!("尝试获取连接池连接失败: {}", e)),
None => Ok(None),
}
}
/// 获取连接(自动选择最佳方式)
/// 如果连接池可用,优先使用连接池
/// 否则使用单连接模式
pub fn get_best_connection(&self) -> Result<ConnectionHandle> {
if let Some(pool) = &self.pool {
// 优先使用连接池
match pool.try_acquire()? {
Some(conn) => return Ok(ConnectionHandle::Pooled(conn)),
None => {
// 连接池中没有可用连接,尝试获取主连接
println!("连接池中没有可用连接,尝试获取主连接");
}
}
}
// 回退到单连接模式
match self.connection.try_lock() {
Ok(conn) => Ok(ConnectionHandle::Single(conn)),
Err(_) => Err(anyhow!("所有数据库连接都被占用")),
}
}
/// 执行数据库操作的辅助方法,自动处理锁的获取和释放
/// 这是推荐的数据库访问方式,可以避免锁竞争问题
pub fn with_connection<T, F>(&self, operation: F) -> Result<T, rusqlite::Error>
where
F: FnOnce(&Connection) -> Result<T, rusqlite::Error>,
{
// 直接使用 connection 字段,避免额外的 Arc::clone
match self.connection.try_lock() {
Ok(conn) => {
// 成功获取锁,执行操作
operation(&*conn)
},
Err(_) => {
// 锁被占用,等待一小段时间后重试
std::thread::sleep(std::time::Duration::from_millis(10));
// 使用阻塞方式获取锁,但有错误处理
let conn = self.connection.lock().map_err(|e| {
eprintln!("数据库连接锁获取失败: {}", e);
rusqlite::Error::SqliteFailure(
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_BUSY),
Some("数据库连接被锁定".to_string()),
)
})?;
operation(&*conn)
}
}
}
/// 执行只读查询的辅助方法
pub fn query<T, F>(&self, operation: F) -> Result<T, rusqlite::Error>
where
F: FnOnce(&Connection) -> Result<T, rusqlite::Error>,
{
self.with_connection(operation)
}
/// 执行写入操作的辅助方法
pub fn execute<T, F>(&self, operation: F) -> Result<T, rusqlite::Error>
where
F: FnOnce(&Connection) -> Result<T, rusqlite::Error>,
{
self.with_connection(operation)
}
/// 初始化数据库表结构
/// 遵循模块化设计原则,清晰的表结构定义
fn initialize_tables(&self) -> Result<()> {
let conn = self.connection.lock().unwrap();
// 创建项目表
conn.execute(
"CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
path TEXT NOT NULL UNIQUE,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT 1
)",
[],
)?;
// 创建项目配置表
conn.execute(
"CREATE TABLE IF NOT EXISTS project_configs (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
config_key TEXT NOT NULL,
config_value TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
UNIQUE(project_id, config_key)
)",
[],
)?;
// 创建素材表
conn.execute(
"CREATE TABLE IF NOT EXISTS materials (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
model_id TEXT,
name TEXT NOT NULL,
original_path TEXT NOT NULL,
file_size INTEGER NOT NULL,
md5_hash TEXT NOT NULL,
material_type TEXT NOT NULL,
processing_status TEXT NOT NULL DEFAULT 'Pending',
metadata TEXT,
scene_detection TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
processed_at DATETIME,
error_message TEXT,
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
UNIQUE(project_id, md5_hash)
)",
[],
)?;
// 创建素材片段表
conn.execute(
"CREATE TABLE IF NOT EXISTS material_segments (
id TEXT PRIMARY KEY,
material_id TEXT NOT NULL,
segment_index INTEGER NOT NULL,
start_time REAL NOT NULL,
end_time REAL NOT NULL,
duration REAL NOT NULL,
file_path TEXT NOT NULL,
file_size INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (material_id) REFERENCES materials (id) ON DELETE CASCADE
)",
[],
)?;
// 创建模特表
conn.execute(
"CREATE TABLE IF NOT EXISTS models (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
stage_name TEXT,
gender TEXT NOT NULL,
age INTEGER,
height INTEGER,
weight INTEGER,
measurements TEXT,
description TEXT,
tags TEXT,
avatar_path TEXT,
contact_info TEXT,
social_media TEXT,
status TEXT NOT NULL DEFAULT 'Active',
rating REAL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT 1
)",
[],
)?;
// 创建模特照片表
conn.execute(
"CREATE TABLE IF NOT EXISTS model_photos (
id TEXT PRIMARY KEY,
model_id TEXT NOT NULL,
file_path TEXT NOT NULL,
file_name TEXT NOT NULL,
file_size INTEGER NOT NULL,
photo_type TEXT NOT NULL,
description TEXT,
tags TEXT,
is_cover BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (model_id) REFERENCES models (id) ON DELETE CASCADE
)",
[],
)?;
// 创建AI分类表
conn.execute(
"CREATE TABLE IF NOT EXISTS ai_classifications (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
prompt_text TEXT NOT NULL,
description TEXT,
is_active INTEGER DEFAULT 1,
sort_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)",
[],
)?;
// 创建视频分类记录表
conn.execute(
"CREATE TABLE IF NOT EXISTS video_classification_records (
id TEXT PRIMARY KEY,
segment_id TEXT NOT NULL,
material_id TEXT NOT NULL,
project_id TEXT NOT NULL,
category TEXT NOT NULL,
confidence REAL NOT NULL,
reasoning TEXT NOT NULL,
features TEXT NOT NULL,
product_match INTEGER NOT NULL,
quality_score REAL NOT NULL,
gemini_file_uri TEXT,
raw_response TEXT,
status TEXT NOT NULL,
error_message TEXT,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (segment_id) REFERENCES material_segments (id) ON DELETE CASCADE,
FOREIGN KEY (material_id) REFERENCES materials (id) ON DELETE CASCADE,
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
)",
[],
)?;
// 创建视频分类任务表
conn.execute(
"CREATE TABLE IF NOT EXISTS video_classification_tasks (
id TEXT PRIMARY KEY,
segment_id TEXT NOT NULL,
material_id TEXT NOT NULL,
project_id TEXT NOT NULL,
video_file_path TEXT NOT NULL,
status TEXT NOT NULL,
priority INTEGER DEFAULT 0,
retry_count INTEGER DEFAULT 0,
max_retries INTEGER DEFAULT 3,
gemini_file_uri TEXT,
prompt_text TEXT,
error_message TEXT,
started_at DATETIME,
completed_at DATETIME,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (segment_id) REFERENCES material_segments (id) ON DELETE CASCADE,
FOREIGN KEY (material_id) REFERENCES materials (id) ON DELETE CASCADE,
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
)",
[],
)?;
// 创建模板表
conn.execute(
"CREATE TABLE IF NOT EXISTS templates (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
canvas_width INTEGER NOT NULL,
canvas_height INTEGER NOT NULL,
canvas_ratio TEXT NOT NULL,
duration INTEGER NOT NULL,
fps REAL NOT NULL,
import_status TEXT NOT NULL DEFAULT 'Pending',
source_file_path TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT 1
)",
[],
)?;
// 创建模板素材表
conn.execute(
"CREATE TABLE IF NOT EXISTS template_materials (
id TEXT PRIMARY KEY,
template_id TEXT NOT NULL,
original_id TEXT NOT NULL,
name TEXT NOT NULL,
material_type TEXT NOT NULL,
original_path TEXT NOT NULL,
remote_url TEXT,
file_size INTEGER,
duration INTEGER,
width INTEGER,
height INTEGER,
upload_status TEXT NOT NULL DEFAULT 'Pending',
file_exists BOOLEAN DEFAULT FALSE,
upload_success BOOLEAN DEFAULT FALSE,
metadata TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (template_id) REFERENCES templates (id) ON DELETE CASCADE
)",
[],
)?;
// 创建轨道表
conn.execute(
"CREATE TABLE IF NOT EXISTS tracks (
id TEXT PRIMARY KEY,
template_id TEXT NOT NULL,
name TEXT NOT NULL,
track_type TEXT NOT NULL,
track_index INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (template_id) REFERENCES templates (id) ON DELETE CASCADE
)",
[],
)?;
// 创建轨道片段表
conn.execute(
"CREATE TABLE IF NOT EXISTS track_segments (
id TEXT PRIMARY KEY,
track_id TEXT NOT NULL,
template_material_id TEXT,
name TEXT NOT NULL,
start_time INTEGER NOT NULL,
end_time INTEGER NOT NULL,
duration INTEGER NOT NULL,
segment_index INTEGER NOT NULL,
properties TEXT,
matching_rule TEXT DEFAULT '\"FixedMaterial\"',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (track_id) REFERENCES tracks (id) ON DELETE CASCADE,
FOREIGN KEY (template_material_id) REFERENCES template_materials (id) ON DELETE SET NULL
)",
[],
)?;
// 创建项目-模板绑定表
conn.execute(
"CREATE TABLE IF NOT EXISTS project_template_bindings (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
template_id TEXT NOT NULL,
binding_name TEXT,
description TEXT,
priority INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
binding_type TEXT NOT NULL,
binding_status TEXT NOT NULL,
metadata 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 (template_id) REFERENCES templates (id) ON DELETE CASCADE,
UNIQUE (project_id, template_id)
)",
[],
)?;
// 创建模板匹配结果表
conn.execute(
"CREATE TABLE IF NOT EXISTS template_matching_results (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
template_id TEXT NOT NULL,
binding_id TEXT NOT NULL,
result_name TEXT NOT NULL,
description TEXT,
total_segments INTEGER NOT NULL DEFAULT 0,
matched_segments INTEGER NOT NULL DEFAULT 0,
failed_segments INTEGER NOT NULL DEFAULT 0,
success_rate REAL NOT NULL DEFAULT 0.0,
used_materials INTEGER NOT NULL DEFAULT 0,
used_models INTEGER NOT NULL DEFAULT 0,
matching_duration_ms INTEGER NOT NULL DEFAULT 0,
quality_score REAL,
status TEXT NOT NULL DEFAULT 'Success',
metadata TEXT,
export_count INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
is_active INTEGER DEFAULT 1,
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
FOREIGN KEY (template_id) REFERENCES templates (id) ON DELETE CASCADE,
FOREIGN KEY (binding_id) REFERENCES project_template_bindings (id) ON DELETE CASCADE
)",
[],
)?;
// 创建匹配片段结果表
conn.execute(
"CREATE TABLE IF NOT EXISTS matching_segment_results (
id TEXT PRIMARY KEY,
matching_result_id TEXT NOT NULL,
track_segment_id TEXT NOT NULL,
track_segment_name TEXT NOT NULL,
material_segment_id TEXT NOT NULL,
material_id TEXT NOT NULL,
material_name TEXT NOT NULL,
model_id TEXT,
model_name TEXT,
match_score REAL NOT NULL,
match_reason TEXT NOT NULL,
segment_duration INTEGER NOT NULL,
start_time INTEGER NOT NULL,
end_time INTEGER NOT NULL,
properties TEXT,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (matching_result_id) REFERENCES template_matching_results (id) ON DELETE CASCADE,
FOREIGN KEY (material_segment_id) REFERENCES material_segments (id) ON DELETE CASCADE,
FOREIGN KEY (material_id) REFERENCES materials (id) ON DELETE CASCADE,
FOREIGN KEY (model_id) REFERENCES models (id) ON DELETE SET NULL
)",
[],
)?;
// 创建匹配失败片段结果表
conn.execute(
"CREATE TABLE IF NOT EXISTS matching_failed_segment_results (
id TEXT PRIMARY KEY,
matching_result_id TEXT NOT NULL,
track_segment_id TEXT NOT NULL,
track_segment_name TEXT NOT NULL,
matching_rule_type TEXT NOT NULL,
matching_rule_data TEXT,
failure_reason TEXT NOT NULL,
failure_details TEXT,
segment_duration INTEGER NOT NULL,
start_time INTEGER NOT NULL,
end_time INTEGER NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (matching_result_id) REFERENCES template_matching_results (id) ON DELETE CASCADE
)",
[],
)?;
// 创建导出记录表
conn.execute(
"CREATE TABLE IF NOT EXISTS export_records (
id TEXT PRIMARY KEY,
matching_result_id TEXT NOT NULL,
project_id TEXT NOT NULL,
template_id TEXT NOT NULL,
export_type TEXT NOT NULL,
export_format TEXT NOT NULL,
file_path TEXT NOT NULL,
file_size INTEGER,
export_status TEXT NOT NULL DEFAULT 'Success',
export_duration_ms INTEGER NOT NULL DEFAULT 0,
error_message TEXT,
metadata TEXT,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
is_active INTEGER DEFAULT 1,
FOREIGN KEY (matching_result_id) REFERENCES template_matching_results (id) ON DELETE CASCADE,
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
FOREIGN KEY (template_id) REFERENCES templates (id) ON DELETE CASCADE
)",
[],
)?;
// 创建素材使用记录表
conn.execute(
"CREATE TABLE IF NOT EXISTS material_usage_records (
id TEXT PRIMARY KEY,
material_segment_id TEXT NOT NULL,
material_id TEXT NOT NULL,
project_id TEXT NOT NULL,
template_matching_result_id TEXT NOT NULL,
template_id TEXT NOT NULL,
binding_id TEXT NOT NULL,
track_segment_id TEXT NOT NULL,
usage_type TEXT NOT NULL DEFAULT 'TemplateMatching',
usage_context TEXT,
created_at DATETIME NOT NULL,
FOREIGN KEY (material_segment_id) REFERENCES material_segments (id) ON DELETE CASCADE,
FOREIGN KEY (material_id) REFERENCES materials (id) ON DELETE CASCADE,
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
FOREIGN KEY (template_matching_result_id) REFERENCES template_matching_results (id) ON DELETE CASCADE
)",
[],
)?;
// 创建性能监控表
conn.execute(
"CREATE TABLE IF NOT EXISTS performance_metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
metric_name TEXT NOT NULL,
metric_value REAL NOT NULL,
recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP
)",
[],
)?;
// 创建服装分析表
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)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_projects_created_at ON projects (created_at)",
[],
)?;
// 创建素材表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_materials_project_id ON materials (project_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_materials_md5_hash ON materials (md5_hash)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_materials_type ON materials (material_type)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_materials_model_id ON materials (model_id)",
[],
)?;
// 创建模板表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_templates_import_status ON templates (import_status)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_templates_created_at ON templates (created_at)",
[],
)?;
// 创建项目-模板绑定表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_project_template_bindings_project_id ON project_template_bindings (project_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_project_template_bindings_template_id ON project_template_bindings (template_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_project_template_bindings_binding_type ON project_template_bindings (binding_type)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_project_template_bindings_priority ON project_template_bindings (priority)",
[],
)?;
// 创建模板匹配结果表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_template_matching_results_project_id ON template_matching_results (project_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_template_matching_results_template_id ON template_matching_results (template_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_template_matching_results_binding_id ON template_matching_results (binding_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_template_matching_results_status ON template_matching_results (status)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_template_matching_results_created_at ON template_matching_results (created_at)",
[],
)?;
// 创建匹配片段结果表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_matching_segment_results_matching_result_id ON matching_segment_results (matching_result_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_matching_segment_results_material_id ON matching_segment_results (material_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_matching_segment_results_model_id ON matching_segment_results (model_id)",
[],
)?;
// 创建匹配失败片段结果表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_matching_failed_segment_results_matching_result_id ON matching_failed_segment_results (matching_result_id)",
[],
)?;
// 创建导出记录表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_export_records_matching_result_id ON export_records (matching_result_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_export_records_project_id ON export_records (project_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_export_records_template_id ON export_records (template_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_export_records_export_type ON export_records (export_type)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_export_records_export_status ON export_records (export_status)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_export_records_created_at ON export_records (created_at)",
[],
)?;
// 创建服装分析表索引
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",
[],
);
let _ = conn.execute(
"ALTER TABLE template_materials ADD COLUMN upload_success BOOLEAN DEFAULT FALSE",
[],
);
// 为素材表添加模特ID字段
let _ = conn.execute(
"ALTER TABLE materials ADD COLUMN model_id TEXT",
[],
);
// 为素材表添加模特ID索引
let _ = conn.execute(
"CREATE INDEX IF NOT EXISTS idx_materials_model_id ON materials (model_id)",
[],
);
// 检查是否需要移除模板表的 project_id 字段
// 只有在表中存在 project_id 字段时才需要重建表
let has_project_id_column = conn.prepare("SELECT project_id FROM templates LIMIT 1").is_ok();
if has_project_id_column {
println!("Migrating templates table to remove project_id column...");
// SQLite 不支持 DROP COLUMN需要重建表
let _ = conn.execute(
"CREATE TABLE IF NOT EXISTS templates_new (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
canvas_width INTEGER NOT NULL,
canvas_height INTEGER NOT NULL,
canvas_ratio TEXT NOT NULL,
duration INTEGER NOT NULL,
fps REAL NOT NULL,
import_status TEXT NOT NULL DEFAULT 'Pending',
source_file_path TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT 1
)",
[],
);
// 迁移数据(排除 project_id
let _ = conn.execute(
"INSERT OR IGNORE INTO templates_new
SELECT id, name, description, canvas_width, canvas_height, canvas_ratio,
duration, fps, import_status, source_file_path, created_at, updated_at, is_active
FROM templates",
[],
);
// 删除旧表并重命名新表
let _ = conn.execute("DROP TABLE IF EXISTS templates_old", []);
let _ = conn.execute("ALTER TABLE templates RENAME TO templates_old", []);
let _ = conn.execute("ALTER TABLE templates_new RENAME TO templates", []);
println!("Templates table migration completed");
}
// 检查是否需要修复轨道片段表的外键约束问题
// 只有在表结构需要更新时才重建表
let needs_track_segments_migration = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='track_segments_old'").is_err();
if needs_track_segments_migration {
println!("Migrating track_segments table structure...");
// 重建 track_segments 表,恢复正确的 template_material_id 外键约束
let _ = conn.execute(
"CREATE TABLE IF NOT EXISTS track_segments_new (
id TEXT PRIMARY KEY,
track_id TEXT NOT NULL,
template_material_id TEXT,
name TEXT NOT NULL,
start_time INTEGER NOT NULL,
end_time INTEGER NOT NULL,
duration INTEGER NOT NULL,
segment_index INTEGER NOT NULL,
properties TEXT,
matching_rule TEXT DEFAULT '\"FixedMaterial\"',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (track_id) REFERENCES tracks (id) ON DELETE CASCADE,
FOREIGN KEY (template_material_id) REFERENCES template_materials (id) ON DELETE SET NULL
)",
[],
);
// 迁移轨道片段数据
// 首先检查旧表是否有 matching_rule 字段
let has_matching_rule = conn.prepare("SELECT matching_rule FROM track_segments LIMIT 1").is_ok();
if has_matching_rule {
// 如果有 matching_rule 字段,包含它在迁移中
let _ = conn.execute(
"INSERT OR IGNORE INTO track_segments_new
SELECT id, track_id, template_material_id, name, start_time, end_time,
duration, segment_index, properties, matching_rule, created_at, updated_at
FROM track_segments",
[],
);
} else {
// 如果没有 matching_rule 字段,使用默认值
let _ = conn.execute(
"INSERT OR IGNORE INTO track_segments_new
SELECT id, track_id, template_material_id, name, start_time, end_time,
duration, segment_index, properties, '\"FixedMaterial\"', created_at, updated_at
FROM track_segments",
[],
);
}
// 删除旧表并重命名新表
let _ = conn.execute("DROP TABLE IF EXISTS track_segments_old", []);
let _ = conn.execute("ALTER TABLE track_segments RENAME TO track_segments_old", []);
let _ = conn.execute("ALTER TABLE track_segments_new RENAME TO track_segments", []);
println!("Track segments table migration completed");
}
// 创建模板素材表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_template_materials_template_id ON template_materials (template_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_template_materials_upload_status ON template_materials (upload_status)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_template_materials_original_id ON template_materials (original_id)",
[],
)?;
// 创建轨道表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_tracks_template_id ON tracks (template_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_tracks_track_index ON tracks (track_index)",
[],
)?;
// 创建轨道片段表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_track_segments_track_id ON track_segments (track_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_track_segments_material_id ON track_segments (template_material_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_track_segments_segment_index ON track_segments (segment_index)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_materials_status ON materials (processing_status)",
[],
)?;
// 创建素材片段表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_segments_material_id ON material_segments (material_id)",
[],
)?;
// 创建模特表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_models_name ON models (name)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_models_gender ON models (gender)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_models_status ON models (status)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_models_rating ON models (rating)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_models_created_at ON models (created_at)",
[],
)?;
// 创建模特照片表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_model_photos_model_id ON model_photos (model_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_model_photos_type ON model_photos (photo_type)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_model_photos_cover ON model_photos (is_cover)",
[],
)?;
// 创建视频分类记录表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_video_classification_records_segment_id ON video_classification_records (segment_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_video_classification_records_material_id ON video_classification_records (material_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_video_classification_records_project_id ON video_classification_records (project_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_video_classification_records_category ON video_classification_records (category)",
[],
)?;
// 创建视频分类任务表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_video_classification_tasks_segment_id ON video_classification_tasks (segment_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_video_classification_tasks_material_id ON video_classification_tasks (material_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_video_classification_tasks_project_id ON video_classification_tasks (project_id)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_video_classification_tasks_status ON video_classification_tasks (status)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_video_classification_tasks_priority ON video_classification_tasks (priority)",
[],
)?;
// 创建AI分类表索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_ai_classifications_name ON ai_classifications (name)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_ai_classifications_active ON ai_classifications (is_active)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_ai_classifications_sort_order ON ai_classifications (sort_order)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_ai_classifications_created_at ON ai_classifications (created_at)",
[],
)?;
// 修复AI分类表的is_active字段类型从BOOLEAN改为INTEGER
// 检查表是否存在且字段类型不正确
let table_info: Result<Vec<(i32, String, String, bool, Option<String>, bool)>, rusqlite::Error> =
conn.prepare("PRAGMA table_info(ai_classifications)")?
.query_map([], |row| {
Ok((
row.get::<_, i32>(0)?, // cid
row.get::<_, String>(1)?, // name
row.get::<_, String>(2)?, // type
row.get::<_, bool>(3)?, // notnull
row.get::<_, Option<String>>(4)?, // dflt_value
row.get::<_, bool>(5)?, // pk
))
})?
.collect();
if let Ok(columns) = table_info {
// 检查is_active字段是否为BOOLEAN类型
let has_boolean_is_active = columns.iter().any(|(_, name, type_name, _, _, _)| {
name == "is_active" && type_name.to_uppercase() == "BOOLEAN"
});
if has_boolean_is_active {
println!("检测到is_active字段为BOOLEAN类型开始修复...");
// 创建新表
conn.execute(
"CREATE TABLE ai_classifications_new (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
prompt_text TEXT NOT NULL,
description TEXT,
is_active INTEGER DEFAULT 1,
sort_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)",
[],
)?;
// 复制数据将BOOLEAN转换为INTEGER
conn.execute(
"INSERT INTO ai_classifications_new
SELECT id, name, prompt_text, description,
CASE WHEN is_active THEN 1 ELSE 0 END as is_active,
sort_order, created_at, updated_at
FROM ai_classifications",
[],
)?;
// 删除旧表
conn.execute("DROP TABLE ai_classifications", [])?;
// 重命名新表
conn.execute("ALTER TABLE ai_classifications_new RENAME TO ai_classifications", [])?;
// 重新创建索引
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_ai_classifications_name ON ai_classifications (name)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_ai_classifications_active ON ai_classifications (is_active)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_ai_classifications_sort_order ON ai_classifications (sort_order)",
[],
)?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_ai_classifications_created_at ON ai_classifications (created_at)",
[],
)?;
println!("AI分类表结构修复完成");
}
}
Ok(())
}
/// 运行数据库迁移
fn run_migrations(&self) -> Result<()> {
let conn = self.connection.lock().unwrap();
// 修复 is_active 字段的数据类型问题
println!("Running database migrations...");
// 检查是否需要迁移(如果有字符串类型的 is_active 值)
let needs_migration: i64 = conn.query_row(
"SELECT COUNT(*) FROM projects WHERE is_active = 'true' OR is_active = 'false'",
[],
|row| row.get(0)
).unwrap_or(0);
if needs_migration > 0 {
println!("Found {} rows that need migration", needs_migration);
// 将 "true" 字符串转换为 1
let updated_rows = conn.execute(
"UPDATE projects SET is_active = 1 WHERE is_active = 'true'",
[],
)?;
if updated_rows > 0 {
println!("Updated {} rows: converted 'true' to 1", updated_rows);
}
// 将 "false" 字符串转换为 0
let updated_rows = conn.execute(
"UPDATE projects SET is_active = 0 WHERE is_active = 'false'",
[],
)?;
if updated_rows > 0 {
println!("Updated {} rows: converted 'false' to 0", updated_rows);
}
} else {
// 如果所有项目的 is_active 都是 0可能是之前的迁移错误恢复为 1
let all_inactive: i64 = conn.query_row(
"SELECT COUNT(*) FROM projects WHERE is_active = 0",
[],
|row| row.get(0)
).unwrap_or(0);
let total_projects: i64 = conn.query_row(
"SELECT COUNT(*) FROM projects",
[],
|row| row.get(0)
).unwrap_or(0);
if all_inactive == total_projects && total_projects > 0 {
println!("All {} projects are inactive, restoring to active state", total_projects);
let updated_rows = conn.execute(
"UPDATE projects SET is_active = 1",
[],
)?;
println!("Restored {} projects to active state", updated_rows);
}
}
// 添加模特关联字段到素材表
let has_model_id_column = conn.prepare("SELECT model_id FROM materials LIMIT 1").is_ok();
if !has_model_id_column {
println!("Adding model_id column to materials table");
match conn.execute(
"ALTER TABLE materials ADD COLUMN model_id TEXT",
[],
) {
Ok(_) => {
println!("Successfully added model_id column to materials table");
// 创建模特关联索引
match conn.execute(
"CREATE INDEX IF NOT EXISTS idx_materials_model_id ON materials (model_id)",
[],
) {
Ok(_) => println!("Successfully created index on model_id column"),
Err(e) => println!("Warning: Failed to create index on model_id column: {}", e),
}
}
Err(e) => {
println!("Error adding model_id column to materials table: {}", e);
return Err(e.into());
}
}
} else {
println!("model_id column already exists in materials table");
}
// 添加片段匹配规则字段到轨道片段表
let has_matching_rule_column = conn.prepare("SELECT matching_rule FROM track_segments LIMIT 1").is_ok();
if !has_matching_rule_column {
println!("Adding matching_rule column to track_segments table");
conn.execute(
"ALTER TABLE track_segments ADD COLUMN matching_rule TEXT DEFAULT '\"FixedMaterial\"'",
[],
)?;
println!("Added matching_rule column to track_segments table");
}
// 添加缩略图路径字段到素材片段表
let has_thumbnail_path_column = conn.prepare("SELECT thumbnail_path FROM material_segments LIMIT 1").is_ok();
if !has_thumbnail_path_column {
println!("Adding thumbnail_path column to material_segments table");
conn.execute(
"ALTER TABLE material_segments ADD COLUMN thumbnail_path TEXT",
[],
)?;
println!("Added thumbnail_path column to material_segments table");
}
// 添加缩略图路径字段到素材表
let has_material_thumbnail_path_column = conn.prepare("SELECT thumbnail_path FROM materials LIMIT 1").is_ok();
if !has_material_thumbnail_path_column {
println!("Adding thumbnail_path column to materials table");
conn.execute(
"ALTER TABLE materials ADD COLUMN thumbnail_path TEXT",
[],
)?;
println!("Added thumbnail_path column to materials table");
}
// 添加素材使用状态字段到素材片段表
let has_usage_count_column = conn.prepare("SELECT usage_count FROM material_segments LIMIT 1").is_ok();
if !has_usage_count_column {
println!("Adding usage tracking columns to material_segments table");
conn.execute(
"ALTER TABLE material_segments ADD COLUMN usage_count INTEGER DEFAULT 0",
[],
)?;
conn.execute(
"ALTER TABLE material_segments ADD COLUMN is_used BOOLEAN DEFAULT 0",
[],
)?;
conn.execute(
"ALTER TABLE material_segments ADD COLUMN last_used_at DATETIME",
[],
)?;
println!("Added usage tracking columns to material_segments table");
}
// 添加导出次数字段到模板匹配结果表
let has_export_count_column = conn.prepare("SELECT export_count FROM template_matching_results LIMIT 1").is_ok();
if !has_export_count_column {
println!("Adding export_count column to template_matching_results table");
conn.execute(
"ALTER TABLE template_matching_results ADD COLUMN export_count INTEGER DEFAULT 0",
[],
)?;
println!("Added export_count column to template_matching_results table");
}
// 暂时禁用自动清理,避免启动时卡住
// self.cleanup_invalid_projects()?;
println!("Database migrations completed");
Ok(())
}
/// 获取数据库文件路径
/// 遵循安全存储原则,将数据库存储在应用数据目录
fn get_database_path() -> PathBuf {
// 优先使用应用数据目录
if let Some(data_dir) = dirs::data_dir() {
let app_dir = data_dir.join("mixvideo");
// 确保目录存在
if let Err(e) = std::fs::create_dir_all(&app_dir) {
eprintln!("Failed to create app data directory: {}", e);
// 如果创建失败,使用当前目录
return PathBuf::from("mixvideo.db");
}
app_dir.join("mixvideo.db")
} else {
// 备用方案:使用当前目录
PathBuf::from("mixvideo.db")
}
}
/// 获取数据库路径的调试信息
pub fn get_database_path_info() -> String {
let path = Self::get_database_path();
format!("Database path: {}", path.display())
}
}