Sfoglia il codice sorgente

新增接口get-parsed-talents,用于获取已解析的人才记录

maxiaolong 3 settimane fa
parent
commit
26544b6994

+ 81 - 7
app/api/data_parse/routes.py

@@ -1326,12 +1326,6 @@ def fix_broken_duplicate_records_route():
             'data': None
         }), 500
 
-
-
-
-
-
-
 # 获取解析任务列表接口
 @bp.route('/get-parse-tasks', methods=['GET'])
 def get_parse_tasks_route():
@@ -1705,7 +1699,7 @@ def execute_parse_task():
             if result.get('success'):
                 logging.info(f"执行{task_type}解析任务成功: {result.get('message', '')}")
                 # ===== 精简:只根据id字段唯一定位任务记录 =====
-                from app.core.data_parse.parse_system import db, ParseTaskRepository
+                from app.core.data_parse.parse_system import db, ParseTaskRepository, record_parsed_talents
                 task_id = data.get('id')
                 if task_id:
                     task_obj = ParseTaskRepository.query.filter_by(id=task_id).first()
@@ -1714,6 +1708,23 @@ def execute_parse_task():
                         task_obj.parse_result = result.get('data')
                         db.session.commit()
                         logging.info(f"已更新解析任务记录: id={getattr(task_obj, 'id', None)}, 状态=成功")
+                
+                # 调用record_parsed_talents函数将解析结果写入parsed_talents表
+                try:
+                    # 为result添加任务信息
+                    result_with_task_info = result.copy()
+                    if 'data' in result_with_task_info:
+                        result_with_task_info['data'] = result_with_task_info['data'].copy() if isinstance(result_with_task_info['data'], dict) else {}
+                        result_with_task_info['data']['task_id'] = str(task_id) if task_id else ''
+                        result_with_task_info['data']['task_type'] = task_type
+                    
+                    record_result = record_parsed_talents(result_with_task_info)
+                    if record_result.get('success'):
+                        logging.info(f"成功将解析结果写入parsed_talents表: {record_result.get('message', '')}")
+                    else:
+                        logging.warning(f"写入parsed_talents表失败: {record_result.get('message', '')}")
+                except Exception as record_error:
+                    logging.error(f"调用record_parsed_talents函数失败: {str(record_error)}")
             else:
                 logging.error(f"执行{task_type}解析任务失败: {result.get('message', '')}")
             
@@ -1904,3 +1915,66 @@ def add_parsed_talents_route():
             'data': None
         }), 500
 
+
+@bp.route('/get-parsed-talents', methods=['GET'])
+def get_parsed_talents_route():
+    """
+    获取解析人才记录列表接口
+    
+    请求参数:
+        - status (str, optional): 状态过滤参数,如果为空则查询所有记录
+        
+    请求示例:
+        GET /get-parsed-talents?status=待审核
+        GET /get-parsed-talents (查询所有记录)
+        
+    返回:
+        - JSON: 包含人才记录列表和状态信息
+        - 200: 成功获取数据
+        - 500: 服务器内部错误
+    """
+    try:
+        # 获取查询参数
+        status = request.args.get('status', '').strip()
+        
+        # 调用核心业务逻辑
+        from app.core.data_parse.parse_system import get_parsed_talents
+        result = get_parsed_talents(status)
+        
+        # 根据处理结果设置HTTP状态码
+        if result.get('success', False):
+            status_code = result.get('code', 200)
+        else:
+            status_code = result.get('code', 500)
+        
+        # 记录处理结果日志
+        if result.get('success'):
+            count = result.get('count', 0)
+            if status:
+                logging.info(f"成功获取状态为 '{status}' 的解析人才记录: {count} 条")
+            else:
+                logging.info(f"成功获取所有解析人才记录: {count} 条")
+        else:
+            logging.error(f"获取解析人才记录失败: {result.get('message', '未知错误')}")
+        
+        # 返回结果
+        return jsonify({
+            'success': result.get('success', False),
+            'message': result.get('message', '处理完成'),
+            'data': result.get('data', []),
+            'count': result.get('count', 0)
+        }), status_code
+        
+    except Exception as e:
+        # 记录错误日志
+        error_msg = f"获取解析人才记录接口失败: {str(e)}"
+        logging.error(error_msg, exc_info=True)
+        
+        # 返回错误响应
+        return jsonify({
+            'success': False,
+            'message': error_msg,
+            'data': [],
+            'count': 0
+        }), 500
+

+ 6 - 3
app/core/data_parse/parse_card.py

