浏览代码

```
feat(api): 添加图片目录批量导入功能

新增_import_images_from_dir API端点,支持遍历指定目录及其子目录,
将图片路径批量导入数据库。包含完整的错误处理、事务管理和
重复检查机制,防止相同货号的数据重复导入。

fix(smart_shooter): 优化相机信息获取延迟和设备状态码

调整GetCameraInfo方法中的异步延迟时间从0.01秒缩短至0.001秒,
提升响应速度;修正设备状态码从-1改为2以符合预期的状态标识。
```

rambo 1 月之前
父节点
当前提交
1c64b0fdf4
共有 2 个文件被更改,包括 141 次插入3 次删除
  1. 139 1
      python/api.py
  2. 2 2
      python/mcu/capture/smart_shooter_class.py

+ 139 - 1
python/api.py

@@ -1851,4 +1851,142 @@ def minimize_window(window_title: str):
         hwnd = hwnd_list[0]
         win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE)
         return {"code": 0, "msg": "最小化成功", "data": {"status": True}}
-    return {"code": 0, "msg": "最小化失败", "data": {"status": False}}
+    return {"code": 0, "msg": "最小化失败", "data": {"status": False}}
+
+
+def _sync_import_images_logic(dir_path: str, specific_goods_art_no: str = None):
+    """
+    同步执行的文件遍历和数据库插入逻辑
+    :param dir_path: 图片根目录
+    :param specific_goods_art_no: 指定货号。如果提供,所有图片归为此货号;如果为None,则尝试从子目录名获取货号
+    """
+    if not os.path.exists(dir_path):
+        raise FileNotFoundError(f"目录不存在: {dir_path}")
+    
+    session = SqlQuery()
+    photo_record_crud = CRUD(PhotoRecord)
+    
+    # 支持的图片扩展名
+    image_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp')
+    
+    success_count = 0
+    fail_count = 0
+    skipped_count = 0
+    
+    try:
+        # 1. 预检查:如果指定了货号,先检查数据库中是否已存在该货号的活跃记录
+        # 参考 message_handler 213-275 行的逻辑
+        # 如果没有指定货号,且我们假设根目录下的第一层子目录是货号,
+        # 这里很难在不遍历的情况下知道有哪些货号。
+        # 策略:如果 specific_goods_art_no 为空,我们在遍历每个子目录时单独检查。
+        
+        # 2. 遍历目录
+        # os.walk 会递归遍历所有子目录
+        for root, dirs, files in os.walk(dir_path):
+            
+            current_goods_art_no = specific_goods_art_no
+            
+            # 如果没有指定全局货号,尝试从相对路径提取货号
+            # 假设结构为: dir_path / goods_art_no / images...
+            if not current_goods_art_no:
+                relative_path = os.path.relpath(root, dir_path)
+                parts = relative_path.split(os.sep)
+                # 只有当 root 是 dir_path 的直接子目录时,才将其视为货号文件夹
+                # 如果 relative_path 是 ".",说明还在根目录,跳过或报错
+                if relative_path == ".":
+                    # 根目录下的文件可能没有货号,视业务逻辑而定,这里选择跳过或赋予默认值
+                    # 如果要求必须有货号,可以 continue 或 raise
+                    continue 
+                
+                # 取第一层目录名作为货号
+                current_goods_art_no = parts[0]
+                
+                # 针对当前提取到的货号进行存在性检查 (仅在该货号的第一层目录入口处检查一次效率更高,但为了简单放在这里)
+                # 优化:可以使用一个 set 记录已经检查过的货号,避免重复查询数据库
+                if not hasattr(_sync_import_images_logic, 'checked_nos'):
+                    _sync_import_images_logic.checked_nos = set()
+                
+                if current_goods_art_no and current_goods_art_no not in _sync_import_images_logic.checked_nos:
+                    existing_record = photo_record_crud.read(
+                        session, 
+                        conditions={"goods_art_no": current_goods_art_no, "delete_time": None}
+                    )
+                    if existing_record:
+                        raise UnicornException(f"货号 [{current_goods_art_no}] 已存在,请删除后重新导入~")
+                    _sync_import_images_logic.checked_nos.add(current_goods_art_no)
+
+            if not current_goods_art_no:
+                logger.warning(f"无法确定货号,跳过目录: {root}")
+                continue
+
+            for file in files:
+                if file.lower().endswith(image_extensions):
+                    full_path = os.path.join(root, file)
+                    
+                    try:
+                        # 可选:检查单张图片路径是否已存在,避免重复插入同一条记录
+                        # 如果业务允许同一个货号有多张图片,则不需要检查 image_path,只需要检查货号是否存在(上面已做)
+                        
+                        # 构造插入数据
+                        # 注意:PhotoRecord 模型可能需要 create_time, action_id 等其他字段
+                        # 这里根据最小可行性原则,只填入必填项,其他依赖数据库默认值或后续更新
+                        data_to_insert = {
+                            "image_path": full_path,
+                            "goods_art_no": current_goods_art_no,
+                            # 如果有 action_id 要求,可能需要查询默认 action 或设为 NULL
+                            # "action_id": None, 
+                            # "create_time": datetime.datetime.now(),
+                        }
+                        
+                        new_record = PhotoRecord(**data_to_insert)
+                        session.add(new_record)
+                        success_count += 1
+                        
+                    except Exception as e:
+                        logger.error(f"导入图片失败 {full_path}: {str(e)}")
+                        fail_count += 1
+        
+        # 提交事务
+        session.commit()
+        logger.info(f"导入完成: 成功 {success_count}, 失败 {fail_count}, 跳过 {skipped_count}")
+        
+        # 清理静态变量,防止内存泄漏或状态污染(如果是长期运行的服务)
+        if hasattr(_sync_import_images_logic, 'checked_nos'):
+            del _sync_import_images_logic.checked_nos
+
+        return {
+            "message": "导入完成",
+            "success": success_count,
+            "failed": fail_count,
+            "skipped": skipped_count
+        }
+        
+    except UnicornException:
+        # 重新抛出自定义异常,以便上层捕获
+        raise
+    except Exception as e:
+        session.rollback()
+        logger.error(f"导入过程发生严重错误: {str(e)}")
+        raise e
+    finally:
+        session.close()
+@app.post("/import-images-from-dir")
+async def import_images_from_dir():
+    """
+    遍历指定目录及其子目录,将图片路径导入数据库
+    """
+    dir_path = None
+    # 基本安全检查,防止路径遍历攻击或无效路径
+    if not os.path.isdir(dir_path):
+        raise HTTPException(status_code=400, detail=f"无效目录: {dir_path}")
+
+    try:
+        # 将阻塞的 IO 和 DB 操作放在线程池中执行
+        result  = _sync_import_images_logic(dir_path)
+        return {"code": 0, "msg": "操作成功", "data": result}
+    
+    except FileNotFoundError as e:
+        raise HTTPException(status_code=404, detail=str(e))
+    except Exception as e:
+        logger.error(f"API 调用异常: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}")

+ 2 - 2
python/mcu/capture/smart_shooter_class.py

@@ -112,7 +112,7 @@ class SmartShooter(metaclass=SingletonType):
             msg_send = "相机未连接或软件未打开"
             return False, msg_send
     async def GetCameraInfo(self, is_send=True, msg_type=""):
-        await asyncio.sleep(0.01)
+        await asyncio.sleep(0.001)
         self.msg_type = msg_type
         """
             实时获取相机信息,是否连接、软件是否被打开
@@ -200,7 +200,7 @@ class SmartShooter(metaclass=SingletonType):
                     "msg": msg_send,
                     "data": None,
                     "msg_type": self.msg_type,
-                    "device_status": -1,
+                    "device_status": 2,
                 }
                 await self.websocket_manager.send_personal_message(
                     message, self.websocket