Przeglądaj źródła

feat(image-processing): 优化图像处理主流程并增加重试机制

- 添加详细的函数文档说明处理流程和返回值结构
- 实现处理结果统计包括成功/失败货号列表和数量统计
- 增加重试机制对失败货号进行自动重试处理
- 添加目录完整性校验确保处理结果符合预期
- 优化错误处理和日志记录提高调试效率
- 修复回调函数中显示货号名称的bug
- 在图像生成过程中添加异常捕获和错误处理
- 移除未使用的异步代码实现同步处理逻辑
rambo 1 dzień temu
rodzic
commit
ca84d7db10
2 zmienionych plików z 373 dodań i 90 usunięć
  1. 77 41
      python/service/base_deal.py
  2. 296 49
      python/service/run_main.py

+ 77 - 41
python/service/base_deal.py

@@ -74,6 +74,20 @@ class BaseDealImage(object):
         logo_path=None,
         image_order_list=None,
     ):
+        """
+        执行主流程处理
+        
+        Returns:
+            dict: {
+                'success': bool,  # 是否全部成功
+                'successful_folders': list,  # 成功的货号列表
+                'failed_folders': list,  # 失败的货号列表
+                'successful_num': int,  # 成功数量
+                'error_num': int  # 失败数量
+            }
+        """
+        from logger import logger
+        
         # 对所有缺失已抠图的进行抠图处理
         self.run_cutout_image(
             all_goods_art_no_folder_data=all_goods_art_no_folder_data,
@@ -83,6 +97,9 @@ class BaseDealImage(object):
         )
         error_num = 0
         successful_num = 0
+        successful_folders = []
+        failed_folders = []
+        
         for goods_art_no_folder_data in all_goods_art_no_folder_data:
             if goods_art_no_folder_data["label"] != "待处理":
                 continue
@@ -90,7 +107,9 @@ class BaseDealImage(object):
                 if windows.state != 1:
                     break
             folder_name = goods_art_no_folder_data["folder_name"]
-            callback_func("开始处理文件夹==========  {} ".format(all_goods_art_no_folder_data))
+            callback_func("开始处理文件夹==========  {} ".format(folder_name))
+            
+            flag = None
             if settings.IS_TEST:
                 flag = self.shoes_run_one_folder_to_deal(
                     goods_art_no_folder_data=goods_art_no_folder_data,
@@ -100,15 +119,6 @@ class BaseDealImage(object):
                     callback_func=callback_func,
                     windows=windows,
                 )
-                if flag is None:
-                    callback_func("货号:{} 数据异常".format(folder_name))
-                else:
-                    if flag:
-                        successful_num += 1
-                        callback_func("货号:{} 图片生成处理成功".format(folder_name))
-                    else:
-                        error_num += 1
-                        callback_func("货号:{} 图片生成处理失败".format(folder_name))
             else:
                 try:
                     flag = self.shoes_run_one_folder_to_deal(
@@ -119,28 +129,48 @@ class BaseDealImage(object):
                         callback_func=callback_func,
                         windows=windows,
                     )
-                    if flag is None:
-                        callback_func("货号:{} 数据异常".format(folder_name))
-                    else:
-                        if flag:
-                            successful_num += 1
-                            callback_func(
-                                "货号:{} 图片生成处理成功".format(folder_name)
-                            )
-                        else:
-                            error_num += 1
-                            callback_func(
-                                "货号:{} 图片生成处理失败".format(folder_name)
-                            )
                 except BaseException as e:
                     error_num += 1
                     import traceback
-
                     traceback.print_exc()
+                    logger.error(f"货号 {folder_name} 处理异常: {e}")
                     callback_func(
                         "货号:{} 图片生成处理异常,原因:{}".format(folder_name, e)
                     )
