fix: 修复项目数据持久化问题
问题修复: - 修复软件重新打开后项目数据丢失的问题 - 修复 is_active 字段数据类型不一致导致的查询问题 - 修复数据库事务提交和数据持久化问题 技术修复: - 改进数据库连接配置,使用 DELETE 模式确保数据立即写入 - 修复 is_active 字段的布尔值存储和读取逻辑 - 添加数据库迁移机制,自动修复历史数据 - 增强数据库路径管理,确保数据存储在正确位置 数据库优化: - 使用事务确保数据一致性 - 添加数据验证和错误处理 - 优化数据库 PRAGMA 设置提高可靠性 - 支持多种数据类型的兼容性读取 测试验证: - 验证项目创建后数据正确保存 - 验证应用重启后数据正确加载 - 验证数据库迁移正确执行 - 确保所有现有项目数据完整性
This commit is contained in:
parent
42c5dcef8e
commit
7b11ed04bd
|
|
@ -18,7 +18,13 @@ impl ProjectRepository {
|
||||||
/// 创建项目
|
/// 创建项目
|
||||||
pub fn create(&self, project: &Project) -> Result<()> {
|
pub fn create(&self, project: &Project) -> Result<()> {
|
||||||
let conn = self.connection.lock().unwrap();
|
let conn = self.connection.lock().unwrap();
|
||||||
conn.execute(
|
|
||||||
|
|
||||||
|
|
||||||
|
// 开始事务
|
||||||
|
let tx = conn.unchecked_transaction()?;
|
||||||
|
|
||||||
|
let result = tx.execute(
|
||||||
"INSERT INTO projects (id, name, path, description, created_at, updated_at, is_active)
|
"INSERT INTO projects (id, name, path, description, created_at, updated_at, is_active)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||||
[
|
[
|
||||||
|
|
@ -28,9 +34,15 @@ impl ProjectRepository {
|
||||||
project.description.as_deref().unwrap_or(""),
|
project.description.as_deref().unwrap_or(""),
|
||||||
project.created_at.to_rfc3339().as_str(),
|
project.created_at.to_rfc3339().as_str(),
|
||||||
project.updated_at.to_rfc3339().as_str(),
|
project.updated_at.to_rfc3339().as_str(),
|
||||||
project.is_active.to_string().as_str(),
|
if project.is_active { "1" } else { "0" },
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
tx.commit()?;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,6 +87,16 @@ impl ProjectRepository {
|
||||||
/// 获取所有活跃项目
|
/// 获取所有活跃项目
|
||||||
pub fn find_all_active(&self) -> Result<Vec<Project>> {
|
pub fn find_all_active(&self) -> Result<Vec<Project>> {
|
||||||
let conn = self.connection.lock().unwrap();
|
let conn = self.connection.lock().unwrap();
|
||||||
|
|
||||||
|
// 首先检查表是否存在
|
||||||
|
let table_exists: i64 = conn.query_row(
|
||||||
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='projects'",
|
||||||
|
[],
|
||||||
|
|row| row.get(0)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT id, name, path, description, created_at, updated_at, is_active
|
"SELECT id, name, path, description, created_at, updated_at, is_active
|
||||||
FROM projects WHERE is_active = 1 ORDER BY updated_at DESC"
|
FROM projects WHERE is_active = 1 ORDER BY updated_at DESC"
|
||||||
|
|
@ -89,6 +111,7 @@ impl ProjectRepository {
|
||||||
projects.push(project?);
|
projects.push(project?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Ok(projects)
|
Ok(projects)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,12 +160,19 @@ impl ProjectRepository {
|
||||||
fn row_to_project(&self, row: &Row) -> Result<Project> {
|
fn row_to_project(&self, row: &Row) -> Result<Project> {
|
||||||
let created_at_str: String = row.get(4)?;
|
let created_at_str: String = row.get(4)?;
|
||||||
let updated_at_str: String = row.get(5)?;
|
let updated_at_str: String = row.get(5)?;
|
||||||
let is_active_str: String = row.get(6)?;
|
|
||||||
|
// 尝试读取 is_active 字段,支持多种数据类型
|
||||||
|
let is_active = match row.get::<_, rusqlite::types::Value>(6)? {
|
||||||
|
rusqlite::types::Value::Integer(i) => i != 0,
|
||||||
|
rusqlite::types::Value::Text(s) => s == "1" || s.to_lowercase() == "true",
|
||||||
|
rusqlite::types::Value::Real(f) => f != 0.0,
|
||||||
|
_ => true, // 默认为 true
|
||||||
|
};
|
||||||
|
|
||||||
let created_at = DateTime::parse_from_rfc3339(&created_at_str)
|
let created_at = DateTime::parse_from_rfc3339(&created_at_str)
|
||||||
.map_err(|_| rusqlite::Error::InvalidColumnType(4, "created_at".to_string(), rusqlite::types::Type::Text))?
|
.map_err(|_| rusqlite::Error::InvalidColumnType(4, "created_at".to_string(), rusqlite::types::Type::Text))?
|
||||||
.with_timezone(&Utc);
|
.with_timezone(&Utc);
|
||||||
|
|
||||||
let updated_at = DateTime::parse_from_rfc3339(&updated_at_str)
|
let updated_at = DateTime::parse_from_rfc3339(&updated_at_str)
|
||||||
.map_err(|_| rusqlite::Error::InvalidColumnType(5, "updated_at".to_string(), rusqlite::types::Type::Text))?
|
.map_err(|_| rusqlite::Error::InvalidColumnType(5, "updated_at".to_string(), rusqlite::types::Type::Text))?
|
||||||
.with_timezone(&Utc);
|
.with_timezone(&Utc);
|
||||||
|
|
@ -157,7 +187,7 @@ impl ProjectRepository {
|
||||||
description,
|
description,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
is_active: is_active_str == "1" || is_active_str.to_lowercase() == "true",
|
is_active,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,21 +13,31 @@ impl Database {
|
||||||
/// 遵循安全第一原则,确保数据库文件的安全存储
|
/// 遵循安全第一原则,确保数据库文件的安全存储
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
let db_path = Self::get_database_path();
|
let db_path = Self::get_database_path();
|
||||||
|
|
||||||
|
// 打印数据库路径用于调试
|
||||||
|
println!("Initializing database at: {}", db_path.display());
|
||||||
|
|
||||||
// 确保数据库目录存在
|
// 确保数据库目录存在
|
||||||
if let Some(parent) = db_path.parent() {
|
if let Some(parent) = db_path.parent() {
|
||||||
std::fs::create_dir_all(parent).map_err(|e| {
|
std::fs::create_dir_all(parent).map_err(|e| {
|
||||||
|
eprintln!("Failed to create database directory: {}", e);
|
||||||
rusqlite::Error::SqliteFailure(
|
rusqlite::Error::SqliteFailure(
|
||||||
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CANTOPEN),
|
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CANTOPEN),
|
||||||
Some(format!("Failed to create database directory: {}", e)),
|
Some(format!("Failed to create database directory: {}", e)),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let connection = Connection::open(db_path)?;
|
let connection = Connection::open(&db_path)?;
|
||||||
|
println!("Database connection established successfully");
|
||||||
// 启用外键约束
|
|
||||||
|
// 配置数据库设置
|
||||||
connection.execute("PRAGMA foreign_keys = ON", [])?;
|
connection.execute("PRAGMA 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 {
|
let database = Database {
|
||||||
connection: Arc::new(Mutex::new(connection)),
|
connection: Arc::new(Mutex::new(connection)),
|
||||||
|
|
@ -35,6 +45,9 @@ impl Database {
|
||||||
|
|
||||||
// 初始化数据库表
|
// 初始化数据库表
|
||||||
database.initialize_tables()?;
|
database.initialize_tables()?;
|
||||||
|
|
||||||
|
// 运行数据库迁移
|
||||||
|
database.run_migrations()?;
|
||||||
|
|
||||||
Ok(database)
|
Ok(database)
|
||||||
}
|
}
|
||||||
|
|
@ -92,13 +105,92 @@ impl Database {
|
||||||
Ok(())
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Database migrations completed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取数据库文件路径
|
/// 获取数据库文件路径
|
||||||
/// 遵循安全存储原则,将数据库存储在应用数据目录
|
/// 遵循安全存储原则,将数据库存储在应用数据目录
|
||||||
fn get_database_path() -> PathBuf {
|
fn get_database_path() -> PathBuf {
|
||||||
|
// 优先使用应用数据目录
|
||||||
if let Some(data_dir) = dirs::data_dir() {
|
if let Some(data_dir) = dirs::data_dir() {
|
||||||
data_dir.join("mixvideo").join("mixvideo.db")
|
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 {
|
} else {
|
||||||
PathBuf::from(".").join("mixvideo.db")
|
// 备用方案:使用当前目录
|
||||||
|
PathBuf::from("mixvideo.db")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取数据库路径的调试信息
|
||||||
|
pub fn get_database_path_info() -> String {
|
||||||
|
let path = Self::get_database_path();
|
||||||
|
format!("Database path: {}", path.display())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,8 @@ pub fn run() {
|
||||||
commands::system_commands::select_directory,
|
commands::system_commands::select_directory,
|
||||||
commands::system_commands::get_app_info,
|
commands::system_commands::get_app_info,
|
||||||
commands::system_commands::validate_directory,
|
commands::system_commands::validate_directory,
|
||||||
commands::system_commands::get_directory_name
|
commands::system_commands::get_directory_name,
|
||||||
|
commands::system_commands::get_database_info
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
// 初始化应用状态
|
// 初始化应用状态
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,17 @@ pub async fn validate_directory(path: String) -> Result<bool, String> {
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn get_directory_name(path: String) -> Result<String, String> {
|
pub async fn get_directory_name(path: String) -> Result<String, String> {
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
let path = Path::new(&path);
|
let path = Path::new(&path);
|
||||||
match path.file_name() {
|
match path.file_name() {
|
||||||
Some(name) => Ok(name.to_string_lossy().to_string()),
|
Some(name) => Ok(name.to_string_lossy().to_string()),
|
||||||
None => Err("无效的目录路径".to_string()),
|
None => Err("无效的目录路径".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取数据库信息命令(调试用)
|
||||||
|
#[command]
|
||||||
|
pub async fn get_database_info() -> Result<String, String> {
|
||||||
|
use crate::infrastructure::database::Database;
|
||||||
|
Ok(Database::get_database_path_info())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue