|
|
@@ -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)}")
|