-        callback_func("处理成功:{}个,失败:{}".format(successful_num, error_num))
+                    failed_folders.append(goods_art_no_folder_data)
+                    continue
+            
+            # 判断处理结果
+            if flag is None:
+                callback_func("货号:{} 数据异常".format(folder_name))
+                error_num += 1
+                failed_folders.append(goods_art_no_folder_data)
+            else:
+                if flag:
+                    successful_num += 1
+                    callback_func("货号:{} 图片生成处理成功".format(folder_name))
+                    successful_folders.append(goods_art_no_folder_data)
+                else:
+                    error_num += 1
+                    callback_func("货号:{} 图片生成处理失败".format(folder_name))
+                    failed_folders.append(goods_art_no_folder_data)
+        
+        callback_func("处理成功:{}个,失败:{}个".format(successful_num, error_num))
+        
+        # 返回详细的处理结果
+        result = {
+            'success': error_num == 0,
+            'successful_folders': successful_folders,
+            'failed_folders': failed_folders,
+            'successful_num': successful_num,
+            'error_num': error_num
+        }
+        
+        logger.info(f"[主流程完成] 成功: {successful_num}, 失败: {error_num}")
+        if failed_folders:
+            logger.warning(f"[主流程完成] 失败货号列表: {[f['folder_name'] for f in failed_folders]}")
+        
+        return result
 
     def checkImageAmount(
         self, image_dir: str, amount: int, todo_goods_art_no_folder_name_list=None
@@ -470,22 +500,28 @@ class BaseDealImage(object):
                 )
             print("**********123456********************")
             curve_mask = True if "俯视" in image_order_list else False
-            if not generate_pic.run(
-                image_path=original_image_path,
-                cut_image_path=original_move_bg_image_path,
-                out_path=out_path,
-                image_deal_mode=is_image_deal_mode,
-                # resize_mode=resize_mode,
-                resize_mode=1,#将这里得缩放模式改为强制不缩放 2025-10-22
-                out_pic_size=out_pic_size,
-                is_logo=True if i_n == 1 else False,
-                out_process_path_1=out_process_path_1,
-                out_process_path_2=out_process_path_2,
-                max_box=max_box,
-                logo_path=logo_path,
-                curve_mask=curve_mask,
-            ):
-                print("**********222222222222222222222222222********************")
+            try:
+                if not generate_pic.run(
+                    image_path=original_image_path,
+                    cut_image_path=original_move_bg_image_path,
+                    out_path=out_path,
+                    image_deal_mode=is_image_deal_mode,
+                    # resize_mode=resize_mode,
+                    resize_mode=1,#将这里得缩放模式改为强制不缩放 2025-10-22
+                    out_pic_size=out_pic_size,
+                    is_logo=True if i_n == 1 else False,
+                    out_process_path_1=out_process_path_1,
+                    out_process_path_2=out_process_path_2,
+                    max_box=max_box,
+                    logo_path=logo_path,
+                    curve_mask=curve_mask,
+                ):
+                    print("**********222222222222222222222222222********************")
+                    is_successful = False
+            except Exception as e:
+                import traceback
+                logger.error(f"货号 {folder_name} - 图片 {file_name} 生成失败: {e}\n{traceback.format_exc()}")
+                callback_func(f"货号 {folder_name} - 图片 {file_name} 处理异常: {e}")
                 is_successful = False
         if is_successful:
             return True

+ 296 - 49
python/service/run_main.py

@@ -35,6 +35,9 @@ class RunMain:
     # dialog_result_signal = Signal(str)
 
     # dialog_result_signal = Signal(str)
+    
+    # 重试机制配置:最大重试次数(固定为1,避免死循环)
+    MAX_RETRY_COUNT = 1
 
     def __init__(self, windows, token, uuid):
         super().__init__()
@@ -259,6 +262,147 @@ class RunMain:
             print("已结束抠图处理")
             return True
 
