Quellcode durchsuchen

统一解析函数的返回数据格式。
新增添加人才记录入库的接口以及对应函数。

hedpma vor 1 Tag
Ursprung
Commit
168f6a9d92

+ 135 - 2
app/api/data_parse/routes.py

@@ -22,7 +22,8 @@ from app.core.data_parse.parse_system import (
 from app.core.data_parse.parse_task import (
     get_parse_tasks, 
     get_parse_task_detail,
-    add_parse_task
+    add_parse_task,
+    add_parsed_talents
 )
 # 导入酒店管理相关函数
 from app.core.data_parse.hotel_management import (
@@ -2092,7 +2093,7 @@ def add_parse_task_route():
         }), 500
 
 
-@bp.route('/execute_parse_task', methods=['POST'])
+@bp.route('/execute-parse-task', methods=['POST'])
 def execute_parse_task():
     """
     执行解析任务接口
@@ -2238,3 +2239,135 @@ def execute_parse_task():
             'data': None
         }), 500
 
+
+@bp.route('/add-parsed-talents', methods=['POST'])
+def add_parsed_talents_route():
+    """
+    处理解析任务响应数据并写入人才信息接口
+    
+    请求参数:
+        - api_response_data: execute-parse-task API的完整返回数据 (JSON格式)
+        
+    请求体示例:
+        {
+            "success": true,
+            "message": "处理完成",
+            "data": {
+                "summary": {
+                    "total_files": 5,
+                    "success_count": 4,
+                    "failed_count": 1,
+                    "success_rate": 80.0
+                },
+                "results": [
+                    {
+                        "index": 0,
+                        "success": true,
+                        "data": {
+                            "name_zh": "张三",
+                            "title_zh": "经理",
+                            "hotel_zh": "某酒店"
+                        }
+                    }
+                ],
+                "processed_time": "2025-01-18T10:30:00"
+            }
+        }
+        
+    返回:
+        - JSON: 包含批量处理结果和状态信息
+        
+    功能说明:
+        - 接收 execute-parse-task API 的完整返回数据
+        - 自动识别和处理不同格式的人才数据(单人/批量)
+        - 调用 add_single_talent 函数将人才信息写入 business_cards 表
+        - 支持新任命等包含多个人员信息的批量数据
+        - 提供详细的处理统计和结果追踪
+        - 保留原始API响应数据用于调试
+        
+    状态码:
+        - 200: 全部处理成功
+        - 206: 部分处理成功
+        - 400: 请求参数错误
+        - 500: 服务器内部错误
+    """
+    try:
+        # 检查请求是否为 JSON 格式
+        if not request.is_json:
+            return jsonify({
+                'success': False,
+                'message': '请求必须是 JSON 格式',
+                'data': None
+            }), 400
+        
+        # 获取请求数据
+        api_response_data = request.get_json()
+        
+        # 基本参数验证
+        if not api_response_data:
+            return jsonify({
+                'success': False,
+                'message': '请求数据不能为空',
+                'data': None
+            }), 400
+        
+        # 验证数据格式
+        if not isinstance(api_response_data, dict):
+            return jsonify({
+                'success': False,
+                'message': '请求数据必须是JSON对象格式',
+                'data': None
+            }), 400
+        
+        # 记录请求日志
+        total_results = 0
+        if api_response_data.get('data') and api_response_data['data'].get('results'):
+            total_results = len(api_response_data['data']['results'])
+        
+        logger.info(f"收到处理解析任务响应数据请求,包含 {total_results} 条结果记录")
+        
+        # 调用核心业务逻辑
+        result = add_parsed_talents(api_response_data)
+        
+        # 根据处理结果设置HTTP状态码
+        if result.get('success', False):
+            if result.get('code') == 200:
+                status_code = 200  # 全部成功
+            elif result.get('code') == 206:
+                status_code = 206  # 部分成功
+            else:
+                status_code = 200  # 默认成功
+        else:
+            if result.get('code') == 400:
+                status_code = 400  # 参数错误
+            else:
+                status_code = 500  # 服务器错误
+        
+        # 记录处理结果日志
+        if result.get('success'):
+            data_summary = result.get('data', {}).get('summary', {})
+            success_count = data_summary.get('success_count', 0)
+            failed_count = data_summary.get('failed_count', 0)
+            logger.info(f"处理解析任务响应数据完成: 成功 {success_count} 条,失败 {failed_count} 条")
+        else:
+            logger.error(f"处理解析任务响应数据失败: {result.get('message', '未知错误')}")
+        
+        # 返回结果
+        return jsonify({
+            'success': result.get('success', False),
+            'message': result.get('message', '处理完成'),
+            'data': result.get('data')
+        }), status_code
+        
+    except Exception as e:
+        # 记录错误日志
+        error_msg = f"处理解析任务响应数据接口失败: {str(e)}"
+        logger.error(error_msg, exc_info=True)
+        
+        # 返回错误响应
+        return jsonify({
+            'success': False,
+            'message': error_msg,
+            'data': None
+        }), 500
+

+ 4 - 0
app/config/config.py

@@ -52,6 +52,10 @@ class BaseConfig:
     # LLM_API_KEY = os.environ.get('LLM_API_KEY', "sk-86d4622141d74e9a8d7c38ee873c4d91")
     LLM_API_KEY = os.environ.get('LLM_API_KEY', "sk-db68e37f00974031935395315bfe07f0")
     
+    # Qwen VL模型配置 - 用于图片解析
+    QWEN_API_KEY = os.environ.get('QWEN_API_KEY', "sk-db68e37f00974031935395315bfe07f0")
+    QWEN_API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
+    
     # 日志基础配置
     LOG_FORMAT = '%(asctime)s - %(levelname)s - %(filename)s - %(funcName)s - %(lineno)s - %(message)s'
     LOG_ENCODING = 'UTF-8'

+ 115 - 1
app/core/data_parse/parse_card.py

@@ -7,6 +7,7 @@ from botocore.config import Config
 import logging
 import uuid
 import json
+import re
 from io import BytesIO
 from werkzeug.datastructures import FileStorage
 from app.config.config import DevelopmentConfig, ProductionConfig
@@ -97,6 +98,24 @@ def process_business_card_image(image_file):
                 }
             except Exception as qwen_error:
                 logging.warning(f"Qwen 模型解析失败,错误原因: {str(qwen_error)}")
+                
+                # 如果是连接错误,尝试使用备用方案
+                if "Connection error" in str(qwen_error):
+                    logging.info("Qwen连接失败,尝试使用备用OCR+规则解析方案")
+                    try:
+                        # 尝试使用备用的OCR+规则提取方案
+                        fallback_data = fallback_ocr_extraction(image_data)
+                        if fallback_data:
+                            logging.info("备用OCR解析成功")
+                            return {
+                                'code': 200,
+                                'success': True,
+                                'message': '使用备用OCR方案解析成功(Qwen连接失败)',
+                                'data': fallback_data
+                            }
+                    except Exception as fallback_error:
+                        logging.error(f"备用OCR解析也失败: {str(fallback_error)}")
+                
                 return {
                     'code': 500,
                     'success': False,
@@ -921,4 +940,99 @@ def parse_business_card_with_qwen(image_data):
     except Exception as e:
         error_msg = f"Qwen VL Max 模型解析失败: {str(e)}"
         logging.error(error_msg, exc_info=True)
-        raise Exception(error_msg) 
+        raise Exception(error_msg)
+
+
+def fallback_ocr_extraction(image_data):
+    """
+    备用OCR+规则提取方案
+    当Qwen API不可用时使用
+    
+    Args:
+        image_data (bytes): 图像的二进制数据
+        
+    Returns:
+        dict: 基础的名片信息(可能不完整)
+    """
+    try:
+        logging.info("开始备用OCR文本提取")
+        
+        # 使用PIL处理图像
+        from PIL import Image
+        import pytesseract
+        
+        # 将字节数据转换为PIL图像
+        image = Image.open(BytesIO(image_data))
+        
+        # 使用OCR提取文本
+        text = pytesseract.image_to_string(image, lang='chi_sim+eng')
+        logging.info(f"OCR提取的文本: {text}")
+        
+        if not text.strip():
+            logging.warning("OCR未能提取到任何文本")
+            return None
+        
+        # 基础的规则提取
+        extracted_data = {
+            'name_zh': '',
+            'name_en': '',
+            'title_zh': '',
+            'title_en': '',
+            'hotel_zh': '',
+            'hotel_en': '',
+            'mobile': '',
+            'phone': '',
+            'email': '',
+            'address_zh': '',
+            'address_en': '',
+            'postal_code_zh': '',
+            'postal_code_en': '',
+            'birthday': '',
+            'age': 0,
+            'native_place': '',
+            'residence': '',
+            'brand_group': '',
+            'career_path': [],
+            'affiliation': []
+        }
+        
+        # 简单的规则提取
+        lines = [line.strip() for line in text.split('\n') if line.strip()]
+        
+        # 提取邮箱
+        email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
+        email_matches = re.findall(email_pattern, text)
+        if email_matches:
+            extracted_data['email'] = email_matches[0]
+        
+        # 提取手机号码(中国手机号)
+        mobile_pattern = r'\b1[3-9]\d{9}\b'
+        mobile_matches = re.findall(mobile_pattern, text)
+        if mobile_matches:
+            extracted_data['mobile'] = ','.join(mobile_matches[:3])  # 最多3个
+        
+        # 提取固定电话
+        phone_pattern = r'\b\d{3,4}-?\d{7,8}\b'
+        phone_matches = re.findall(phone_pattern, text)
+        if phone_matches:
+            extracted_data['phone'] = ','.join(phone_matches[:2])  # 最多2个
+        
+        # 如果找到了一些基础信息,添加一个职业轨迹记录
+        if extracted_data['email'] or extracted_data['mobile']:
+            career_entry = {
+                'date': datetime.now().strftime('%Y-%m-%d'),
+                'hotel_en': '',
+                'hotel_zh': '',
+                'image_path': '',
+                'source': 'ocr_fallback',
+                'title_en': '',
+                'title_zh': ''
+            }
+            extracted_data['career_path'] = [career_entry]
+        
+        logging.info("备用OCR解析完成")
+        return extracted_data
+        
+    except Exception as e:
+        logging.error(f"备用OCR解析失败: {str(e)}")
+        return None 

+ 92 - 32
app/core/data_parse/parse_menduner.py

@@ -293,65 +293,125 @@ def batch_process_menduner_data(data_list: List[Dict[str, Any]]) -> Dict[str, An
         data_list (List[Dict[str, Any]]): 待处理的人才数据列表
         
     Returns:
-        Dict[str, Any]: 批量处理结果
+        Dict[str, Any]: 批量处理结果,格式与batch_process_business_card_images保持一致
     """
     try:
-        processed_data = []
-        validation_results = []
+        # 验证参数
+        if not data_list or not isinstance(data_list, list):
+            return {
+                'code': 400,
+                'success': False,
+                'message': 'data_list参数必须是非空数组',
+                'data': None
+            }
+        
+        if len(data_list) == 0:
+            return {
+                'code': 400,
+                'success': False,
+                'message': '门墩儿数据数组不能为空',
+                'data': None
+            }
+        
+        results = []
         success_count = 0
-        error_count = 0
+        failed_count = 0
+        
+        logging.info(f"开始批量处理门墩儿人才数据,共 {len(data_list)} 条记录")
         
+        # 逐一处理每条数据
         for i, data in enumerate(data_list):
             try:
+                logging.debug(f"处理第 {i+1}/{len(data_list)} 条数据")
+                
                 # 标准化数据
                 normalized = _normalize_talent_profile(data)
                 
                 # 验证数据
                 validation = validate_menduner_data(normalized)
                 
-                processed_data.append(normalized)
-                validation_results.append({
-                    'index': i,
-                    'validation': validation
-                })
-                
-                if validation['is_valid']:
+                if validation.get('is_valid', False):
                     success_count += 1
+                    results.append({
+                        'index': i,
+                        'data_id': data.get('id', f'record_{i}'),
+                        'success': True,
+                        'error': None,
+                        'data': {
+                            'normalized_data': normalized,
+                            'validation': validation
+                        },
+                        'message': f'处理成功,验证得分: {validation.get("score", 0)}'
+                    })
+                    logging.debug(f"成功处理第 {i+1} 条数据")
                 else:
-                    error_count += 1
+                    failed_count += 1
+                    error_messages = validation.get('errors', ['验证失败'])
+                    results.append({
+                        'index': i,
+                        'data_id': data.get('id', f'record_{i}'),
+                        'success': False,
+                        'error': '; '.join(error_messages),
+                        'data': {
+                            'normalized_data': normalized,
+                            'validation': validation
+                        }
+                    })
+                    logging.warning(f"处理第 {i+1} 条数据失败: {'; '.join(error_messages)}")
                     
-            except Exception as e:
-                error_count += 1
-                validation_results.append({
+            except Exception as item_error:
+                failed_count += 1
+                error_msg = f"处理门墩儿数据失败: {str(item_error)}"
+                logging.error(error_msg, exc_info=True)
+                results.append({
                     'index': i,
-                    'validation': {
-                        'is_valid': False,
-                        'errors': [f"处理失败: {str(e)}"],
-                        'warnings': [],
-                        'score': 0
-                    }
+                    'data_id': data.get('id', f'record_{i}') if isinstance(data, dict) else f'record_{i}',
+                    'success': False,
+                    'error': error_msg,
+                    'data': None
                 })
         
-        return {
-            'success': True,
+        # 组装最终结果
+        batch_result = {
             'summary': {
-                'total_count': len(data_list),
+                'total_files': len(data_list),
                 'success_count': success_count,
-                'error_count': error_count,
-                'success_rate': (success_count / len(data_list)) * 100 if data_list else 0
+                'failed_count': failed_count,
+                'success_rate': round((success_count / len(data_list)) * 100, 2) if len(data_list) > 0 else 0
             },
-            'processed_data': processed_data,
-            'validation_results': validation_results
+            'results': results,
+            'processed_time': datetime.now().isoformat()
         }
         
+        if failed_count == 0:
+            return {
+                'code': 200,
+                'success': True,
+                'message': f'批量处理完成,全部 {success_count} 条数据处理成功',
+                'data': batch_result
+            }
+        elif success_count == 0:
+            return {
+                'code': 500,
+                'success': False,
+                'message': f'批量处理失败,全部 {failed_count} 条数据处理失败',
+                'data': batch_result
+            }
+        else:
+            return {
+                'code': 206,  # Partial Content
+                'success': True,
+                'message': f'批量处理部分成功,成功 {success_count} 条,失败 {failed_count} 条',
+                'data': batch_result
+            }
+            
     except Exception as e:
         error_msg = f"批量处理门墩儿数据失败: {str(e)}"
         logging.error(error_msg, exc_info=True)
         
         return {
+            'code': 500,
             'success': False,
-            'error': error_msg,
-            'summary': None,
-            'processed_data': [],
-            'validation_results': []
+            'message': error_msg,
+            'data': None
         } 

+ 96 - 22
app/core/data_parse/parse_pic.py

@@ -670,64 +670,138 @@ def batch_process_images(image_paths: List[str], process_type: str = 'table') ->
         process_type (str): 处理类型,只支持 'table'
         
     Returns:
-        Dict[str, Any]: 批量处理结果
+        Dict[str, Any]: 批量处理结果,格式与batch_process_business_card_images保持一致
     """
     try:
         # 验证处理类型
         if process_type != 'table':
             return {
+                'code': 400,
                 'success': False,
-                'error': f'不支持的处理类型: {process_type},只支持 "table" 类型',
-                'results': []
+                'message': f'不支持的处理类型: {process_type},只支持 "table" 类型',
+                'data': {
+                    'summary': {
+                        'total_files': len(image_paths),
+                        'success_count': 0,
+                        'failed_count': len(image_paths),
+                        'success_rate': 0.0,
+                        'process_type': process_type
+                    },
+                    'results': [],
+                    'processed_time': datetime.now().isoformat()
+                }
+            }
+        
+        # 验证参数
+        if not image_paths or not isinstance(image_paths, list):
+            return {
+                'code': 400,
+                'success': False,
+                'message': 'image_paths参数必须是非空数组',
+                'data': None
+            }
+        
+        if len(image_paths) == 0:
+            return {
+                'code': 400,
+                'success': False,
+                'message': '图片路径数组不能为空',
+                'data': None
             }
         
         results = []
         success_count = 0
         failed_count = 0
         
-        for image_path in image_paths:
+        logging.info(f"开始批量处理图片,共 {len(image_paths)} 个文件")
+        
+        # 逐一处理每个图片路径
+        for i, image_path in enumerate(image_paths):
             try:
+                logging.info(f"处理第 {i+1}/{len(image_paths)} 个文件: {image_path}")
+                
                 # 只支持表格处理
                 result = parse_table_image(image_path)
                 
-                results.append({
-                    'image_path': image_path,
-                    'result': result
-                })
-                
-                if result['success']:
+                if result.get('success', False):
                     success_count += 1
+                    results.append({
+                        'index': i,
+                        'image_path': image_path,
+                        'filename': os.path.basename(image_path) if image_path else f'file_{i}',
+                        'success': True,
+                        'error': None,
+                        'data': result.get('data'),
+                        'message': result.get('message', '处理成功')
+                    })
+                    logging.info(f"成功处理第 {i+1} 个文件: {os.path.basename(image_path)}")
                 else:
                     failed_count += 1
+                    results.append({
+                        'index': i,
+                        'image_path': image_path,
+                        'filename': os.path.basename(image_path) if image_path else f'file_{i}',
+                        'success': False,
+                        'error': result.get('error', '处理失败'),
+                        'data': None
+                    })
+                    logging.error(f"处理第 {i+1} 个文件失败: {result.get('error', '未知错误')}")
                     
-            except Exception as e:
+            except Exception as item_error:
                 failed_count += 1
+                error_msg = f"处理图片失败: {str(item_error)}"
+                logging.error(error_msg, exc_info=True)
                 results.append({
+                    'index': i,
                     'image_path': image_path,
-                    'result': {
-                        'success': False,
-                        'error': str(e)
-                    }
+                    'filename': os.path.basename(image_path) if image_path else f'file_{i}',
+                    'success': False,
+                    'error': error_msg,
+                    'data': None
                 })
         
-        return {
-            'success': True,
+        # 组装最终结果
+        batch_result = {
             'summary': {
-                'total_images': len(image_paths),
+                'total_files': len(image_paths),
                 'success_count': success_count,
                 'failed_count': failed_count,
-                'success_rate': (success_count / len(image_paths)) * 100 if image_paths else 0,
+                'success_rate': round((success_count / len(image_paths)) * 100, 2) if len(image_paths) > 0 else 0,
                 'process_type': process_type
             },
-            'results': results
+            'results': results,
+            'processed_time': datetime.now().isoformat()
         }
         
+        if failed_count == 0:
+            return {
+                'code': 200,
+                'success': True,
+                'message': f'批量处理完成,全部 {success_count} 个文件处理成功',
+                'data': batch_result
+            }
+        elif success_count == 0:
+            return {
+                'code': 500,
+                'success': False,
+                'message': f'批量处理失败,全部 {failed_count} 个文件处理失败',
+                'data': batch_result
+            }
+        else:
+            return {
+                'code': 206,  # Partial Content
+                'success': True,
+                'message': f'批量处理部分成功,成功 {success_count} 个,失败 {failed_count} 个',
+                'data': batch_result
+            }
+            
     except Exception as e:
         error_msg = f"批量处理图片失败: {str(e)}"
         logging.error(error_msg, exc_info=True)
         
         return {
+            'code': 500,
             'success': False,
-            'error': error_msg,
-            'results': []
+            'message': error_msg,
+            'data': None
         } 

+ 94 - 18
app/core/data_parse/parse_resume.py

@@ -374,41 +374,117 @@ def batch_parse_resumes(file_paths: List[str]) -> Dict[str, Any]:
         file_paths (List[str]): 简历文件路径列表
         
     Returns:
-        Dict[str, Any]: 批量解析结果
+        Dict[str, Any]: 批量解析结果,格式与batch_process_business_card_images保持一致
     """
     try:
+        # 验证参数
+        if not file_paths or not isinstance(file_paths, list):
+            return {
+                'code': 400,
+                'success': False,
+                'message': 'file_paths参数必须是非空数组',
+                'data': None
+            }
+        
+        if len(file_paths) == 0:
+            return {
+                'code': 400,
+                'success': False,
+                'message': '简历文件路径数组不能为空',
+                'data': None
+            }
+        
         results = []
         success_count = 0
         failed_count = 0
         
-        for file_path in file_paths:
-            result = parse_resume_file(file_path)
-            results.append({
-                'file_path': file_path,
-                'result': result
-            })
-            
-            if result['success']:
-                success_count += 1
-            else:
+        logging.info(f"开始批量解析简历文件,共 {len(file_paths)} 个文件")
+        
+        # 逐一处理每个简历文件
+        for i, file_path in enumerate(file_paths):
+            try:
+                logging.info(f"处理第 {i+1}/{len(file_paths)} 个文件: {file_path}")
+                
+                result = parse_resume_file(file_path)
+                
+                if result.get('success', False):
+                    success_count += 1
+                    results.append({
+                        'index': i,
+                        'file_path': file_path,
+                        'filename': os.path.basename(file_path) if file_path else f'file_{i}',
+                        'success': True,
+                        'error': None,
+                        'data': result.get('data'),
+                        'message': result.get('message', '处理成功')
+                    })
+                    logging.info(f"成功处理第 {i+1} 个文件: {os.path.basename(file_path)}")
+                else:
+                    failed_count += 1
+                    results.append({
+                        'index': i,
+                        'file_path': file_path,
+                        'filename': os.path.basename(file_path) if file_path else f'file_{i}',
+                        'success': False,
+                        'error': result.get('error', '处理失败'),
+                        'data': None
+                    })
+                    logging.error(f"处理第 {i+1} 个文件失败: {result.get('error', '未知错误')}")
+                    
+            except Exception as item_error:
                 failed_count += 1
-        
-        return {
-            'success': True,
+                error_msg = f"处理简历文件失败: {str(item_error)}"
+                logging.error(error_msg, exc_info=True)
+                results.append({
+                    'index': i,
+                    'file_path': file_path,
+                    'filename': os.path.basename(file_path) if file_path else f'file_{i}',
+                    'success': False,
+                    'error': error_msg,
+                    'data': None
+                })
+        
+        # 组装最终结果
+        batch_result = {
             'summary': {
                 'total_files': len(file_paths),
                 'success_count': success_count,
-                'failed_count': failed_count
+                'failed_count': failed_count,
+                'success_rate': round((success_count / len(file_paths)) * 100, 2) if len(file_paths) > 0 else 0
             },
-            'results': results
+            'results': results,
+            'processed_time': datetime.now().isoformat()
         }
         
+        if failed_count == 0:
+            return {
+                'code': 200,
+                'success': True,
+                'message': f'批量处理完成,全部 {success_count} 个文件处理成功',
+                'data': batch_result
+            }
+        elif success_count == 0:
+            return {
+                'code': 500,
+                'success': False,
+                'message': f'批量处理失败,全部 {failed_count} 个文件处理失败',
+                'data': batch_result
+            }
+        else:
+            return {
+                'code': 206,  # Partial Content
+                'success': True,
+                'message': f'批量处理部分成功,成功 {success_count} 个,失败 {failed_count} 个',
+                'data': batch_result
+            }
+            
     except Exception as e:
         error_msg = f"批量解析简历失败: {str(e)}"
         logging.error(error_msg, exc_info=True)
         
         return {
+            'code': 500,
             'success': False,
-            'error': error_msg,
-            'results': []
+            'message': error_msg,
+            'data': None
         } 

+ 33 - 16
app/core/data_parse/parse_system.py

@@ -1972,22 +1972,39 @@ def parse_text_with_qwen25VLplus(image_data):
 ```"""
         
         # 调用 Qwen VL Max  API(添加重试机制)
-        logging.info("发送请求到 Qwen VL Max 模型")
-        completion = client.chat.completions.create(
-            # model="qwen-vl-plus",
-            model="qwen-vl-max-latest",
-            messages=[
-                {
-                    "role": "user",
-                    "content": [
-                        {"type": "text", "text": prompt},
-                        {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}
-                    ]
-                }
-            ],
-            temperature=0.1,  # 降低温度增加精确性
-            response_format={"type": "json_object"}  # 要求输出JSON格式
-        )
+        max_retries = 3
+        retry_delay = 2  # 秒
+        
+        for attempt in range(max_retries):
+            try:
+                logging.info(f"发送请求到 Qwen VL Max 模型 (第 {attempt + 1}/{max_retries} 次尝试)")
+                completion = client.chat.completions.create(
+                    # model="qwen-vl-plus",
+                    model="qwen-vl-max-latest",
+                    messages=[
+                        {
+                            "role": "user",
+                            "content": [
+                                {"type": "text", "text": prompt},
+                                {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}
+                            ]
+                        }
+                    ],
+                    temperature=0.1,  # 降低温度增加精确性
+                    response_format={"type": "json_object"},  # 要求输出JSON格式
+                    timeout=30  # 30秒超时
+                )
+                break  # 如果成功,退出重试循环
+            except Exception as api_error:
+                logging.warning(f"Qwen API 调用失败 (第 {attempt + 1}/{max_retries} 次): {str(api_error)}")
+                if attempt == max_retries - 1:  # 最后一次尝试
+                    if "Connection error" in str(api_error) or "timeout" in str(api_error).lower():
+                        raise Exception("Connection error.")
+                    else:
+                        raise api_error
+                else:
+                    time.sleep(retry_delay)  # 等待后重试
+                    retry_delay *= 2  # 指数退避
         
         # 解析响应
         response_content = completion.choices[0].message.content

+ 498 - 0
app/core/data_parse/parse_task.py

@@ -573,6 +573,504 @@ def add_parse_task(files, task_type, created_by='system'):
         error_msg = f"新增解析任务失败: {str(e)}"
         logging.error(error_msg, exc_info=True)
         
+        return {
+            'code': 500,
+            'success': False,
+            'message': error_msg,
+            'data': None
+        }
+
+
+def add_single_talent(talent_data):
+    """
+    添加单个人才记录(基于add_business_card逻辑,去除MinIO图片上传)
+    
+    Args:
+        talent_data (dict): 人才信息数据
+        
+    Returns:
+        dict: 处理结果,包含保存的信息和状态
+    """
+    try:
+        # 检查必要的数据
+        if not talent_data:
+            return {
+                'code': 400,
+                'success': False,
+                'message': '人才数据不能为空',
+                'data': None
+            }
+        
+        # 检查重复记录
+        try:
+            from app.core.data_parse.parse_system import check_duplicate_business_card
+            duplicate_check = check_duplicate_business_card(talent_data)
+            logging.info(f"重复记录检查结果: {duplicate_check['reason']}")
+        except Exception as e:
+            logging.error(f"重复记录检查失败: {str(e)}", exc_info=True)
+            # 如果检查失败,默认创建新记录
+            duplicate_check = {
+                'is_duplicate': False,
+                'action': 'create_new',
+                'existing_card': None,
+                'reason': f'重复检查失败,创建新记录: {str(e)}'
+            }
+        
+        try:
+            # 根据重复检查结果执行不同操作
+            if duplicate_check['action'] == 'update':
+                # 更新现有记录
+                existing_card = duplicate_check['existing_card']
+                
+                # 导入手机号码处理函数
+                from app.core.data_parse.parse_system import normalize_mobile_numbers, merge_mobile_numbers
+                
+                # 更新基本信息
+                existing_card.name_en = talent_data.get('name_en', existing_card.name_en)
+                existing_card.title_zh = talent_data.get('title_zh', existing_card.title_zh)
+                existing_card.title_en = talent_data.get('title_en', existing_card.title_en)
+                
+                # 处理手机号码字段,支持多个手机号码
+                if 'mobile' in talent_data:
+                    new_mobile = normalize_mobile_numbers(talent_data.get('mobile', ''))
+                    if new_mobile:
+                        # 如果有新的手机号码,合并到现有手机号码中
+                        existing_card.mobile = merge_mobile_numbers(existing_card.mobile, new_mobile)
+                    elif talent_data.get('mobile') == '':
+                        # 如果明确传入空字符串,则清空手机号码
+                        existing_card.mobile = ''
+                
+                existing_card.phone = talent_data.get('phone', existing_card.phone)
+                existing_card.email = talent_data.get('email', existing_card.email)
+                existing_card.hotel_zh = talent_data.get('hotel_zh', existing_card.hotel_zh)
+                existing_card.hotel_en = talent_data.get('hotel_en', existing_card.hotel_en)
+                existing_card.address_zh = talent_data.get('address_zh', existing_card.address_zh)
+                existing_card.address_en = talent_data.get('address_en', existing_card.address_en)
+                existing_card.postal_code_zh = talent_data.get('postal_code_zh', existing_card.postal_code_zh)
+                existing_card.postal_code_en = talent_data.get('postal_code_en', existing_card.postal_code_en)
+                existing_card.brand_zh = talent_data.get('brand_zh', existing_card.brand_zh)
+                existing_card.brand_en = talent_data.get('brand_en', existing_card.brand_en)
+                existing_card.affiliation_zh = talent_data.get('affiliation_zh', existing_card.affiliation_zh)
+                existing_card.affiliation_en = talent_data.get('affiliation_en', existing_card.affiliation_en)
+                
+                # 处理生日字段
+                if talent_data.get('birthday'):
+                    try:
+                        existing_card.birthday = datetime.strptime(talent_data.get('birthday'), '%Y-%m-%d').date()
+                    except ValueError:
+                        # 如果日期格式不正确,保持原值
+                        pass
+                
+                # 处理年龄字段
+                if 'age' in talent_data:
+                    try:
+                        if talent_data['age'] is not None and str(talent_data['age']).strip():
+                            age_value = int(talent_data['age'])
+                            if 0 < age_value <= 150:  # 合理的年龄范围检查
+                                existing_card.age = age_value
+                        else:
+                            existing_card.age = None
+                    except (ValueError, TypeError):
+                        # 如果年龄格式不正确,保持原值
+                        pass
+                
+                existing_card.native_place = talent_data.get('native_place', existing_card.native_place)
+                existing_card.residence = talent_data.get('residence', existing_card.residence)
+                existing_card.brand_group = talent_data.get('brand_group', existing_card.brand_group)
+                # 更新image_path字段,从talent_data中获取
+                existing_card.image_path = talent_data.get('image_path', existing_card.image_path)
+                existing_card.origin_source = talent_data.get('origin_source', existing_card.origin_source)
+                existing_card.talent_profile = talent_data.get('talent_profile', existing_card.talent_profile)
+                existing_card.updated_by = 'talent_system'
+                
+                # 更新职业轨迹,传递从talent_data获取的图片路径
+                from app.core.data_parse.parse_system import update_career_path
+                image_path = talent_data.get('image_path', '')
+                existing_card.career_path = update_career_path(existing_card, talent_data, image_path)
+                
+                db.session.commit()
+                
+                logging.info(f"已更新现有人才记录,ID: {existing_card.id}")
+                
+                return {
+                    'code': 200,
+                    'success': True,
+                    'message': f'人才信息已更新。{duplicate_check["reason"]}',
+                    'data': existing_card.to_dict()
+                }
+                
+            elif duplicate_check['action'] == 'create_with_duplicates':
+                # 创建新记录作为主记录,并保存疑似重复记录信息
+                from app.core.data_parse.parse_system import create_main_card_with_duplicates
+                main_card, duplicate_record = create_main_card_with_duplicates(
+                    talent_data, 
+                    talent_data.get('image_path', ''),  # 从talent_data获取图片路径
+                    duplicate_check['suspected_duplicates'],
+                    duplicate_check['reason']
+                )
+                
+                return {
+                    'code': 202,  # Accepted,表示已接受但需要进一步处理
+                    'success': True,
+                    'message': f'创建新记录成功,发现疑似重复记录待处理。{duplicate_check["reason"]}',
+                    'data': {
+                        'main_card': main_card.to_dict(),
+                        'duplicate_record_id': duplicate_record.id,
+                        'suspected_duplicates_count': len(duplicate_check['suspected_duplicates']),
+                        'processing_status': 'pending',
+                        'duplicate_reason': duplicate_record.duplicate_reason,
+                        'created_at': duplicate_record.created_at.strftime('%Y-%m-%d %H:%M:%S')
+                    }
+                }
+                
+            else:
+                # 创建新记录
+                # 准备初始职业轨迹,包含从talent_data获取的图片路径
+                image_path = talent_data.get('image_path', '')
+                initial_entry = {
+                    'date': datetime.now().strftime('%Y-%m-%d'),
+                    'hotel_zh': talent_data.get('hotel_zh', ''),
+                    'hotel_en': talent_data.get('hotel_en', ''),
+                    'title_zh': talent_data.get('title_zh', ''),
+                    'title_en': talent_data.get('title_en', ''),
+                    'image_path': image_path,  # 从talent_data获取图片路径
+                    'source': 'talent_data_creation'
+                }
+                initial_career_path = [initial_entry]
+                
+                # 导入手机号码处理函数和BusinessCard模型
+                from app.core.data_parse.parse_system import normalize_mobile_numbers, BusinessCard
+                
+                # 处理年龄字段,确保是有效的整数或None
+                age_value = None
+                if talent_data.get('age'):
+                    try:
+                        age_value = int(talent_data.get('age'))
+                        if age_value <= 0 or age_value > 150:  # 合理的年龄范围检查
+                            age_value = None
+                    except (ValueError, TypeError):
+                        age_value = None
+                
+                business_card = BusinessCard(
+                    name_zh=talent_data.get('name_zh', ''),
+                    name_en=talent_data.get('name_en', ''),
+                    title_zh=talent_data.get('title_zh', ''),
+                    title_en=talent_data.get('title_en', ''),
+                    mobile=normalize_mobile_numbers(talent_data.get('mobile', '')),
+                    phone=talent_data.get('phone', ''),
+                    email=talent_data.get('email', ''),
+                    hotel_zh=talent_data.get('hotel_zh', ''),
+                    hotel_en=talent_data.get('hotel_en', ''),
+                    address_zh=talent_data.get('address_zh', ''),
+                    address_en=talent_data.get('address_en', ''),
+                    postal_code_zh=talent_data.get('postal_code_zh', ''),
+                    postal_code_en=talent_data.get('postal_code_en', ''),
+                    brand_zh=talent_data.get('brand_zh', ''),
+                    brand_en=talent_data.get('brand_en', ''),
+                    affiliation_zh=talent_data.get('affiliation_zh', ''),
+                    affiliation_en=talent_data.get('affiliation_en', ''),
+                    birthday=datetime.strptime(talent_data.get('birthday'), '%Y-%m-%d').date() if talent_data.get('birthday') else None,
+                    age=age_value,
+                    native_place=talent_data.get('native_place', ''),
+                    residence=talent_data.get('residence', ''),
+                    image_path=image_path,  # 从talent_data获取图片路径
+                    career_path=initial_career_path,
+                    brand_group=talent_data.get('brand_group', ''),
+                    origin_source=talent_data.get('origin_source'),
+                    talent_profile=talent_data.get('talent_profile', ''),
+                    status='active',
+                    updated_by='talent_system'
+                )
+                
+                db.session.add(business_card)
+                db.session.commit()
+                
+                logging.info(f"人才信息已保存到数据库,ID: {business_card.id}")
+                
+                return {
+                    'code': 200,
+                    'success': True,
+                    'message': f'人才信息保存成功。{duplicate_check["reason"]}',
+                    'data': business_card.to_dict()
+                }
+                
+        except Exception as e:
+            db.session.rollback()
+            error_msg = f"保存人才信息到数据库失败: {str(e)}"
+            logging.error(error_msg, exc_info=True)
+            
+            return {
+                'code': 500,
+                'success': False,
+                'message': error_msg,
+                'data': None
+            }
+            
+    except Exception as e:
+        db.session.rollback()
+        error_msg = f"人才处理失败: {str(e)}"
+        logging.error(error_msg, exc_info=True)
+        
+        return {
+            'code': 500,
+            'success': False,
+            'message': error_msg,
+            'data': None
+        }
+
+
+def add_parsed_talents(api_response_data):
+    """
+    处理execute-parse-task API响应数据,提取人才信息并写入business_cards表
+    
+    Args:
+        api_response_data (dict): execute-parse-task API的返回数据
+        
+    Returns:
+        dict: 批量处理结果,格式与其他batch函数保持一致
+    """
+    try:
+        # 验证参数
+        if not api_response_data or not isinstance(api_response_data, dict):
+            return {
+                'code': 400,
+                'success': False,
+                'message': 'api_response_data参数必须是非空字典',
+                'data': None
+            }
+        
+        # 检查API响应是否成功
+        if not api_response_data.get('success', False):
+            return {
+                'code': 400,
+                'success': False,
+                'message': f"API响应表示处理失败: {api_response_data.get('message', '未知错误')}",
+                'data': None
+            }
+        
+        # 获取data字段
+        response_data = api_response_data.get('data')
+        if not response_data or not isinstance(response_data, dict):
+            return {
+                'code': 400,
+                'success': False,
+                'message': 'API响应中缺少有效的data字段',
+                'data': None
+            }
+        
+        # 获取results数组
+        results = response_data.get('results', [])
+        if not isinstance(results, list):
+            return {
+                'code': 400,
+                'success': False,
+                'message': 'API响应中的results字段必须是数组',
+                'data': None
+            }
+        
+        if len(results) == 0:
+            return {
+                'code': 400,
+                'success': False,
+                'message': 'API响应中的results数组为空,没有人才数据需要处理',
+                'data': None
+            }
+        
+        logging.info(f"开始处理API响应中的人才数据,共 {len(results)} 条记录")
+        
+        processed_results = []
+        success_count = 0
+        failed_count = 0
+        
+        # 逐一处理每个结果项
+        for i, result_item in enumerate(results):
+            try:
+                logging.debug(f"处理第 {i+1}/{len(results)} 条记录")
+                
+                # 检查结果项是否成功
+                if not result_item.get('success', False):
+                    failed_count += 1
+                    processed_results.append({
+                        'index': i,
+                        'original_index': result_item.get('index', i),
+                        'success': False,
+                        'error': f"原始解析失败: {result_item.get('error', '未知错误')}",
+                        'data': None
+                    })
+                    logging.warning(f"第 {i+1} 条记录原始解析失败,跳过处理")
+                    continue
+                
+                # 获取人才数据
+                item_data = result_item.get('data')
+                if not item_data:
+                    failed_count += 1
+                    processed_results.append({
+                        'index': i,
+                        'original_index': result_item.get('index', i),
+                        'success': False,
+                        'error': '结果项中缺少data字段',
+                        'data': None
+                    })
+                    logging.warning(f"第 {i+1} 条记录缺少data字段")
+                    continue
+                
+                # 处理不同的数据格式
+                talent_data = None
+                
+                # 检查是否是批量解析结果(如新任命等,包含多个人员)
+                if isinstance(item_data, dict):
+                    if 'all_results' in item_data and isinstance(item_data['all_results'], list):
+                        # 新任命等批量数据格式,包含多个人员
+                        all_talents = item_data['all_results']
+                        logging.info(f"第 {i+1} 条记录包含 {len(all_talents)} 个人员信息")
+                        
+                        # 处理每个人员
+                        for j, single_talent in enumerate(all_talents):
+                            try:
+                                talent_result = add_single_talent(single_talent)
+                                if talent_result.get('success', False):
+                                    success_count += 1
+                                    processed_results.append({
+                                        'index': i,
+                                        'original_index': result_item.get('index', i),
+                                        'sub_index': j,
+                                        'success': True,
+                                        'error': None,
+                                        'data': talent_result.get('data'),
+                                        'message': f'成功处理人员: {single_talent.get("name_zh", "未知")}'
+                                    })
+                                    logging.debug(f"成功处理第 {i+1} 条记录中的第 {j+1} 个人员")
+                                else:
+                                    failed_count += 1
+                                    processed_results.append({
+                                        'index': i,
+                                        'original_index': result_item.get('index', i),
+                                        'sub_index': j,
+                                        'success': False,
+                                        'error': talent_result.get('message', '处理失败'),
+                                        'data': None
+                                    })
+                                    logging.error(f"处理第 {i+1} 条记录中的第 {j+1} 个人员失败")
+                            except Exception as talent_error:
+                                failed_count += 1
+                                error_msg = f"处理人员数据异常: {str(talent_error)}"
+                                processed_results.append({
+                                    'index': i,
+                                    'original_index': result_item.get('index', i),
+                                    'sub_index': j,
+                                    'success': False,
+                                    'error': error_msg,
+                                    'data': None
+                                })
+                                logging.error(error_msg, exc_info=True)
+                        continue
+                    else:
+                        # 单个人员数据格式
+                        talent_data = item_data
+                elif isinstance(item_data, list) and len(item_data) > 0:
+                    # 如果是数组,取第一个元素
+                    talent_data = item_data[0]
+                else:
+                    failed_count += 1
+                    processed_results.append({
+                        'index': i,
+                        'original_index': result_item.get('index', i),
+                        'success': False,
+                        'error': 'data字段格式不正确,无法识别人才数据',
+                        'data': None
+                    })
+                    logging.warning(f"第 {i+1} 条记录data字段格式不正确")
+                    continue
+                
+                # 处理单个人才数据
+                if talent_data:
+                    try:
+                        talent_result = add_single_talent(talent_data)
+                        if talent_result.get('success', False):
+                            success_count += 1
+                            processed_results.append({
+                                'index': i,
+                                'original_index': result_item.get('index', i),
+                                'success': True,
+                                'error': None,
+                                'data': talent_result.get('data'),
+                                'message': f'成功处理人员: {talent_data.get("name_zh", "未知")}'
+                            })
+                            logging.debug(f"成功处理第 {i+1} 条记录")
+                        else:
+                            failed_count += 1
+                            processed_results.append({
+                                'index': i,
+                                'original_index': result_item.get('index', i),
+                                'success': False,
+                                'error': talent_result.get('message', '处理失败'),
+                                'data': None
+                            })
+                            logging.error(f"处理第 {i+1} 条记录失败: {talent_result.get('message', '未知错误')}")
+                    except Exception as talent_error:
+                        failed_count += 1
+                        error_msg = f"处理人才数据异常: {str(talent_error)}"
+                        processed_results.append({
+                            'index': i,
+                            'original_index': result_item.get('index', i),
+                            'success': False,
+                            'error': error_msg,
+                            'data': None
+                        })
+                        logging.error(error_msg, exc_info=True)
+                    
+            except Exception as item_error:
+                failed_count += 1
+                error_msg = f"处理结果项异常: {str(item_error)}"
+                processed_results.append({
+                    'index': i,
+                    'original_index': result_item.get('index', i),
+                    'success': False,
+                    'error': error_msg,
+                    'data': None
+                })
+                logging.error(error_msg, exc_info=True)
+        
+        # 组装最终结果
+        batch_result = {
+            'summary': {
+                'total_files': len(results),
+                'success_count': success_count,
+                'failed_count': failed_count,
+                'success_rate': round((success_count / len(results)) * 100, 2) if len(results) > 0 else 0,
+                'original_summary': response_data.get('summary', {})
+            },
+            'results': processed_results,
+            'processed_time': datetime.now().isoformat(),
+            'original_api_response': api_response_data  # 保留原始API响应用于调试
+        }
+        
+        if failed_count == 0:
+            return {
+                'code': 200,
+                'success': True,
+                'message': f'批量处理完成,全部 {success_count} 条人才数据写入成功',
+                'data': batch_result
+            }
+        elif success_count == 0:
+            return {
+                'code': 500,
+                'success': False,
+                'message': f'批量处理失败,全部 {failed_count} 条人才数据写入失败',
+                'data': batch_result
+            }
+        else:
+            return {
+                'code': 206,  # Partial Content
+                'success': True,
+                'message': f'批量处理部分成功,成功 {success_count} 条,失败 {failed_count} 条',
+                'data': batch_result
+            }
+            
+    except Exception as e:
+        error_msg = f"处理API响应数据失败: {str(e)}"
+        logging.error(error_msg, exc_info=True)
+        
         return {
             'code': 500,
             'success': False,

+ 63 - 50
app/core/data_parse/parse_web.py

@@ -757,7 +757,7 @@ def batch_process_md(markdown_file_list, publish_time):
         publish_time (str): 发布时间,用于career_path中的date字段
         
     Returns:
-        dict: 批量处理结果,包含所有人员的解析结果
+        dict: 批量处理结果,格式与batch_process_business_card_images保持一致
     """
     try:
         # 参数验证
@@ -779,80 +779,93 @@ def batch_process_md(markdown_file_list, publish_time):
         
         logging.info(f"开始批量处理 {len(markdown_file_list)} 个markdown文件")
         
-        # 批量处理结果统计
-        batch_results = {
-            'total_files': len(markdown_file_list),
-            'processed_files': 0,
-            'failed_files': 0,
-            'total_sections': 0,
-            'total_persons': 0,
-            'all_results': [],
-            'file_results': [],
-            'failed_files_info': []
-        }
+        results = []
+        success_count = 0
+        failed_count = 0
+        total_persons = 0
         
         # 逐个处理每个markdown文件
-        for file_index, minio_path in enumerate(markdown_file_list):
+        for i, minio_path in enumerate(markdown_file_list):
             try:
-                logging.info(f"开始处理第 {file_index + 1} 个文件: {minio_path}")
+                logging.info(f"处理第 {i+1}/{len(markdown_file_list)} 个文件: {minio_path}")
                 
                 # 处理单个文件
                 file_result = process_single_markdown_file(minio_path, publish_time)
                 
-                if file_result['success']:
-                    batch_results['processed_files'] += 1
-                    batch_results['total_sections'] += file_result['data']['total_sections']
-                    batch_results['total_persons'] += file_result['data']['total_persons']
-                    batch_results['all_results'].extend(file_result['data']['all_results'])
-                    batch_results['file_results'].append({
-                        'file_index': file_index + 1,
+                if file_result.get('success', False):
+                    success_count += 1
+                    persons_count = file_result.get('data', {}).get('total_persons', 0)
+                    total_persons += persons_count
+                    
+                    results.append({
+                        'index': i,
                         'minio_path': minio_path,
-                        'result': file_result['data']
+                        'filename': minio_path.split('/')[-1] if '/' in minio_path else minio_path,
+                        'success': True,
+                        'error': None,
+                        'data': file_result.get('data'),
+                        'message': f'处理成功,提取 {persons_count} 个人员信息'
                     })
-                    logging.info(f"文件 {minio_path} 处理成功,提取 {file_result['data']['total_persons']} 个人员信息")
+                    logging.info(f"成功处理第 {i+1} 个文件: {minio_path},提取 {persons_count} 个人员信息")
                 else:
-                    batch_results['failed_files'] += 1
-                    batch_results['failed_files_info'].append({
-                        'file_index': file_index + 1,
+                    failed_count += 1
+                    error_msg = file_result.get('message', '处理失败')
+                    results.append({
+                        'index': i,
                         'minio_path': minio_path,
-                        'error': file_result['message']
+                        'filename': minio_path.split('/')[-1] if '/' in minio_path else minio_path,
+                        'success': False,
+                        'error': error_msg,
+                        'data': None
                     })
-                    logging.error(f"文件 {minio_path} 处理失败: {file_result['message']}")
+                    logging.error(f"处理第 {i+1} 个文件失败: {error_msg}")
                     
-            except Exception as e:
-                error_msg = f"处理文件 {minio_path} 时发生异常: {str(e)}"
+            except Exception as item_error:
+                failed_count += 1
+                error_msg = f"处理markdown文件失败: {str(item_error)}"
                 logging.error(error_msg, exc_info=True)
-                batch_results['failed_files'] += 1
-                batch_results['failed_files_info'].append({
-                    'file_index': file_index + 1,
+                results.append({
+                    'index': i,
                     'minio_path': minio_path,
-                    'error': error_msg
+                    'filename': minio_path.split('/')[-1] if '/' in minio_path else minio_path,
+                    'success': False,
+                    'error': error_msg,
+                    'data': None
                 })
         
-        # 生成最终结果
-        if batch_results['processed_files'] == batch_results['total_files']:
-            # 全部处理成功
+        # 组装最终结果
+        batch_result = {
+            'summary': {
+                'total_files': len(markdown_file_list),
+                'success_count': success_count,
+                'failed_count': failed_count,
+                'success_rate': round((success_count / len(markdown_file_list)) * 100, 2) if len(markdown_file_list) > 0 else 0,
+                'total_persons': total_persons
+            },
+            'results': results,
+            'processed_time': datetime.now().isoformat()
+        }
+        
+        if failed_count == 0:
             return {
                 'code': 200,
                 'success': True,
-                'message': f'所有 {batch_results["total_files"]} 个文件处理成功,共提取 {batch_results["total_persons"]} 个人员信息',
-                'data': batch_results
+                'message': f'批量处理完成,全部 {success_count} 个文件处理成功,共提取 {total_persons} 个人员信息',
+                'data': batch_result
             }
-        elif batch_results['processed_files'] > 0:
-            # 部分处理成功
+        elif success_count == 0:
             return {
-                'code': 206,  # Partial Content
-                'success': True,
-                'message': f'部分处理成功:{batch_results["processed_files"]}/{batch_results["total_files"]} 个文件成功,共提取 {batch_results["total_persons"]} 个人员信息',
-                'data': batch_results
+                'code': 500,
+                'success': False,
+                'message': f'批量处理失败,全部 {failed_count} 个文件处理失败',
+                'data': batch_result
             }
         else:
-            # 全部处理失败
             return {
-                'code': 500,
-                'success': False,
-                'message': f'所有 {batch_results["total_files"]} 个文件处理失败',
-                'data': batch_results
+                'code': 206,  # Partial Content
+                'success': True,
+                'message': f'批量处理部分成功,成功 {success_count} 个,失败 {failed_count} 个,共提取 {total_persons} 个人员信息',
+                'data': batch_result
             }
             
     except Exception as e:

BIN
吴娟.jpg


BIN
庞涛.png