|
|
@@ -2064,3 +2064,124 @@ async def import_images_from_dir(params:ImportDirs):
|
|
|
except Exception as e:
|
|
|
logger.error(f"API 调用异常: {str(e)}")
|
|
|
raise UnicornException(f"{str(e)}")
|
|
|
+
|
|
|
+
|
|
|
+def auto_add_missing_columns():
|
|
|
+ """
|
|
|
+ 自动检测并添加缺失的数据库字段(打包兼容版本)
|
|
|
+ - 使用原生 SQLite,避免 SQLAlchemy 在打包环境中的问题
|
|
|
+ - 简化字段类型处理,避免复杂类型导致的问题
|
|
|
+ - 只添加新字段,不会修改或删除现有字段
|
|
|
+ - 不会丢失任何数据
|
|
|
+ - 可以安全地重复执行(幂等性)
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ import sqlite3
|
|
|
+
|
|
|
+ # 获取数据库文件路径
|
|
|
+ db_path = sqlite_file_name.replace("sqlite:///", "")
|
|
|
+
|
|
|
+ # 直接连接 SQLite 数据库
|
|
|
+ conn = sqlite3.connect(db_path)
|
|
|
+ cursor = conn.cursor()
|
|
|
+
|
|
|
+ # 定义模型和表名的映射
|
|
|
+ models_tables = [
|
|
|
+ (DeviceConfig, "device_config"),
|
|
|
+ (PhotoRecord, "photo_record"),
|
|
|
+ (DeviceConfigTabs, "device_config_tabs"),
|
|
|
+ (SysConfigs, "sys_configs"),
|
|
|
+ ]
|
|
|
+
|
|
|
+ for model_class, table_name in models_tables:
|
|
|
+ try:
|
|
|
+ # 检查表是否存在
|
|
|
+ cursor.execute(
|
|
|
+ f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table_name}'")
|
|
|
+ if not cursor.fetchone():
|
|
|
+ print(f"⚠️ 表 {table_name} 不存在,跳过")
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 获取现有列
|
|
|
+ cursor.execute(f"PRAGMA table_info({table_name})")
|
|
|
+ existing_columns = {row[1] for row in cursor.fetchall()}
|
|
|
+ print(f"📋 表 {table_name} 现有字段: {existing_columns}")
|
|
|
+
|
|
|
+ # 从模型中获取所有字段
|
|
|
+ model_columns = model_class.__table__.columns
|
|
|
+
|
|
|
+ for column_name, column_obj in model_columns.items():
|
|
|
+ # 如果列已存在,跳过
|
|
|
+ if column_name in existing_columns:
|
|
|
+ continue
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 简化类型映射 - 使用 SQLite 原生类型
|
|
|
+ column_type_str = str(column_obj.type).upper()
|
|
|
+
|
|
|
+ # 映射 SQLAlchemy 类型到 SQLite 类型
|
|
|
+ if 'INTEGER' in column_type_str or 'INT' in column_type_str:
|
|
|
+ sqlite_type = 'INTEGER'
|
|
|
+ elif 'FLOAT' in column_type_str or 'DOUBLE' in column_type_str or 'DECIMAL' in column_type_str or 'REAL' in column_type_str:
|
|
|
+ sqlite_type = 'REAL'
|
|
|
+ elif 'BOOLEAN' in column_type_str or 'BOOL' in column_type_str:
|
|
|
+ sqlite_type = 'INTEGER' # SQLite 用 0/1 表示布尔
|
|
|
+ elif 'DATETIME' in column_type_str or 'TIMESTAMP' in column_type_str:
|
|
|
+ sqlite_type = 'TEXT' # SQLite 用 TEXT 存储时间
|
|
|
+ elif 'VARCHAR' in column_type_str or 'CHAR' in column_type_str or 'STRING' in column_type_str or 'TEXT' in column_type_str:
|
|
|
+ sqlite_type = 'TEXT'
|
|
|
+ else:
|
|
|
+ sqlite_type = 'TEXT' # 默认使用 TEXT
|
|
|
+
|
|
|
+ # 处理 NULL/NOT NULL
|
|
|
+ nullable_str = "" if column_obj.nullable else "NOT NULL"
|
|
|
+
|
|
|
+ # 简化默认值处理
|
|
|
+ default_str = ""
|
|
|
+ if column_obj.default is not None:
|
|
|
+ default_value = column_obj.default.arg
|
|
|
+ if callable(default_value):
|
|
|
+ # 跳过函数默认值
|
|
|
+ default_str = ""
|
|
|
+ elif isinstance(default_value, bool):
|
|
|
+ default_str = f"DEFAULT {int(default_value)}"
|
|
|
+ elif isinstance(default_value, (int, float)):
|
|
|
+ default_str = f"DEFAULT {default_value}"
|
|
|
+ elif isinstance(default_value, str):
|
|
|
+ default_str = f"DEFAULT '{default_value}'"
|
|
|
+
|
|
|
+ # 构建 ALTER TABLE 语句
|
|
|
+ parts = [
|
|
|
+ f"ALTER TABLE {table_name}",
|
|
|
+ f"ADD COLUMN {column_name}",
|
|
|
+ sqlite_type,
|
|
|
+ ]
|
|
|
+ if nullable_str:
|
|
|
+ parts.append(nullable_str)
|
|
|
+ if default_str:
|
|
|
+ parts.append(default_str)
|
|
|
+
|
|
|
+ sql = " ".join(parts)
|
|
|
+
|
|
|
+ cursor.execute(sql)
|
|
|
+ print(
|
|
|
+ f"✅ 成功添加字段: {table_name}.{column_name} ({sqlite_type})")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"⚠️ 添加字段失败 {table_name}.{column_name}: {e}")
|
|
|
+ # 继续处理下一个字段
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"⚠️ 处理表 {table_name} 时出错: {e}")
|
|
|
+ import traceback
|
|
|
+ traceback.print_exc()
|
|
|
+
|
|
|
+ conn.commit()
|
|
|
+ conn.close()
|
|
|
+ print("🎉 数据库迁移检查完成")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"⚠️ 数据库迁移整体失败: {e}")
|
|
|
+ import traceback
|
|
|
+ traceback.print_exc()
|
|
|
+ # 不抛出异常,允许程序继续运行
|