+    def validate_folder_integrity(self, folder_path, folder_name, expected_output_count=None):
+        """
+        验证货号文件夹的完整性
+        检查800x800目录中的文件数量是否等于预期数量
+        
+        Args:
+            folder_path: 货号文件夹路径
+            folder_name: 货号名称
+            expected_output_count: 预期的输出文件数量(如果为None则根据原始图数量计算)
+            
+        Returns:
+            tuple: (is_valid, original_count, processed_count, expected_count, message)
+        """
+        from logger import logger
+        import settings
+        
+        original_dir = "{}/原始图".format(folder_path)
+        processed_dir = "{}/800x800".format(folder_path)
+        
+        # 检查目录是否存在
+        if not os.path.exists(original_dir):
+            logger.warning(f"[目录校验] 货号 {folder_name} - 原始图目录不存在: {original_dir}")
+            return False, 0, 0, 0, f"原始图目录不存在"
+        
+        if not os.path.exists(processed_dir):
+            logger.warning(f"[目录校验] 货号 {folder_name} - 800x800目录不存在: {processed_dir}")
+            return False, 0, 0, 0, f"800x800目录不存在"
+        
+        # 统计原始图文件数量(只统计图片文件)
+        _Type = [".png", ".PNG", ".jpg", ".JPG", ".gif", ".GIF", ".jpge", ".JPGE"]
+        original_files = []
+        for f in os.listdir(original_dir):
+            _, ext = os.path.splitext(f)
+            if ext in _Type:
+                original_files.append(f)
+        
+        original_count = len(original_files)
+        
+        # 统计800x800目录文件数量
+        processed_files = []
+        for f in os.listdir(processed_dir):
+            _, ext = os.path.splitext(f)
+            if ext in _Type:
+                processed_files.append(f)
+        
+        processed_count = len(processed_files)
+        
+        # 计算预期的输出文件数量
+        if expected_output_count is None:
+            # 从配置中获取主图尺寸列表
+            try:
+                out_pic_size_list = settings.getSysConfigs("basic_configs", "main_image_size", [1600])
+                # 如果是字符串,尝试解析为列表
+                if isinstance(out_pic_size_list, str):
+                    import json
+                    try:
+                        out_pic_size_list = json.loads(out_pic_size_list)
+                    except:
+                        out_pic_size_list = [1600]
+                # 确保是列表
+                if not isinstance(out_pic_size_list, list):
+                    out_pic_size_list = [out_pic_size_list]
+                # 过滤空值
+                out_pic_size_list = [x for x in out_pic_size_list if x]
+                
+                if not out_pic_size_list:
+                    out_pic_size_list = [1600]
+                
+                # 预期输出数量 = 原始图数量 * 尺寸数量
+                expected_count = original_count * len(out_pic_size_list)
+                logger.info(f"[目录校验] 货号 {folder_name} - 配置的输出尺寸: {out_pic_size_list}, 共{len(out_pic_size_list)}个尺寸")
+            except Exception as e:
+                logger.warning(f"[目录校验] 货号 {folder_name} - 获取配置失败,使用默认值: {e}")
+                expected_count = original_count  # 默认1倍
+        else:
+            expected_count = expected_output_count
+        
+        logger.info(f"[目录校验] 货号 {folder_name} - 原始图: {original_count}张, 800x800: {processed_count}张, 预期: {expected_count}张")
+        
+        # 如果原始图为空,认为无效
+        if original_count == 0:
+            logger.error(f"[目录校验] 货号 {folder_name} - 原始图目录为空")
+            return False, original_count, processed_count, expected_count, "原始图目录为空"
+        
+        # 严格检查:处理后的文件数量必须等于预期数量
+        if processed_count != expected_count:
+            logger.error(f"[目录校验] 货号 {folder_name} - 处理失败: 预期{expected_count}张,实际{processed_count}张 (原始图{original_count}张 × {len(out_pic_size_list) if 'out_pic_size_list' in locals() else '?'}个尺寸)")
+            return False, original_count, processed_count, expected_count, f"处理文件数量不匹配: 预期{expected_count}张,实际{processed_count}张"
+        
+        logger.info(f"[目录校验] 货号 {folder_name} - 校验通过 ✓")
+        return True, original_count, processed_count, expected_count, "校验通过"
+    
+    def retry_single_folder(self, goods_art_no_folder_data, image_order_list, cutout_mode, resize_image_view, logo_path, callback_func):
+        """
+        重试单个货号的处理(仅执行一次,不会递归或循环)
+        
+        Args:
+            goods_art_no_folder_data: 货号文件夹数据
+            image_order_list: 图片顺序列表
+            cutout_mode: 抠图模式
+            resize_image_view: 缩放视角
+            logo_path: Logo路径
+            callback_func: 回调函数
+            
+        Returns:
+            bool: 重试是否成功
+        """
+        from logger import logger
+        
+        folder_name = goods_art_no_folder_data["folder_name"]
+        folder_path = goods_art_no_folder_data["folder_path"]
+        
+        logger.info(f"[重试处理] 开始重试货号: {folder_name} (仅此一次,不会重复重试)")
+        callback_func(f"正在重试货号: {folder_name}")
+        
+        deal = BaseDealImage(token=self.token)
+        try:
+            # 只处理这一个货号 - 注意:这里直接调用处理逻辑,不会再触发校验和重试
+            result = deal.shoes_run_one_folder_to_deal(
+                goods_art_no_folder_data=goods_art_no_folder_data,
+                resize_image_view=resize_image_view,
+                logo_path=logo_path,
+                image_order_list=image_order_list,
+                callback_func=callback_func,
+                windows=None,
+            )
+            
+            if result:
+                logger.info(f"[重试处理] 货号 {folder_name} 重试成功 ✓")
+                callback_func(f"货号 {folder_name} 重试成功")
+                return True
+            else:
+                logger.error(f"[重试处理] 货号 {folder_name} 重试失败 (不再重试)")
+                callback_func(f"货号 {folder_name} 重试失败")
+                return False
+        except Exception as e:
+            import traceback
+            logger.error(f"[重试处理] 货号 {folder_name} 重试异常: {e}\n{traceback.format_exc()}")
+            callback_func(f"货号 {folder_name} 重试异常: {e}")
+            return False
+
     def do_run_cutout_image(
         self,
         all_goods_art_no_folder_data,
@@ -270,43 +414,171 @@ class RunMain:
         logo_path,
         config_data,
     ):
