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