Explorar el Código

feat(db): 实现数据库字段自动添加功能

- 新增auto_add_missing_columns函数,自动检测并添加缺失的数据库字段
- 采用原生SQLite连接,避免SQLAlchemy在打包环境中的兼容性问题
- 只添加新字段,不修改或删除现有字段,保证数据安全及幂等性
- 针对多个表(device_config、photo_record等)支持字段自动添加
- 简化字段类型映射至SQLite原生类型,支持常见类型与默认值处理
- 在device_config表添加特定缺失字段,支持默认值设置
- 优化智能拍摄模块中ZMQ套接字超时设置,发送与接收超时调整为5000ms
- 调整相机连接检测超时时间为5秒,增强稳定性
- 修正相机连接状态异常处理,关闭套接字并终止上下文,避免资源泄露
- 注释和示例代码清理,提升代码可维护性
rambo hace 1 semana
padre
commit
2e0aef0596
Se han modificado 3 ficheros con 188 adiciones y 83 borrados
  1. 121 0
      python/api.py
  2. 45 64
      python/databases.py
  3. 22 19
      python/mcu/capture/smart_shooter_class.py

+ 121 - 0
python/api.py

@@ -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()
+        # 不抛出异常,允许程序继续运行

+ 45 - 64
python/databases.py

@@ -320,73 +320,54 @@ async def insert_photo_records(
 
 def auto_add_missing_columns():
     """
-    自动检测并添加缺失的数据库字段
-    - 只添加新字段,不会修改或删除现有字段
-    - 不会丢失任何数据
-    - 可以安全地重复执行(幂等性)
+    自动检测并添加缺失的数据库字段(最简化版本)
+    只为 device_config 表添加缺失的字段
     """
-    from sqlalchemy import inspect, text
-
-    inspector = inspect(engine)
-
-    # 定义每个模型类对应的表名和期望的列
-    models_tables = {
-        DeviceConfig: "device_config",
-        PhotoRecord: "Photo_record",
-    }
-
-    with engine.connect() as conn:
-        for model_class, table_name in models_tables.items():
-            # 检查表是否存在
-            if not inspector.has_table(table_name):
-                continue
-
-            # 获取数据库中现有的列
-            existing_columns = {
-                col["name"] for col in inspector.get_columns(table_name)
-            }
-
-            # 获取模型中定义的所有列
-            model_columns = model_class.__table__.columns
+    try:
+        import sqlite3
+        
+        # 数据库文件路径
+        db_path = "C:/Zhihuiyin/database.db"
+        
+        # 连接数据库
+        conn = sqlite3.connect(db_path)
+        cursor = conn.cursor()
+        
+        # 检查表是否存在
+        cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='device_config'")
+        if not cursor.fetchone():
+            conn.close()
+            return
+        
+        # 获取现有字段
+        cursor.execute("PRAGMA table_info(device_config)")
+        existing_columns = [row[1] for row in cursor.fetchall()]
+        
+        # 定义需要添加的字段
+        # 格式: (字段名, SQL类型, 默认值)
+        # 根据你在 DeviceConfig model 中新增的字段来配置
+        new_fields = [
+            # 请在这里添加你实际新增的字段
+            ("point_name", "VARCHAR", "DEFAULT 'A'"),
+            ("is_move_device", "BOOLEAN", "DEFAULT 1")
+        ]
+        
+        # 添加缺失的字段
+        for field_name, field_type, default_clause in new_fields:
+            if field_name not in existing_columns:
+                try:
+                    sql = f"ALTER TABLE device_config ADD COLUMN {field_name} {field_type} {default_clause}"
+                    cursor.execute(sql)
+                    conn.commit()
+                except Exception:
+                    pass  # 忽略错误,继续下一个
+        
+        conn.close()
+        
+    except Exception:
+        pass  # 静默失败,不影响启动
 
-            for column_name, column_obj in model_columns.items():
-                # 如果列已存在,跳过
-                if column_name in existing_columns:
-                    continue
 
-                try:
-                    # 构建列类型字符串
-                    column_type = str(column_obj.type.compile(engine.dialect))
-
-                    # 处理 NULL/NOT NULL
-                    nullable_str = "NULL" 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):
-                            # 如果是函数(如 func.now()),SQLite 不支持直接在 ALTER TABLE 中使用
-                            continue
-                        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 语句
-                    sql = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type} {nullable_str} {default_str}".strip()
-
-                    conn.execute(text(sql))
-                    print(f"✅ 成功添加字段: {table_name}.{column_name}")
-
-                except Exception as e:
-                    print(f"⚠️ 添加字段失败 {table_name}.{column_name}: {e}")
-
-        conn.commit()
-
-    print("🎉 数据库迁移检查完成")
 
 
 def SqlQuery():

+ 22 - 19
python/mcu/capture/smart_shooter_class.py

@@ -63,9 +63,9 @@ class SmartShooter(metaclass=SingletonType):
         listen_socket = context.socket(zmq.SUB)
         listen_socket.setsockopt(zmq.SUBSCRIBE, b"")
         # 设置发送超时为 5000 毫秒(5 秒)
-        listen_socket.setsockopt(zmq.RCVTIMEO, 4000)
+        listen_socket.setsockopt(zmq.RCVTIMEO, 5000)
         # 设置接收超时为 5000 毫秒(5 秒)
-        listen_socket.setsockopt(zmq.SNDTIMEO, 4000)
+        listen_socket.setsockopt(zmq.SNDTIMEO, 5000)
         listen_socket.setsockopt(zmq.LINGER, 0)  # 设置为 0 表示不等待未完成的操作
         listen_socket.connect(self.LISTEN_REQ)
         return listen_socket, context
@@ -144,7 +144,7 @@ class SmartShooter(metaclass=SingletonType):
         """
             实时获取相机信息,是否连接、软件是否被打开
             """
-        socket, context = self.__create_req(time_out=2)
+        socket, context = self.__create_req(time_out=5)
         try:
             req = {}
             req["msg_type"] = "Request"
@@ -234,24 +234,27 @@ class SmartShooter(metaclass=SingletonType):
                     "device_status": 2,
                 }
                 await self.sendMessageSocket(message)
-            # print("相机已连接状态信息---->", cameraInfo)
-            self.initConfigIsoSettings(
-                CameraLists=CameraLists, isMultCameraMode=isMultCameraMode
-            )
+            print("相机已连接状态信息---->", cameraInfo)
+            # self.initConfigIsoSettings(
+            #     CameraLists=CameraLists, isMultCameraMode=isMultCameraMode
+            # )
             return True, "相机已连接"
-        except zmq.Again:
-            print("获取相机信息超时,继续监听...")
+        except zmq.Again as e:
+            self.connect_status = False
+            socket.close()
+            context.term()
+            print("获取相机信息超时,继续监听...",e)
             msg_send =  f"请检查{CameraKey},相机是否连接" if CameraKey else f"请检查相机是否连接"
-            if is_send:
-                message = {
-                    "code": 1,
-                    "msg": msg_send,
-                    "data": None,
-                    "CameraKey":CameraKey,
-                    "msg_type": msg_type,
-                    "device_status": 2,
-                }
-                await self.sendMessageSocket(message)
+            # if is_send:
+            #     message = {
+            #         "code": 1,
+            #         "msg": msg_send,
+            #         "data": None,
+            #         "CameraKey":CameraKey,
+            #         "msg_type": msg_type,
+            #         "device_status": 2,
+            #     }
+            #     await self.sendMessageSocket(message)
             return False, msg_send
         except Exception as e:
             print("相机状态获取异常", e)