Files
mixvideo-v2/apps/desktop/src-tauri/src/infrastructure/database.rs
2025-07-15 18:10:54 +08:00

1007 lines
37 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};
/// 数据库管理器
/// 遵循 Tauri 开发规范的数据库设计模式
pub struct Database {
connection: Arc<Mutex<Connection>>,
}
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_path(db_path: &str) -> 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", "DELETE")?; // 使用 DELETE 模式而不是 WAL
connection.pragma_update(None, "synchronous", "FULL")?; // 确保数据立即写入磁盘
connection.pragma_update(None, "cache_size", "10000")?; // 增加缓存大小
println!("Database pragmas configured");
let database = Database {
connection: Arc::new(Mutex::new(connection)),
};
// 初始化数据库表
database.initialize_tables()?;
// 运行数据库迁移
database.run_migrations()?;
Ok(database)
}
/// 获取数据库连接
pub fn get_connection(&self) -> Arc<Mutex<Connection>> {
Arc::clone(&self.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(),
}
}
/// 执行数据库操作的辅助方法,自动处理锁的获取和释放
/// 这是推荐的数据库访问方式,可以避免锁竞争问题
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 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 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)",
[],
)?;
// 添加新字段(如果不存在)- 数据库迁移
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");
}
// 暂时禁用自动清理,避免启动时卡住
// 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())
}
}