-        try:
-            loop = asyncio.get_event_loop()
-        except:
-            loop = asyncio.new_event_loop()
-        executor = ThreadPoolExecutor(max_workers=10)
+        from logger import logger
+        
         goods_arts = [
             goods_art_no_folder_data["folder_name"]
             for goods_art_no_folder_data in all_goods_art_no_folder_data
         ]
         print("BaseDealImage().run_main========>>>>")
         deal = BaseDealImage(token=self.token)
-        try:
-            loop.run_in_executor(
-                executor,
-                deal.run_main(
-                    all_goods_art_no_folder_data=all_goods_art_no_folder_data,
+        
+        # 重试机制:最多重试1次,避免死循环
+        max_retry_count = self.MAX_RETRY_COUNT
+        retry_count = 0
+        is_success = False
+        failed_folders_from_run_main = []  # 从 run_main 返回的失败货号列表
+        
+        while retry_count <= max_retry_count and not is_success:
+            try:
+                if retry_count > 0:
+                    logger.warning(f"[主流程重试] 第 {retry_count} 次重试,重新执行抠图处理")
+                    callback_func(f"处理失败,正在重试(第{retry_count}次)...")
+                    
+                    # 重试时只处理之前失败的货号
+                    if failed_folders_from_run_main:
+                        logger.info(f"[主流程重试] 将重试以下货号: {[f['folder_name'] for f in failed_folders_from_run_main]}")
+                        # 临时替换为只包含失败货号的数据
+                        retry_folders = failed_folders_from_run_main
+                        failed_folders_from_run_main = []  # 清空,准备接收新的失败列表
+                    else:
+                        retry_folders = all_goods_art_no_folder_data
+                else:
+                    retry_folders = all_goods_art_no_folder_data
+                
+                # 执行主流程,获取返回结果
+                result = deal.run_main(
+                    all_goods_art_no_folder_data=retry_folders,
                     callback_func=callback_func,
                     image_order_list=image_order_list,
                     cutout_mode=cutout_mode,
                     resize_image_view=resize_image_view,
                     windows=windows,
                     logo_path=logo_path,
-                ),
+                )
+                
+                # 检查返回结果
+                if result and isinstance(result, dict):
+                    is_success = result.get('success', False)
+                    failed_folders_from_run_main = result.get('failed_folders', [])
+                    successful_num = result.get('successful_num', 0)
+                    error_num = result.get('error_num', 0)
+                    
+                    logger.info(f"[主流程完成] 成功: {successful_num}, 失败: {error_num}")
+                    
+                    if is_success:
+                        logger.info("[主流程完成] 所有货号处理成功 ✓")
+                    else:
+                        # 有失败的货号,需要重试
+                        retry_count += 1  # 增加重试计数
+                        logger.warning(f"[主流程完成] 有 {error_num} 个货号失败,准备重试")
+                        callback_func(f"有 {error_num} 个货号处理失败,准备重试...")
+                        
+                        if retry_count > max_retry_count:
+                            # 已达到最大重试次数
+                            logger.error(f"[主流程重试] 已达到最大重试次数 {max_retry_count},放弃重试")
+                            callback_func(f"处理失败,已重试{max_retry_count}次仍无法完成")
+                            # 不抛出异常,继续执行后置校验
+                            is_success = True  # 强制退出循环,让后置校验处理
+                        else:
+                            logger.warning(f"[主流程重试] 准备第 {retry_count} 次重试,将只处理失败的货号")
+                            callback_func(f"准备重试失败的货号...")
+                else:
+                    # 兼容旧版本,如果没有返回值则认为成功
+                    is_success = True
+                
+            except UnicornException as e:
+                retry_count += 1
+                logger.error(f"[主流程重试] 第 {retry_count} 次尝试失败: {e.msg}")
+                
+                if retry_count > max_retry_count:
+                    logger.error(f"[主流程重试] 已达到最大重试次数 {max_retry_count},放弃重试")
+                    callback_func(f"处理失败,已重试{max_retry_count}次仍无法完成")
+                    raise UnicornException(e.msg)
+                else:
+                    logger.warning(f"[主流程重试] 准备第 {retry_count} 次重试")
+                    callback_func(f"处理异常,准备重试...")
+                    
+            except Exception as e:
+                import traceback
+                retry_count += 1
+                logger.error(f"[主流程重试] 第 {retry_count} 次尝试异常: {e}\n{traceback.format_exc()}")
+                
+                if retry_count > max_retry_count:
+                    logger.error(f"[主流程重试] 已达到最大重试次数 {max_retry_count},放弃重试")
+                    callback_func(f"处理异常,已重试{max_retry_count}次仍无法完成")
+                    raise UnicornException(e)
+                else:
+                    logger.warning(f"[主流程重试] 准备第 {retry_count} 次重试")
+                    callback_func(f"处理异常,准备重试...")
+
+        # ========== 后置校验:检查所有货号的目录完整性 ==========
+        logger.info("=" * 50)
+        logger.info("[后置校验] 开始检查所有货号的目录完整性")
+        logger.info("=" * 50)
+        
+        failed_folders = []
+        for goods_art_no_folder_data in all_goods_art_no_folder_data:
+            if goods_art_no_folder_data["label"] != "待处理":
+                continue
+            
+            folder_name = goods_art_no_folder_data["folder_name"]
+            folder_path = goods_art_no_folder_data["folder_path"]
+            
+            is_valid, original_count, processed_count, expected_count, message = self.validate_folder_integrity(
+                folder_path, folder_name
             )
-        except UnicornException as e:
-            raise UnicornException(e.msg)
-        except Exception as e:
-            raise UnicornException(e)
-        # deal.run_main(
-        #     all_goods_art_no_folder_data=all_goods_art_no_folder_data,
-        #     callback_func=callback_func,
-        #     image_order_list=image_order_list,
-        #     cutout_mode=cutout_mode,
-        #     resize_image_view=resize_image_view,
-        #     windows=windows,
-        #     logo_path=logo_path,
-        # )
+            
+            if not is_valid:
+                logger.warning(f"[后置校验] 货号 {folder_name} 校验失败: {message}")
+                failed_folders.append({
+                    "data": goods_art_no_folder_data,
+                    "reason": message,
+                    "original_count": original_count,
+                    "processed_count": processed_count,
+                    "expected_count": expected_count
+                })
+        
+        # ========== 如果有失败的货号,进行重试(仅重试一次,避免死循环)==========
+        if failed_folders:
+            logger.info("=" * 50)
+            logger.info(f"[重试机制] 发现 {len(failed_folders)} 个货号需要重试")
+            logger.info(f"[重试机制] 重要提示:每个货号最多重试 {self.MAX_RETRY_COUNT} 次,不会无限重试")
+            logger.info("=" * 50)
+            
+            retry_success_count = 0
+            retry_failed_count = 0
+            
+            for failed_item in failed_folders:
+                folder_name = failed_item["data"]["folder_name"]
+                logger.info(f"[重试机制] 开始重试货号: {folder_name}, 原因: {failed_item['reason']}")
+                callback_func(f"检测到 {folder_name} 处理不完整,正在重试...")
+                
+                # 执行单次重试(retry_single_folder 内部不会再触发校验和重试)
+                # 注意:这里只调用一次,不会循环或递归
+                retry_result = self.retry_single_folder(
+                    goods_art_no_folder_data=failed_item["data"],
+                    image_order_list=image_order_list,
+                    cutout_mode=cutout_mode,
+                    resize_image_view=resize_image_view,
+                    logo_path=logo_path,
+                    callback_func=callback_func
+                )
+                
+                if retry_result:
+                    retry_success_count += 1
+                else:
+                    retry_failed_count += 1
+                    logger.warning(f"[重试机制] 货号 {folder_name} 重试后仍然失败,不再继续重试(已达最大重试次数 {self.MAX_RETRY_COUNT})")
+            
+            logger.info("=" * 50)
+            logger.info(f"[重试机制] 重试完成: 成功 {retry_success_count} 个, 失败 {retry_failed_count} 个")
+            logger.info(f"[重试机制] 所有货号处理结束(包含一次重试),流程终止")
+            logger.info("=" * 50)
+            
+            callback_func(f"重试完成: 成功 {retry_success_count} 个, 失败 {retry_failed_count} 个")
+        else:
+            logger.info("[后置校验] 所有货号校验通过,无需重试 ✓")
 
         recordDataPoint(
             token=self.token,
@@ -314,31 +586,6 @@ class RunMain:
             page="抠图结束",
             data=goods_arts,
         )
-        # try:
-        #     loop = asyncio.get_event_loop()
-        #     loop.create_task(
-        #             sendSocketMessage(
-        #                 code=0,
-        #                 msg="抠图结束",
-        #                 data={
-        #                     "status": "已完成",
-        #                     "goods_art_nos": goods_arts,
-        #                 },
-        #                 msg_type="segment_progress",
-        #             )
-        #         )
-        # except:
-        #     asyncio.run(
-        #         sendSocketMessage(
-        #             code=0,
-        #             msg="抠图结束",
-        #             data={
-        #                 "status": "已完成",
-        #                 "goods_art_nos": goods_arts,
-        #             },
-        #             msg_type="segment_progress",
-        #         )
-        #     )
         callback_func("已结束抠图处理")
         return True