2025-09-17 03:59:51 +08:00
|
|
|
|
"""
|
|
|
|
|
|
数据库迁移系统
|
|
|
|
|
|
"""
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
import os
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
import sqlite3
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
from src.utils import logger
|
2025-10-13 15:08:54 +08:00
|
|
|
|
from src.utils.datetime_utils import shanghai_now
|
2025-09-17 03:59:51 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DatabaseMigrator:
|
|
|
|
|
|
"""数据库迁移器"""
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
def __init__(self, db_path: str):
|
|
|
|
|
|
self.db_path = db_path
|
|
|
|
|
|
self.backup_dir = os.path.join(os.path.dirname(db_path), "backups")
|
|
|
|
|
|
self.migration_version_key = "migration_version"
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
def ensure_backup_dir(self):
|
|
|
|
|
|
"""确保备份目录存在"""
|
|
|
|
|
|
Path(self.backup_dir).mkdir(parents=True, exist_ok=True)
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
def backup_database(self) -> str:
|
|
|
|
|
|
"""备份数据库文件"""
|
|
|
|
|
|
if not os.path.exists(self.db_path):
|
|
|
|
|
|
logger.info("数据库文件不存在,无需备份")
|
|
|
|
|
|
return ""
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
self.ensure_backup_dir()
|
2025-10-13 15:08:54 +08:00
|
|
|
|
timestamp = shanghai_now().strftime("%Y%m%d_%H%M%S")
|
2025-09-17 03:59:51 +08:00
|
|
|
|
backup_filename = f"server_backup_{timestamp}.db"
|
|
|
|
|
|
backup_path = os.path.join(self.backup_dir, backup_filename)
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
try:
|
|
|
|
|
|
shutil.copy2(self.db_path, backup_path)
|
|
|
|
|
|
logger.info(f"数据库已备份到: {backup_path}")
|
|
|
|
|
|
return backup_path
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"数据库备份失败: {e}")
|
|
|
|
|
|
raise
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
def get_current_version(self) -> int:
|
|
|
|
|
|
"""获取当前数据库版本"""
|
|
|
|
|
|
if not os.path.exists(self.db_path):
|
|
|
|
|
|
return 0
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
try:
|
|
|
|
|
|
conn = sqlite3.connect(self.db_path)
|
|
|
|
|
|
cursor = conn.cursor()
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
# 检查版本表是否存在
|
|
|
|
|
|
cursor.execute("""
|
2025-09-18 20:32:33 +08:00
|
|
|
|
SELECT name FROM sqlite_master
|
|
|
|
|
|
WHERE type='table' AND name='migration_versions' """)
|
|
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
if not cursor.fetchone():
|
|
|
|
|
|
# 版本表不存在,检查是否为旧版本数据库
|
|
|
|
|
|
cursor.execute("""
|
2025-09-18 20:32:33 +08:00
|
|
|
|
SELECT name FROM sqlite_master
|
2025-09-17 03:59:51 +08:00
|
|
|
|
WHERE type='table' AND name='users'
|
|
|
|
|
|
""")
|
|
|
|
|
|
if cursor.fetchone():
|
|
|
|
|
|
# 用户表存在但版本表不存在,说明是旧版本
|
|
|
|
|
|
return 0
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 全新数据库
|
|
|
|
|
|
return 0
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
# 获取当前版本
|
|
|
|
|
|
cursor.execute("SELECT version FROM migration_versions ORDER BY version DESC LIMIT 1")
|
|
|
|
|
|
result = cursor.fetchone()
|
|
|
|
|
|
return result[0] if result else 0
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"获取数据库版本失败: {e}")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
finally:
|
2025-09-18 20:32:33 +08:00
|
|
|
|
if "conn" in locals():
|
2025-09-17 03:59:51 +08:00
|
|
|
|
conn.close()
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
def set_version(self, version: int):
|
|
|
|
|
|
"""设置数据库版本"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
conn = sqlite3.connect(self.db_path)
|
|
|
|
|
|
cursor = conn.cursor()
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
# 创建版本表
|
|
|
|
|
|
cursor.execute("""
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS migration_versions (
|
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
|
version INTEGER NOT NULL,
|
|
|
|
|
|
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
|
|
description TEXT
|
|
|
|
|
|
)
|
|
|
|
|
|
""")
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
# 插入版本记录
|
2025-09-18 20:32:33 +08:00
|
|
|
|
cursor.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO migration_versions (version, description)
|
2025-09-17 03:59:51 +08:00
|
|
|
|
VALUES (?, ?)
|
2025-09-18 20:32:33 +08:00
|
|
|
|
""",
|
|
|
|
|
|
(version, f"Migration to version {version}"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
conn.commit()
|
|
|
|
|
|
logger.info(f"数据库版本设置为: {version}")
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"设置数据库版本失败: {e}")
|
|
|
|
|
|
raise
|
|
|
|
|
|
finally:
|
2025-09-18 20:32:33 +08:00
|
|
|
|
if "conn" in locals():
|
2025-09-17 03:59:51 +08:00
|
|
|
|
conn.close()
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
|
|
|
|
|
def execute_migration(self, version: int, description: str, sql_commands: list[str]):
|
2025-09-17 03:59:51 +08:00
|
|
|
|
"""执行迁移"""
|
|
|
|
|
|
logger.info(f"执行迁移 v{version}: {description}")
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
try:
|
|
|
|
|
|
conn = sqlite3.connect(self.db_path)
|
|
|
|
|
|
cursor = conn.cursor()
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
# 执行迁移SQL命令
|
|
|
|
|
|
for sql in sql_commands:
|
|
|
|
|
|
if sql.strip(): # 跳过空命令
|
|
|
|
|
|
logger.info(f"执行SQL: {sql}")
|
|
|
|
|
|
cursor.execute(sql)
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
conn.commit()
|
|
|
|
|
|
logger.info(f"迁移 v{version} 执行成功")
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"迁移 v{version} 执行失败: {e}")
|
|
|
|
|
|
raise
|
|
|
|
|
|
finally:
|
2025-09-18 20:32:33 +08:00
|
|
|
|
if "conn" in locals():
|
2025-09-17 03:59:51 +08:00
|
|
|
|
conn.close()
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
def check_column_exists(self, table_name: str, column_name: str) -> bool:
|
|
|
|
|
|
"""检查列是否存在"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
conn = sqlite3.connect(self.db_path)
|
|
|
|
|
|
cursor = conn.cursor()
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
cursor.execute(f"PRAGMA table_info({table_name})")
|
|
|
|
|
|
columns = [column[1] for column in cursor.fetchall()]
|
|
|
|
|
|
return column_name in columns
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
except Exception:
|
|
|
|
|
|
return False
|
|
|
|
|
|
finally:
|
2025-09-18 20:32:33 +08:00
|
|
|
|
if "conn" in locals():
|
2025-09-17 03:59:51 +08:00
|
|
|
|
conn.close()
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2026-01-18 22:54:43 +08:00
|
|
|
|
def check_table_exists(self, table_name: str) -> bool:
|
|
|
|
|
|
"""检查表是否存在"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
conn = sqlite3.connect(self.db_path)
|
|
|
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
|
|
|
|
|
|
cursor.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
SELECT name FROM sqlite_master
|
|
|
|
|
|
WHERE type='table' AND name=?
|
|
|
|
|
|
""",
|
|
|
|
|
|
(table_name,),
|
|
|
|
|
|
)
|
|
|
|
|
|
return cursor.fetchone() is not None
|
|
|
|
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False
|
|
|
|
|
|
finally:
|
|
|
|
|
|
if "conn" in locals():
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
def run_migrations(self):
|
|
|
|
|
|
"""运行所有待执行的迁移"""
|
|
|
|
|
|
current_version = self.get_current_version()
|
|
|
|
|
|
latest_version = self.get_latest_migration_version()
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
# 如果数据库已存在但没有版本表,创建版本表并设置为最新版本
|
|
|
|
|
|
if current_version == 0 and latest_version > 0 and os.path.exists(self.db_path):
|
|
|
|
|
|
# 检查users表是否已有新字段,如果有,说明是通过SQLAlchemy创建的
|
2025-10-13 17:29:59 +08:00
|
|
|
|
required_columns = [
|
|
|
|
|
|
"login_failed_count",
|
|
|
|
|
|
"last_failed_login",
|
|
|
|
|
|
"login_locked_until",
|
|
|
|
|
|
"is_deleted",
|
|
|
|
|
|
"deleted_at",
|
|
|
|
|
|
]
|
|
|
|
|
|
if all(self.check_column_exists("users", column) for column in required_columns):
|
2025-09-17 03:59:51 +08:00
|
|
|
|
# 字段已存在,直接设置为最新版本
|
|
|
|
|
|
logger.info(f"检测到现有数据库已包含最新字段,设置版本为 v{latest_version}")
|
|
|
|
|
|
self.set_version(latest_version)
|
|
|
|
|
|
return
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
if current_version >= latest_version:
|
|
|
|
|
|
logger.info(f"数据库已是最新版本 v{current_version}")
|
|
|
|
|
|
return
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
logger.info(f"开始数据库迁移: v{current_version} -> v{latest_version}")
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
# 备份数据库
|
|
|
|
|
|
backup_path = self.backup_database()
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
try:
|
|
|
|
|
|
# 执行迁移
|
|
|
|
|
|
migrations = self.get_migrations()
|
|
|
|
|
|
has_executed_migrations = False
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
for version, description, sql_commands in migrations:
|
|
|
|
|
|
if version > current_version:
|
|
|
|
|
|
if sql_commands: # 只有当有SQL命令时才执行迁移
|
|
|
|
|
|
self.execute_migration(version, description, sql_commands)
|
|
|
|
|
|
has_executed_migrations = True
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.info(f"迁移 v{version}: {description} - 无需执行,字段已存在")
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
# 无论是否有SQL命令,都设置版本
|
|
|
|
|
|
self.set_version(version)
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
if has_executed_migrations:
|
|
|
|
|
|
logger.info("数据库迁移完成")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.info("数据库结构已是最新,仅更新版本记录")
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"数据库迁移失败: {e}")
|
|
|
|
|
|
if backup_path and os.path.exists(backup_path):
|
|
|
|
|
|
logger.info(f"尝试从备份恢复: {backup_path}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
shutil.copy2(backup_path, self.db_path)
|
|
|
|
|
|
logger.info("数据库已从备份恢复")
|
|
|
|
|
|
except Exception as restore_error:
|
|
|
|
|
|
logger.error(f"数据库恢复失败: {restore_error}")
|
|
|
|
|
|
raise
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
def get_latest_migration_version(self) -> int:
|
|
|
|
|
|
"""获取最新迁移版本号"""
|
2025-10-13 17:29:59 +08:00
|
|
|
|
migrations = self.get_migrations()
|
|
|
|
|
|
return max((version for version, _, _ in migrations), default=0)
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
|
|
|
|
|
def get_migrations(self) -> list[tuple[int, str, list[str]]]:
|
2025-09-17 03:59:51 +08:00
|
|
|
|
"""获取所有迁移定义
|
|
|
|
|
|
返回格式: [(version, description, [sql_commands])]
|
|
|
|
|
|
"""
|
|
|
|
|
|
migrations = []
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
# 迁移 v1: 为 users 表添加登录失败限制字段
|
|
|
|
|
|
# 使用条件检查来避免重复添加字段
|
|
|
|
|
|
v1_commands = []
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
# 检查并添加 login_failed_count 字段
|
2025-09-18 20:32:33 +08:00
|
|
|
|
if not self.check_column_exists("users", "login_failed_count"):
|
2025-09-17 03:59:51 +08:00
|
|
|
|
v1_commands.append("ALTER TABLE users ADD COLUMN login_failed_count INTEGER NOT NULL DEFAULT 0")
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
# 检查并添加 last_failed_login 字段
|
2025-09-18 20:32:33 +08:00
|
|
|
|
if not self.check_column_exists("users", "last_failed_login"):
|
2025-09-17 03:59:51 +08:00
|
|
|
|
v1_commands.append("ALTER TABLE users ADD COLUMN last_failed_login DATETIME")
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
# 检查并添加 login_locked_until 字段
|
2025-09-18 20:32:33 +08:00
|
|
|
|
if not self.check_column_exists("users", "login_locked_until"):
|
2025-09-17 03:59:51 +08:00
|
|
|
|
v1_commands.append("ALTER TABLE users ADD COLUMN login_locked_until DATETIME")
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-10-13 17:29:59 +08:00
|
|
|
|
migrations.append((1, "为用户表添加登录失败限制字段", v1_commands))
|
|
|
|
|
|
|
|
|
|
|
|
# 迁移 v2: 为 users 表添加软删除字段
|
|
|
|
|
|
v2_commands: list[str] = []
|
|
|
|
|
|
|
|
|
|
|
|
if not self.check_column_exists("users", "is_deleted"):
|
|
|
|
|
|
v2_commands.append("ALTER TABLE users ADD COLUMN is_deleted INTEGER NOT NULL DEFAULT 0")
|
|
|
|
|
|
|
|
|
|
|
|
if not self.check_column_exists("users", "deleted_at"):
|
|
|
|
|
|
v2_commands.append("ALTER TABLE users ADD COLUMN deleted_at DATETIME")
|
|
|
|
|
|
|
|
|
|
|
|
migrations.append((2, "为用户表添加软删除字段", v2_commands))
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-11-12 14:04:34 +08:00
|
|
|
|
# 迁移 v3: 为 messages 表添加多模态图片支持
|
|
|
|
|
|
v3_commands: list[str] = []
|
|
|
|
|
|
|
|
|
|
|
|
if not self.check_column_exists("messages", "image_content"):
|
|
|
|
|
|
v3_commands.append("ALTER TABLE messages ADD COLUMN image_content TEXT")
|
|
|
|
|
|
|
|
|
|
|
|
migrations.append((3, "为消息表添加多模态图片支持字段", v3_commands))
|
|
|
|
|
|
|
2026-01-18 22:54:43 +08:00
|
|
|
|
# 迁移 v4: 添加部门功能
|
|
|
|
|
|
v4_commands: list[str] = []
|
|
|
|
|
|
|
|
|
|
|
|
# 检查 departments 表是否存在
|
|
|
|
|
|
if not self.check_table_exists("departments"):
|
|
|
|
|
|
v4_commands.append("""
|
|
|
|
|
|
CREATE TABLE departments (
|
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
|
name VARCHAR(50) NOT NULL UNIQUE,
|
|
|
|
|
|
description VARCHAR(255),
|
|
|
|
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
|
)
|
|
|
|
|
|
""")
|
|
|
|
|
|
v4_commands.append("CREATE INDEX idx_departments_name ON departments(name)")
|
|
|
|
|
|
|
|
|
|
|
|
# 检查 users 表是否有 department_id 字段
|
|
|
|
|
|
if not self.check_column_exists("users", "department_id"):
|
|
|
|
|
|
v4_commands.append("ALTER TABLE users ADD COLUMN department_id INTEGER REFERENCES departments(id)")
|
|
|
|
|
|
|
|
|
|
|
|
v4_commands.append("CREATE INDEX idx_users_department_id ON users(department_id)")
|
|
|
|
|
|
|
|
|
|
|
|
migrations.append((4, "添加部门功能", v4_commands))
|
|
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
# 未来的迁移可以在这里添加
|
|
|
|
|
|
# migrations.append((
|
|
|
|
|
|
# 2,
|
|
|
|
|
|
# "添加新功能相关表",
|
|
|
|
|
|
# [
|
|
|
|
|
|
# "CREATE TABLE new_feature (...)",
|
|
|
|
|
|
# "ALTER TABLE existing_table ADD COLUMN new_field ..."
|
|
|
|
|
|
# ]
|
|
|
|
|
|
# ))
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
return migrations
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-09-21 13:33:09 +08:00
|
|
|
|
def validate_database_schema(db_path: str) -> tuple[bool, list[str]]:
|
|
|
|
|
|
"""验证数据库结构是否符合当前模型
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
tuple: (是否符合, 缺失的字段列表)
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not os.path.exists(db_path):
|
|
|
|
|
|
return False, ["数据库文件不存在"]
|
|
|
|
|
|
|
|
|
|
|
|
missing_fields = []
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
conn = sqlite3.connect(db_path)
|
|
|
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
|
|
|
|
|
|
# 检查users表必需字段
|
|
|
|
|
|
required_fields = {
|
2025-09-22 17:18:07 +08:00
|
|
|
|
"users": [
|
|
|
|
|
|
"id",
|
|
|
|
|
|
"username",
|
|
|
|
|
|
"user_id",
|
|
|
|
|
|
"phone_number",
|
|
|
|
|
|
"avatar",
|
|
|
|
|
|
"password_hash",
|
|
|
|
|
|
"role",
|
|
|
|
|
|
"created_at",
|
|
|
|
|
|
"last_login",
|
|
|
|
|
|
"login_failed_count",
|
|
|
|
|
|
"last_failed_login",
|
|
|
|
|
|
"login_locked_until",
|
2025-10-13 17:29:59 +08:00
|
|
|
|
"is_deleted",
|
|
|
|
|
|
"deleted_at",
|
2025-09-22 17:18:07 +08:00
|
|
|
|
],
|
|
|
|
|
|
"operation_logs": ["id", "user_id", "operation", "details", "ip_address", "timestamp"],
|
2025-11-12 14:04:34 +08:00
|
|
|
|
"messages": [
|
|
|
|
|
|
"id",
|
|
|
|
|
|
"conversation_id",
|
|
|
|
|
|
"role",
|
|
|
|
|
|
"content",
|
|
|
|
|
|
"message_type",
|
|
|
|
|
|
"created_at",
|
|
|
|
|
|
"token_count",
|
|
|
|
|
|
"extra_metadata",
|
|
|
|
|
|
"image_content",
|
|
|
|
|
|
],
|
2025-09-21 13:33:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for table_name, fields in required_fields.items():
|
|
|
|
|
|
# 检查表是否存在
|
2025-09-22 17:18:07 +08:00
|
|
|
|
cursor.execute(
|
|
|
|
|
|
"""
|
2025-09-21 13:33:09 +08:00
|
|
|
|
SELECT name FROM sqlite_master
|
|
|
|
|
|
WHERE type='table' AND name=?
|
2025-09-22 17:18:07 +08:00
|
|
|
|
""",
|
|
|
|
|
|
(table_name,),
|
|
|
|
|
|
)
|
2025-09-21 13:33:09 +08:00
|
|
|
|
|
|
|
|
|
|
if not cursor.fetchone():
|
|
|
|
|
|
missing_fields.append(f"表 {table_name} 不存在")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 检查字段是否存在
|
|
|
|
|
|
cursor.execute(f"PRAGMA table_info({table_name})")
|
|
|
|
|
|
existing_columns = [column[1] for column in cursor.fetchall()]
|
|
|
|
|
|
|
|
|
|
|
|
for field in fields:
|
|
|
|
|
|
if field not in existing_columns:
|
|
|
|
|
|
missing_fields.append(f"表 {table_name} 缺少字段 {field}")
|
|
|
|
|
|
|
|
|
|
|
|
return len(missing_fields) == 0, missing_fields
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"验证数据库结构失败: {e}")
|
|
|
|
|
|
return False, [f"验证失败: {str(e)}"]
|
|
|
|
|
|
finally:
|
2025-09-22 17:18:07 +08:00
|
|
|
|
if "conn" in locals():
|
2025-09-21 13:33:09 +08:00
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
def check_and_migrate(db_path: str):
|
|
|
|
|
|
"""检查并执行数据库迁移"""
|
2025-09-21 13:33:09 +08:00
|
|
|
|
# 先验证数据库结构
|
|
|
|
|
|
is_valid, issues = validate_database_schema(db_path)
|
|
|
|
|
|
|
|
|
|
|
|
if not is_valid:
|
|
|
|
|
|
logger.warning("数据库结构不符合当前设计:")
|
|
|
|
|
|
for issue in issues:
|
|
|
|
|
|
logger.warning(f" - {issue}")
|
|
|
|
|
|
|
|
|
|
|
|
if os.path.exists(db_path):
|
2025-10-14 01:46:43 +08:00
|
|
|
|
logger.info("建议运行迁移脚本: docker exec api-dev python /app/scripts/migrate_user_soft_delete.py")
|
2025-09-21 13:33:09 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
migrator = DatabaseMigrator(db_path)
|
2025-09-18 20:32:33 +08:00
|
|
|
|
|
2025-09-17 03:59:51 +08:00
|
|
|
|
try:
|
|
|
|
|
|
migrator.run_migrations()
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"数据库迁移过程中发生错误: {e}")
|
2025-09-18 20:32:33 +08:00
|
|
|
|
return False
|