@@ -17,7 +17,8 @@ import base64
 from app.core.data_parse.parse_system import (
     BusinessCard, DuplicateBusinessCard,
     parse_text_with_qwen25VLplus, check_duplicate_business_card,
-    update_career_path, create_main_card_with_duplicates
+    update_career_path, create_main_card_with_duplicates,
+    create_origin_source_entry, update_origin_source
 )
 
 from openai import OpenAI  # 添加此行以导入 OpenAI 客户端
@@ -275,7 +276,9 @@ def add_business_card(card_data, image_file=None):
                 existing_card.residence = card_data.get('residence', existing_card.residence)
                 existing_card.brand_group = card_data.get('brand_group', existing_card.brand_group)
                 existing_card.image_path = minio_path  # 更新为最新的图片路径
-                existing_card.origin_source = card_data.get('origin_source', existing_card.origin_source)  # 更新原始资料记录
+                # 更新origin_source字段,将新的记录添加到JSON数组中
+                from app.core.data_parse.parse_system import update_origin_source
+                existing_card.origin_source = update_origin_source(existing_card.origin_source, 'business_card_update', minio_path)
                 existing_card.talent_profile = card_data.get('talent_profile', existing_card.talent_profile)  # 更新人才档案
                 existing_card.updated_by = 'system'
                 
@@ -369,7 +372,7 @@ def add_business_card(card_data, image_file=None):
                     image_path=minio_path,  # 最新的图片路径
                     career_path=initial_career_path,  # 包含图片路径的职业轨迹
                     brand_group=card_data.get('brand_group', ''),
-                    origin_source=card_data.get('origin_source'),  # 原始资料记录
+                    origin_source=[create_origin_source_entry('business_card_creation', minio_path)],  # 原始资料记录
                     talent_profile=card_data.get('talent_profile', ''),  # 人才档案
                     status='active',
                     updated_by='system'

+ 341 - 2
app/core/data_parse/parse_system.py

@@ -89,6 +89,82 @@ class BusinessCard(db.Model):
         }
 
 
+# 解析人才数据模型
+class ParsedTalent(db.Model):
+    __tablename__ = 'parsed_talents'
+    
+    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
+    name_zh = db.Column(db.String(100), nullable=False)
+    name_en = db.Column(db.String(100))
+    title_zh = db.Column(db.String(100))
+    title_en = db.Column(db.String(100))
+    mobile = db.Column(db.String(50))
+    phone = db.Column(db.String(50))
+    email = db.Column(db.String(100))
+    hotel_zh = db.Column(db.String(200))
+    hotel_en = db.Column(db.String(200))
+    address_zh = db.Column(db.Text)
+    address_en = db.Column(db.Text)
+    postal_code_zh = db.Column(db.String(20))
+    postal_code_en = db.Column(db.String(20))
+    brand_zh = db.Column(db.String(100))
+    brand_en = db.Column(db.String(100))
+    affiliation_zh = db.Column(db.String(200))
+    affiliation_en = db.Column(db.String(200))
+    image_path = db.Column(db.String(255))
+    career_path = db.Column(db.JSON)
+    brand_group = db.Column(db.String(200))
+    created_at = db.Column(db.DateTime, default=datetime.now, nullable=False)
+    updated_at = db.Column(db.DateTime, onupdate=datetime.now)
+    updated_by = db.Column(db.String(50))
+    status = db.Column(db.String(20), default='active')
+    birthday = db.Column(db.Date)
+    residence = db.Column(db.Text)
+    age = db.Column(db.Integer)
+    native_place = db.Column(db.Text)
+    origin_source = db.Column(db.JSON)
+    talent_profile = db.Column(db.Text)
+    task_id = db.Column(db.String(50))
+    task_type = db.Column(db.String(20))
+    
+    def to_dict(self):
+        return {
+            'id': self.id,
+            'name_zh': self.name_zh,
+            'name_en': self.name_en,
+            'title_zh': self.title_zh,
+            'title_en': self.title_en,
+            'mobile': self.mobile,
+            'phone': self.phone,
+            'email': self.email,
+            'hotel_zh': self.hotel_zh,
+            'hotel_en': self.hotel_en,
+            'address_zh': self.address_zh,
+            'address_en': self.address_en,
+            'postal_code_zh': self.postal_code_zh,
+            'postal_code_en': self.postal_code_en,
+            'brand_zh': self.brand_zh,
+            'brand_en': self.brand_en,
+            'affiliation_zh': self.affiliation_zh,
+            'affiliation_en': self.affiliation_en,
+            'image_path': self.image_path,
+            'career_path': self.career_path,
+            'brand_group': self.brand_group,
+            'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None,
+            'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') if self.updated_at else None,
+            'updated_by': self.updated_by,
+            'status': self.status,
+            'birthday': self.birthday.strftime('%Y-%m-%d') if self.birthday else None,
+            'residence': self.residence,
+            'age': self.age,
+            'native_place': self.native_place,
+            'origin_source': self.origin_source,
+            'talent_profile': self.talent_profile,
+            'task_id': self.task_id,
+            'task_type': self.task_type
+        }
+
+
 # 重复名片处理数据模型
 class DuplicateBusinessCard(db.Model):
     __tablename__ = 'duplicate_business_cards'
@@ -470,7 +546,7 @@ def create_main_card_with_duplicates(extracted_data, minio_path, suspected_dupli
             brand_group=extracted_data.get('brand_group', ''),
             image_path=minio_path,
             career_path=career_path,
-            origin_source={'source': 'manual_upload', 'timestamp': datetime.now().isoformat()},
+            origin_source=[create_origin_source_entry('manual_upload', minio_path)],
             created_at=datetime.now(),
             updated_by='system',
             status='active'
@@ -2058,4 +2134,267 @@ def parse_text_with_qwen25VLplus(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 record_parsed_talents(result):
+    """
+    将解析结果写入parsed_talents数据库表
+    
+    Args:
+        result (dict): 解析任务的结果数据,包含解析成功的人才信息
+        
+    Returns:
+        dict: 包含操作结果的字典
+    """
+    try:
+        # 检查结果是否成功
+        if not result.get('success'):
+            return {
+                'code': 400,
+                'success': False,
+                'message': '解析任务未成功,无法记录人才数据',
+                'data': None
+            }
+        
+        # 获取解析数据
+        parse_data = result.get('data', {})
+        if not parse_data:
+            return {
+                'code': 400,
+                'success': False,
+                'message': '解析结果中没有数据',
+                'data': None
+            }
+        
+        # 提取任务信息
+        task_id = parse_data.get('task_id', '')
+        task_type = parse_data.get('task_type', '')
+        
+        # 处理不同格式的解析结果
+        talent_records = []
+        
+        # 检查是否有results字段(批量处理结果)
+        if 'results' in parse_data:
+            results = parse_data['results']
+            for item in results:
+                if isinstance(item, dict) and item.get('success') and item.get('data'):
+                    talent_data = item['data']
+                    if isinstance(talent_data, dict):
+                        talent_records.append(talent_data)
+        # 检查是否有data字段且为列表
+        elif isinstance(parse_data.get('data'), list):
+            talent_records = parse_data['data']
+        # 检查是否直接是人才数据字典
+        elif isinstance(parse_data, dict) and parse_data.get('name_zh'):
+            talent_records = [parse_data]
+        
+        if not talent_records:
+            return {
+                'code': 400,
+                'success': False,
+                'message': '未找到有效的人才数据',
+                'data': None
+            }
+        
+        # 批量创建ParsedTalent记录
+        created_records = []
+        failed_records = []
+        
+        for talent_data in talent_records:
+            try:
+                # 提取ParsedTalent模型需要的字段
+                parsed_talent = ParsedTalent(
+                    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=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', ''),
+                    image_path=talent_data.get('image_path', ''),
+                    career_path=talent_data.get('career_path', []),
+                    brand_group=talent_data.get('brand_group', ''),
+                    birthday=talent_data.get('birthday'),
+                    residence=talent_data.get('residence', ''),
+                    age=talent_data.get('age'),
+                    native_place=talent_data.get('native_place', ''),
+                    origin_source=talent_data.get('origin_source', []),
+                    talent_profile=talent_data.get('talent_profile', ''),
+                    task_id=task_id,
+                    task_type=task_type,
+                    status='待审核',  # 统一设置为待审核状态
+                    created_at=datetime.now(),
+                    updated_by='system'
+                )
+                
+                # 添加到数据库会话
+                db.session.add(parsed_talent)
+                created_records.append(parsed_talent)
+                
+            except Exception as record_error:
+                logging.error(f"创建人才记录失败: {str(record_error)}")
+                failed_records.append({
+                    'data': talent_data,
+                    'error': str(record_error)
+                })
+        
+        # 提交数据库事务
+        if created_records:
+            db.session.commit()
+            logging.info(f"成功创建 {len(created_records)} 条人才记录")
+        
+        # 构建返回结果
+        result_data = {
+            'created_count': len(created_records),
+            'failed_count': len(failed_records),
+            'created_records': [record.to_dict() for record in created_records],
+            'failed_records': failed_records
+        }
+        
+        if failed_records:
+            return {
+                'code': 206,  # 部分成功
+                'success': True,
+                'message': f'成功创建 {len(created_records)} 条记录,失败 {len(failed_records)} 条',
+                'data': result_data
+            }
+        else:
+            return {
+                'code': 200,
+                'success': True,
+                'message': f'成功创建 {len(created_records)} 条人才记录',
+                'data': result_data
+            }
+            
+    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 get_parsed_talents(status=None):
+    """
+    获取解析人才记录列表
+    
+    Args:
+        status (str, optional): 状态过滤参数,如果为空则查询所有记录
+        
+    Returns:
+        dict: 包含操作结果和人才记录列表的字典
+    """
+    try:
+        # 构建查询
+        query = ParsedTalent.query
+        
+        # 如果提供了status参数,则添加状态过滤条件
+        if status and status.strip():
+            query = query.filter_by(status=status.strip())
+        
+        # 按创建时间倒序排列
+        parsed_talents = query.order_by(ParsedTalent.created_at.desc()).all()
+        
+        # 转换为字典格式
+        talents_data = [talent.to_dict() for talent in parsed_talents]
+        
+        return {
+            'code': 200,
+            'success': True,
+            'message': f'成功获取 {len(talents_data)} 条解析人才记录',
+            'data': talents_data,
+            'count': len(talents_data)
+        }
+    
+    except Exception as e:
+        error_msg = f"获取解析人才记录失败: {str(e)}"
+        logging.error(error_msg, exc_info=True)
+        
+        return {
+            'code': 500,
+            'success': False,
+            'message': error_msg,
+            'data': [],
+            'count': 0
+        } 
+
+
+def create_origin_source_entry(task_type, minio_path):
+    """
+    创建origin_source字段的单个记录
+    
+    Args:
+        task_type (str): 任务类型
+        minio_path (str): MinIO路径
+        
+    Returns:
+        dict: 包含task_type、minio_path和source_date的记录
+    """
+    return {
+        'task_type': task_type,
+        'minio_path': minio_path,
+        'source_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+    }
+
+
+def update_origin_source(existing_origin_source, task_type, minio_path):
+    """
+    更新origin_source字段,将新的记录添加到JSON数组中
+    
+    Args:
+        existing_origin_source: 现有的origin_source内容
+        task_type (str): 任务类型
+        minio_path (str): MinIO路径
+        
+    Returns:
+        list: 更新后的origin_source JSON数组
+    """
+    try:
+        # 解析现有的origin_source
+        if existing_origin_source:
+            if isinstance(existing_origin_source, str):
+                origin_list = json.loads(existing_origin_source)
+            elif isinstance(existing_origin_source, list):
+                origin_list = existing_origin_source
+            elif isinstance(existing_origin_source, dict):
+                # 如果是单个对象,转换为数组
+                origin_list = [existing_origin_source]
+            else:
+                origin_list = []
+        else:
+            origin_list = []
+        
+        # 确保origin_list是列表
+        if not isinstance(origin_list, list):
+            origin_list = [origin_list] if origin_list else []
+        
+        # 创建新的记录
+        new_entry = create_origin_source_entry(task_type, minio_path)
+        
+        # 检查是否已存在相同的minio_path记录
+        existing_paths = [entry.get('minio_path') for entry in origin_list if isinstance(entry, dict)]
+        if minio_path not in existing_paths:
+            origin_list.append(new_entry)
+        
+        return origin_list
+        
+    except Exception as e:
+        logging.error(f"更新origin_source失败: {str(e)}")
+        # 如果处理失败,返回包含新记录的数组
+        return [create_origin_source_entry(task_type, minio_path)]

+ 43 - 2
app/core/data_parse/parse_task.py

@@ -655,10 +655,12 @@ def _update_origin_source_with_minio_path(existing_origin_source, minio_path, ta
         
         # 如果minio_path不为空,则创建新的JSON对象并添加到列表中
         if minio_path:
-            # 创建新的JSON对象,格式为 {task_type: "任务类型", minio_path: "路径"}
+            # 创建新的JSON对象,格式为 {task_type: "任务类型", minio_path: "路径", source_date: "时间"}
+            from datetime import datetime
             new_entry = {
                 "task_type": task_type if task_type else "",
-                "minio_path": minio_path
+                "minio_path": minio_path,
+                "source_date": datetime.now().strftime('%Y-%m-%d %H:%M:%S')
             }
             
             # 检查是否已存在相同的条目
@@ -814,6 +816,19 @@ def add_single_talent(talent_data, minio_path=None, task_type=None):
                     neo4j_node_id = create_or_get_node('Talent', **talent_properties)
                     logging.info(f"成功在Neo4j中更新Talent节点,Neo4j ID: {neo4j_node_id}, PostgreSQL ID: {existing_card.id}")
                     
+                    # 更新parsed_talents表中的对应记录状态
+                    if talent_data.get('id'):
+                        try:
+                            from app.core.data_parse.parse_system import ParsedTalent
+                            parsed_talent = ParsedTalent.query.filter_by(id=talent_data['id']).first()
+                            if parsed_talent:
+                                parsed_talent.status = '已入库'
+                                db.session.commit()
+                                logging.info(f"已更新parsed_talents表记录状态为已入库,ID: {talent_data['id']}")
+                        except Exception as update_error:
+                            logging.error(f"更新parsed_talents表记录状态失败: {str(update_error)}")
+                            # 状态更新失败不影响主流程
+                    
                 except Exception as neo4j_error:
                     logging.error(f"在Neo4j中更新Talent节点失败: {str(neo4j_error)}")
                     # Neo4j操作失败不影响主流程,继续返回成功结果
@@ -859,6 +874,19 @@ def add_single_talent(talent_data, minio_path=None, task_type=None):
                     neo4j_node_id = create_or_get_node('Talent', **talent_properties)
                     logging.info(f"成功在Neo4j中创建Talent节点,Neo4j ID: {neo4j_node_id}, PostgreSQL ID: {main_card.id}")
                     
+                    # 更新parsed_talents表中的对应记录状态
+                    if talent_data.get('id'):
+                        try:
+                            from app.core.data_parse.parse_system import ParsedTalent
+                            parsed_talent = ParsedTalent.query.filter_by(id=talent_data['id']).first()
+                            if parsed_talent:
+                                parsed_talent.status = '已入库'
+                                db.session.commit()
+                                logging.info(f"已更新parsed_talents表记录状态为已入库,ID: {talent_data['id']}")
+                        except Exception as update_error:
+                            logging.error(f"更新parsed_talents表记录状态失败: {str(update_error)}")
+                            # 状态更新失败不影响主流程
+                    
                 except Exception as neo4j_error:
                     logging.error(f"在Neo4j中创建Talent节点失败: {str(neo4j_error)}")
                     # Neo4j操作失败不影响主流程,继续返回成功结果
@@ -959,6 +987,19 @@ def add_single_talent(talent_data, minio_path=None, task_type=None):
                     neo4j_node_id = create_or_get_node('Talent', **talent_properties)
                     logging.info(f"成功在Neo4j中创建Talent节点,Neo4j ID: {neo4j_node_id}, PostgreSQL ID: {business_card.id}")
                     
+                    # 更新parsed_talents表中的对应记录状态
+                    if talent_data.get('id'):
+                        try:
+                            from app.core.data_parse.parse_system import ParsedTalent
+                            parsed_talent = ParsedTalent.query.filter_by(id=talent_data['id']).first()
+                            if parsed_talent:
+                                parsed_talent.status = '已入库'
+                                db.session.commit()
+                                logging.info(f"已更新parsed_talents表记录状态为已入库,ID: {talent_data['id']}")
+                        except Exception as update_error:
+                            logging.error(f"更新parsed_talents表记录状态失败: {str(update_error)}")
+                            # 状态更新失败不影响主流程
+                    
                 except Exception as neo4j_error:
                     logging.error(f"在Neo4j中创建Talent节点失败: {str(neo4j_error)}")
                     # Neo4j操作失败不影响主流程,继续返回成功结果

+ 7 - 22
app/core/data_parse/parse_web.py

@@ -16,7 +16,8 @@ from app.config.config import DevelopmentConfig, ProductionConfig
 from app.core.data_parse.parse_system import (
     BusinessCard, check_duplicate_business_card, 
     create_main_card_with_duplicates, update_career_path,
-    normalize_mobile_numbers, ParseTaskRepository
+    normalize_mobile_numbers, ParseTaskRepository,
+    create_origin_source_entry, update_origin_source
 )
 from app import db
 
@@ -186,11 +187,7 @@ def add_webpage_talent(talent_list, web_md):
                     continue
                 
                 # 设置origin_source为原始资料记录
-                talent_data['origin_source'] = {
-                    'type': 'webpage_talent',
-                    'minio_path': minio_md_path,
-                    'source_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
-                }
+                talent_data['origin_source'] = [create_origin_source_entry('webpage_talent', minio_md_path)]
                 
                 # 处理business_card记录
                 card_result = process_single_talent_card(talent_data, minio_md_path)
@@ -459,12 +456,8 @@ def process_single_talent_card(talent_data, minio_md_path):
             if talent_data.get('talent_profile'):
                 existing_card.talent_profile = talent_data.get('talent_profile')
             
-            # 设置origin_source为原始资料记录
-            existing_card.origin_source = {
-                'type': 'webpage_talent',
-                'minio_path': minio_md_path,
-                'source_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
-            }
+            # 更新origin_source字段,将新的记录添加到JSON数组中
+            existing_card.origin_source = update_origin_source(existing_card.origin_source, 'webpage_talent', minio_md_path)
             
             # 更新职业轨迹,传递图片路径
             existing_card.career_path = update_career_path(existing_card, talent_data, image_path=image_path or '')
@@ -491,11 +484,7 @@ def process_single_talent_card(talent_data, minio_md_path):
             main_card.updated_by = 'webpage_talent_system'
             
             # 设置origin_source为原始资料记录
-            main_card.origin_source = {
-                'type': 'webpage_talent',
-                'minio_path': minio_md_path,
-                'source_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
-            }
+            main_card.origin_source = [create_origin_source_entry('webpage_talent', minio_md_path)]
             db.session.commit()
             
             return {
@@ -560,11 +549,7 @@ def process_single_talent_card(talent_data, minio_md_path):
                 image_path=image_path,  # 使用下载的图片路径
                 career_path=initial_career_path,
                 brand_group=talent_data.get('brand_group', ''),
-                origin_source={
-                    'type': 'webpage_talent',
-                    'minio_path': minio_md_path,
-                    'source_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
-                },
+                origin_source=[create_origin_source_entry('webpage_talent', minio_md_path)],
                 talent_profile=talent_data.get('talent_profile', ''),  # 人才档案
                 status='active',
                 updated_by='webpage_talent_system'

+ 430 - 0
get-parsed-talents-api-documentation.md

@@ -0,0 +1,430 @@
+# get-parsed-talents 接口使用说明书
+
+## 接口概述
+
+`get-parsed-talents` 接口用于获取解析人才记录列表,支持按状态过滤查询。该接口返回 `parsed_talents` 数据库表中的记录,按创建时间倒序排列。
+
+## 基本信息
+
+- **接口名称**: get-parsed-talents
+- **HTTP方法**: GET
+- **访问路径**: `/get-parsed-talents`
+- **接口描述**: 获取解析人才记录列表,支持状态过滤
+- **数据来源**: `parsed_talents` 数据库表
+
+## 请求参数
+
+### 查询参数 (Query Parameters)
+
+| 参数名 | 类型 | 必填 | 默认值 | 说明 |
+|--------|------|------|--------|------|
+| status | string | 否 | null | 状态过滤参数,如果为空则查询所有记录 |
+
+### 参数说明
+
+- **status**: 用于过滤特定状态的记录
+  - 可选值: `"待审核"`, `"已入库"`, `"已拒绝"`, `"active"`, `"inactive"` 等
+  - 如果参数为空或不传,则返回所有状态的记录
+  - 参数值不区分大小写,但建议使用标准状态值
+
+## 响应格式
+
+### 成功响应 (200 OK)
+
+```json
+{
+    "success": true,
+    "message": "成功获取 X 条解析人才记录",
+    "data": [
+        {
+            "id": 1,
+            "name_zh": "张三",
+            "name_en": "Zhang San",
+            "title_zh": "总经理",
+            "title_en": "General Manager",
+            "mobile": "13800138000",
+            "phone": "010-12345678",
+            "email": "zhangsan@example.com",
+            "hotel_zh": "北京希尔顿酒店",
+            "hotel_en": "Beijing Hilton Hotel",
+            "address_zh": "北京市朝阳区建国门外大街1号",
+            "address_en": "1 Jianguomenwai Street, Chaoyang District, Beijing",
+            "postal_code_zh": "100020",
+            "postal_code_en": "100020",
+            "brand_zh": "希尔顿",
+            "brand_en": "Hilton",
+            "affiliation_zh": "希尔顿酒店集团",
+            "affiliation_en": "Hilton Hotels & Resorts",
+            "image_path": "minio/talents/zhangsan.jpg",
+            "career_path": [
+                {
+                    "date": "2024-01-15",
+                    "hotel_zh": "北京希尔顿酒店",
+                    "hotel_en": "Beijing Hilton Hotel",
+                    "title_zh": "总经理",
+                    "title_en": "General Manager",
+                    "image_path": "minio/talents/zhangsan.jpg",
+                    "source": "webpage_extraction"
+                }
+            ],
+            "brand_group": "希尔顿集团",
+            "birthday": "1980-05-15",
+            "residence": "北京市朝阳区",
+            "age": 44,
+            "native_place": "北京市",
+            "origin_source": [
+                {
+                    "task_type": "webpage_talent",
+                    "minio_path": "minio/webpage/zhangsan.md",
+                    "source_date": "2024-01-15 14:30:25"
+                }
+            ],
+            "talent_profile": "资深酒店管理专家,拥有15年酒店管理经验...",
+            "task_id": "task_001",
+            "task_type": "webpage_talent",
+            "created_at": "2024-01-15T14:30:25",
+            "updated_at": "2024-01-15T14:30:25",
+            "updated_by": "system",
+            "status": "待审核"
+        }
+    ],
+    "count": 1
+}
+```
+
+### 错误响应 (500 Internal Server Error)
+
+```json
+{
+    "success": false,
+    "message": "获取解析人才记录失败: 数据库连接错误",
+    "data": [],
+    "count": 0
+}
+```
+
+## 响应字段说明
+
+### 顶层字段
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| success | boolean | 请求是否成功 |
+| message | string | 响应消息 |
+| data | array | 人才记录数组 |
+| count | integer | 记录总数 |
+
+### 人才记录字段 (data 数组中的对象)
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| id | integer | 记录唯一标识符 |
+| name_zh | string | 中文姓名 |
+| name_en | string | 英文姓名 |
+| title_zh | string | 中文职位 |
+| title_en | string | 英文职位 |
+| mobile | string | 手机号码 |
+| phone | string | 固定电话 |
+| email | string | 电子邮箱 |
+| hotel_zh | string | 中文酒店名称 |
+| hotel_en | string | 英文酒店名称 |
+| address_zh | string | 中文地址 |
+| address_en | string | 英文地址 |
+| postal_code_zh | string | 中文邮政编码 |
+| postal_code_en | string | 英文邮政编码 |
+| brand_zh | string | 中文品牌名称 |
+| brand_en | string | 英文品牌名称 |
+| affiliation_zh | string | 中文隶属关系 |
+| affiliation_en | string | 英文隶属关系 |
+| image_path | string | 图片文件路径 |
+| career_path | array | 职业轨迹数组 |
+| brand_group | string | 品牌组合 |
+| birthday | string | 生日 (YYYY-MM-DD格式) |
+| residence | string | 居住地 |
+| age | integer | 年龄 |
+| native_place | string | 籍贯 |
+| origin_source | array | 原始资料记录数组 |
+| talent_profile | string | 人才档案描述 |
+| task_id | string | 解析任务ID |
+| task_type | string | 解析任务类型 |
+| created_at | string | 创建时间 |
+| updated_at | string | 更新时间 |
+| updated_by | string | 更新人 |
+| status | string | 记录状态 |
+
+### career_path 数组字段
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| date | string | 职业记录日期 |
+| hotel_zh | string | 中文酒店名称 |
+| hotel_en | string | 英文酒店名称 |
+| title_zh | string | 中文职位 |
+| title_en | string | 英文职位 |
+| image_path | string | 相关图片路径 |
+| source | string | 数据来源 |
+
+### origin_source 数组字段
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| task_type | string | 任务类型 |
+| minio_path | string | MinIO存储路径 |
+| source_date | string | 数据来源时间 |
+
+## 使用示例
+
+### 1. 查询所有记录
+
+**请求**:
+```
+GET /get-parsed-talents
+```
+
+**响应**:
+```json
+{
+    "success": true,
+    "message": "成功获取 150 条解析人才记录",
+    "data": [...],
+    "count": 150
+}
+```
+
+### 2. 查询待审核记录
+
+**请求**:
+```
+GET /get-parsed-talents?status=待审核
+```
+
+**响应**:
+```json
+{
+    "success": true,
+    "message": "成功获取 25 条解析人才记录",
+    "data": [...],
+    "count": 25
+}
+```
+
+### 3. 查询已入库记录
+
+**请求**:
+```
+GET /get-parsed-talents?status=已入库
+```
+
+**响应**:
+```json
+{
+    "success": true,
+    "message": "成功获取 120 条解析人才记录",
+    "data": [...],
+    "count": 120
+}
+```
+
+### 4. 查询已拒绝记录
+
+**请求**:
+```
+GET /get-parsed-talents?status=已拒绝
+```
+
+**响应**:
+```json
+{
+    "success": true,
+    "message": "成功获取 5 条解析人才记录",
+    "data": [...],
+    "count": 5
+}
+```
+
+## 前端集成示例
+
+### JavaScript (使用 fetch)
+
+```javascript
+// 获取所有记录
+async function getAllParsedTalents() {
+    try {
+        const response = await fetch('/get-parsed-talents');
+        const data = await response.json();
+        
+        if (data.success) {
+            console.log(`获取到 ${data.count} 条记录`);
+            return data.data;
+        } else {
+            console.error('获取失败:', data.message);
+            return [];
+        }
+    } catch (error) {
+        console.error('请求失败:', error);
+        return [];
+    }
+}
+
+// 获取特定状态的记录
+async function getParsedTalentsByStatus(status) {
+    try {
+        const response = await fetch(`/get-parsed-talents?status=${encodeURIComponent(status)}`);
+        const data = await response.json();
+        
+        if (data.success) {
+            console.log(`获取到 ${data.count} 条 ${status} 状态的记录`);
+            return data.data;
+        } else {
+            console.error('获取失败:', data.message);
+            return [];
+        }
+    } catch (error) {
+        console.error('请求失败:', error);
+        return [];
+    }
+}
+
+// 使用示例
+getAllParsedTalents().then(talents => {
+    console.log('所有人才记录:', talents);
+});
+
+getParsedTalentsByStatus('待审核').then(talents => {
+    console.log('待审核人才记录:', talents);
+});
+```
+
+### JavaScript (使用 axios)
+
+```javascript
+import axios from 'axios';
+
+// 获取所有记录
+async function getAllParsedTalents() {
+    try {
+        const response = await axios.get('/get-parsed-talents');
+        const { success, data, count, message } = response.data;
+        
+        if (success) {
+            console.log(`获取到 ${count} 条记录`);
+            return data;
+        } else {
+            console.error('获取失败:', message);
+            return [];
+        }
+    } catch (error) {
+        console.error('请求失败:', error);
+        return [];
+    }
+}
+
+// 获取特定状态的记录
+async function getParsedTalentsByStatus(status) {
+    try {
+        const response = await axios.get('/get-parsed-talents', {
+            params: { status }
+        });
+        const { success, data, count, message } = response.data;
+        
+        if (success) {
+            console.log(`获取到 ${count} 条 ${status} 状态的记录`);
+            return data;
+        } else {
+            console.error('获取失败:', message);
+            return [];
+        }
+    } catch (error) {
+        console.error('请求失败:', error);
+        return [];
+    }
+}
+```
+
+### React Hook 示例
+
+```javascript
+import { useState, useEffect } from 'react';
+
+function useParsedTalents(status = null) {
+    const [talents, setTalents] = useState([]);
+    const [loading, setLoading] = useState(false);
+    const [error, setError] = useState(null);
+
+    useEffect(() => {
+        const fetchTalents = async () => {
+            setLoading(true);
+            setError(null);
+            
+            try {
+                const url = status 
+                    ? `/get-parsed-talents?status=${encodeURIComponent(status)}`
+                    : '/get-parsed-talents';
+                    
+                const response = await fetch(url);
+                const data = await response.json();
+                
+                if (data.success) {
+                    setTalents(data.data);
+                } else {
+                    setError(data.message);
+                }
+            } catch (err) {
+                setError('请求失败: ' + err.message);
+            } finally {
+                setLoading(false);
+            }
+        };
+
+        fetchTalents();
+    }, [status]);
+
+    return { talents, loading, error };
+}
+
+// 使用示例
+function TalentList() {
+    const { talents, loading, error } = useParsedTalents('待审核');
+
+    if (loading) return <div>加载中...</div>;
+    if (error) return <div>错误: {error}</div>;
+
+    return (
+        <div>
+            <h2>待审核人才列表 ({talents.length})</h2>
+            {talents.map(talent => (
+                <div key={talent.id}>
+                    <h3>{talent.name_zh}</h3>
+                    <p>职位: {talent.title_zh}</p>
+                    <p>酒店: {talent.hotel_zh}</p>
+                    <p>状态: {talent.status}</p>
+                </div>
+            ))}
+        </div>
+    );
+}
+```
+
+## 注意事项
+
+1. **分页处理**: 当前接口返回所有匹配的记录,如果数据量很大,建议前端实现分页显示
+2. **状态值**: 建议使用标准状态值,如 `"待审核"`, `"已入库"`, `"已拒绝"`
+3. **错误处理**: 始终检查 `success` 字段,处理可能的错误情况
+4. **数据格式**: 日期时间字段使用 ISO 8601 格式
+5. **空值处理**: 某些字段可能为 `null` 或空字符串,前端需要适当处理
+6. **字符编码**: 中文字段使用 UTF-8 编码
+
+## 状态码说明
+
+| HTTP状态码 | 说明 |
+|-----------|------|
+| 200 | 请求成功 |
+| 500 | 服务器内部错误 |
+
+## 更新日志
+
+- **v1.0.0** (2024-01-15): 初始版本,支持基本查询和状态过滤功能
+
+## 联系方式
+
+如有问题或建议,请联系开发团队。