Sfoglia il codice sorgente

拆分名片解析功能,添加重复记录处理功能,添加重复记录处理表

maxiaolong 3 settimane fa
parent
commit
470ed279a7

+ 91 - 84
app/api/data_parse/routes.py

@@ -1,6 +1,8 @@
 from flask import jsonify, request, make_response, Blueprint, current_app, send_file
 from app.api.data_parse import bp
 from app.core.data_parse.parse import parse_data, process_business_card, update_business_card, get_business_cards, update_business_card_status, create_talent_tag, get_talent_tag_list, update_talent_tag, delete_talent_tag, query_neo4j_graph, talent_get_tags, talent_update_tags, get_business_card, get_hotel_positions_list, add_hotel_positions, update_hotel_positions, query_hotel_positions, delete_hotel_positions, get_hotel_group_brands_list, add_hotel_group_brands, update_hotel_group_brands, query_hotel_group_brands, delete_hotel_group_brands, get_duplicate_records, process_duplicate_record, get_duplicate_record_detail
+# 导入新的名片图片解析函数和添加名片函数
+from app.core.data_parse.parse_card import process_business_card_image, add_business_card
 from app.config.config import DevelopmentConfig, ProductionConfig
 import logging
 import boto3
@@ -12,88 +14,6 @@ import os
 import urllib.parse
 from minio import Minio
 
-"""
-DataOps平台 - 数据解析API路由模块
-
-本模块包含以下功能的API接口:
-
-1. 名片解析功能
-   - POST /business-card-parse: 上传名片图片并解析信息
-   - PUT /business-cards/<id>: 更新名片信息
-   - GET /get-business-cards: 获取所有名片记录
-   - GET /get-business-card/<id>: 获取指定ID的名片记录
-   - PUT /update-business-cards/<id>/status: 更新名片状态
-
-2. 重复记录处理功能(新增)
-   - GET /get-duplicate-records[?status=<status>]: 获取重复记录列表
-   - POST /process-duplicate-record/<id>: 处理重复记录
-   - GET /get-duplicate-record-detail/<id>: 获取重复记录详情
-
-3. 人才标签管理功能
-   - POST /create-talent-tag: 创建人才标签
-   - GET /get-talent-tag-list: 获取人才标签列表
-   - PUT /update-talent-tag/<id>: 更新人才标签
-   - DELETE /delete-talent-tag/<id>: 删除人才标签
-
-4. 人才标签关系管理功能
-   - GET /talent-get-tags/<talent_id>: 获取人才关联的标签
-   - POST /talent-update-tags: 批量更新人才标签关系
-
-5. 知识图谱查询功能
-   - POST /query-kg: 通过自然语言查询图数据库
-
-6. 酒店职位数据管理功能
-   - GET /get-hotel-positions-list: 获取酒店职位列表
-   - POST /add-hotel-positions: 新增酒店职位记录
-   - PUT /update-hotel-positions/<id>: 更新酒店职位记录
-   - GET /query-hotel-positions/<id>: 查询指定职位记录
-   - DELETE /delete-hotel-positions/<id>: 删除职位记录
-
-7. 酒店集团品牌数据管理功能
-   - GET /get-hotel-group-brands-list: 获取酒店集团品牌列表
-   - POST /add-hotel-group-brands: 新增酒店集团品牌记录
-   - PUT /update-hotel-group-brands/<id>: 更新酒店集团品牌记录
-   - GET /query-hotel-group-brands/<id>: 查询指定品牌记录
-   - DELETE /delete-hotel-group-brands/<id>: 删除品牌记录
-
-8. MinIO文件管理功能
-   - GET /business-cards/image/<path>: 获取名片图片
-   - GET /test-minio-connection: 测试MinIO连接
-
-重复记录处理API详细说明:
-═══════════════════════════════
-
-1. 获取重复记录列表
-   GET /get-duplicate-records[?status=<status>]
-   - 查询参数: status (可选): 'pending'/'processed'/'ignored'
-   - 返回: 重复记录列表,包含主记录和疑似重复记录信息
-
-2. 处理重复记录
-   POST /process-duplicate-record/<duplicate_id>
-   - 路径参数: duplicate_id (必填): 重复记录ID
-   - 请求体参数:
-     * action (必填): 'merge_to_suspected'/'keep_main'/'ignore'
-     * selected_duplicate_id (可选): 当action为merge_to_suspected时必填
-     * processed_by (可选): 处理人标识
-     * notes (可选): 处理备注
-   - 处理动作说明:
-     * merge_to_suspected: 合并到选中的疑似重复记录,删除主记录
-     * keep_main: 保留主记录,标记为已处理
-     * ignore: 忽略重复提醒,标记为已处理
-
-3. 获取重复记录详情
-   GET /get-duplicate-record-detail/<duplicate_id>
-   - 路径参数: duplicate_id (必填): 重复记录ID
-   - 返回: 重复记录的详细信息,包含主记录和所有疑似重复记录
-
-业务流程说明:
-1. 上传名片后,系统自动检测重复记录
-2. 如发现重复,创建主记录并生成重复记录条目
-3. 管理员通过API查看待处理的重复记录
-4. 管理员选择处理方式:合并、保留或忽略
-5. 系统根据选择执行相应操作并更新状态
-"""
-
 # Define logger
 logger = logging.getLogger(__name__)
 
@@ -148,13 +68,15 @@ def parse():
 @bp.route('/business-card-parse', methods=['POST'])
 def parse_business_card_route():
     """
-    处理名片图片并提取信息的API接口
+    解析名片图片并提取信息的API接口(仅解析,不保存到数据库)
     
     请求参数:
         - image: 名片图片文件 (multipart/form-data)
         
     返回:
         - JSON: 包含提取的名片信息和处理状态
+        
+    注意:此接口仅负责图片解析和信息提取,不会将数据保存到数据库
     """
     # 检查是否上传了文件
     if 'image' not in request.files:
@@ -183,13 +105,98 @@ def parse_business_card_route():
         }), 400
     
     # 处理名片图片
-    result = process_business_card(image_file)
+    result = process_business_card_image(image_file)
     
     if result['success']:
         return jsonify(result), 200
     else:
         return jsonify(result), 500
 
+# 添加名片记录接口
+@bp.route('/add-business-card', methods=['POST'])
+def add_business_card_route():
+    """
+    添加名片记录的API接口(解析图片并保存到数据库)
+    
+    请求参数:
+        - card_data: 名片信息数据 (JSON格式,可以通过form-data或JSON body传递)
+        - image: 名片图片文件 (multipart/form-data,可选)
+        
+    返回:
+        - JSON: 包含保存结果和处理状态
+        
+    注意:此接口负责业务逻辑处理,包括重复检查、MinIO上传和数据库保存
+    """
+    try:
+        # 获取名片数据 - 支持两种方式
+        card_data = None
+        
+        # 方式1:通过JSON body传递
+        if request.is_json:
+            card_data = request.get_json()
+        # 方式2:通过form-data传递card_data字段
+        elif 'card_data' in request.form:
+            import json
+            try:
+                card_data = json.loads(request.form['card_data'])
+            except json.JSONDecodeError:
+                return jsonify({
+                    'success': False,
+                    'message': 'card_data格式错误,必须是有效的JSON字符串',
+                    'data': None
+                }), 400
+        
+        # 检查是否提供了名片数据
+        if not card_data:
+            return jsonify({
+                'success': False,
+                'message': '未提供名片数据,请通过JSON body或form-data的card_data字段传递',
+                'data': None
+            }), 400
+        
+        # 获取可选的图片文件
+        image_file = None
+        if 'image' in request.files:
+            image_file = request.files['image']
+            
+            # 检查文件是否为空
+            if image_file.filename == '':
+                image_file = None
+            # 检查文件类型是否为图片
+            elif not image_file.content_type.startswith('image/'):
+                return jsonify({
+                    'success': False,
+                    'message': '上传的文件不是图片',
+                    'data': None
+                }), 400
+        
+        # 调用业务逻辑函数处理名片数据
+        result = add_business_card(card_data, image_file)
+        
+        # 根据处理结果设置HTTP状态码
+        if result['success']:
+            if result['code'] == 200:
+                status_code = 200
+            elif result['code'] == 202:
+                status_code = 202  # Accepted - 创建成功但有疑似重复记录
+            else:
+                status_code = 200
+        else:
+            if result['code'] == 400:
+                status_code = 400
+            else:
+                status_code = 500
+        
+        return jsonify(result), status_code
+        
+    except Exception as e:
+        logger.error(f"添加名片记录失败: {str(e)}")
+        return jsonify({
+            'success': False,
+            'message': f'添加名片记录失败: {str(e)}',
+            'data': None
+        }), 500
+
 # 更新名片信息接口
 @bp.route('/business-cards/<int:card_id>', methods=['PUT'])
 def update_business_card_route(card_id):

+ 1 - 39
app/core/data_parse/parse.py

@@ -16,43 +16,6 @@ import base64
 from openai import OpenAI
 from app.config.config import DevelopmentConfig, ProductionConfig
 
-"""
-名片解析功能模块升级说明:
-
-本模块新增了重复记录处理功能,主要包括:
-
-1. 新增数据模型:
-   - DuplicateBusinessCard:用于存储重复记录处理信息
-     * main_card_id: 指向新创建的主记录
-     * suspected_duplicates: JSON格式的疑似重复记录列表
-
-2. 新增功能函数:
-   - check_duplicate_business_card():检查是否存在重复记录
-   - update_career_path():更新职业轨迹信息
-   - create_main_card_with_duplicates():创建主记录并保存疑似重复信息
-   - get_duplicate_records():获取重复记录列表
-   - process_duplicate_record():处理重复记录
-   - get_duplicate_record_detail():获取重复记录详情
-
-3. 重复记录处理逻辑:
-   - 基于中文姓名和手机号码进行重复检查
-   - 如果姓名和手机号码都相同:自动更新现有记录并添加职业轨迹
-   - 如果姓名相同但手机号码不同或缺失:创建新记录作为主记录,疑似重复记录保存为JSON列表
-
-4. 处理状态管理:
-   - pending:待处理
-   - processed:已处理
-   - ignored:已忽略
-
-5. 手动处理选项:
-   - merge_to_suspected:合并到选中的疑似重复记录,删除主记录
-   - keep_main:保留主记录,标记为已处理
-   - ignore:忽略重复记录提醒
-
-升级后的process_business_card()函数会自动应用重复记录检查逻辑。
-新逻辑优势:一个新记录可能与多条现有记录重复,统一管理更加高效。
-"""
-
 # 名片解析数据模型
 class BusinessCard(db.Model):
     __tablename__ = 'business_cards'
@@ -3145,7 +3108,6 @@ def process_duplicate_record(duplicate_id, action, selected_duplicate_id=None, p
             'data': None
         }
 
-
 def get_duplicate_record_detail(duplicate_id):
     """
     获取指定重复记录的详细信息
@@ -3190,4 +3152,4 @@ def get_duplicate_record_detail(duplicate_id):
             'success': False,
             'message': error_msg,
             'data': None
-        }
+        }

+ 329 - 0
app/core/data_parse/parse_card.py

@@ -0,0 +1,329 @@
+from typing import Dict, Any
+from app import db
+from datetime import datetime
+import os
+import boto3
+from botocore.config import Config
+import logging
+import uuid
+from app.config.config import DevelopmentConfig, ProductionConfig
+
+# 导入原有的函数和模型
+from app.core.data_parse.parse import (
+    BusinessCard, DuplicateBusinessCard,
+    parse_text_with_qwen25VLplus, check_duplicate_business_card,
+    update_career_path, create_main_card_with_duplicates
+)
+
+# 使用配置变量,缺省认为在生产环境运行
+config = ProductionConfig()
+# 使用配置变量
+minio_url = f"{'https' if config.MINIO_SECURE else 'http'}://{config.MINIO_HOST}"
+minio_access_key = config.MINIO_USER
+minio_secret_key = config.MINIO_PASSWORD
+minio_bucket = config.MINIO_BUCKET
+use_ssl = config.MINIO_SECURE
+
+
+def get_minio_client():
+    """获取MinIO客户端连接"""
+    try:
+        # 使用全局配置变量
+        global minio_url, minio_access_key, minio_secret_key, minio_bucket, use_ssl
+        
+        logging.info(f"尝试连接MinIO服务器: {minio_url}")
+        
+        minio_client = boto3.client(
+            's3',
+            endpoint_url=minio_url,
+            aws_access_key_id=minio_access_key,
+            aws_secret_access_key=minio_secret_key,
+            config=Config(
+                signature_version='s3v4',
+                retries={'max_attempts': 3, 'mode': 'standard'},
+                connect_timeout=10,
+                read_timeout=30
+            )
+        )
+        
+        # 确保存储桶存在
+        buckets = minio_client.list_buckets()
+        bucket_names = [bucket['Name'] for bucket in buckets.get('Buckets', [])]
+        logging.info(f"成功连接到MinIO服务器,现有存储桶: {bucket_names}")
+        
+        if minio_bucket not in bucket_names:
+            logging.info(f"创建存储桶: {minio_bucket}")
+            minio_client.create_bucket(Bucket=minio_bucket)
+            
+        return minio_client
+    except Exception as e:
+        logging.error(f"MinIO连接错误: {str(e)}")
+        return None
+
+
+def process_business_card_image(image_file):
+    """
+    处理名片图片并提取信息(仅负责图片解析部分)
+    
+    Args:
+        image_file (FileStorage): 上传的名片图片文件
+        
+    Returns:
+        dict: 图片解析结果,包含提取的信息和状态
+    """
+    try:
+        # 读取图片数据
+        image_data = image_file.read()
+        image_file.seek(0)  # 重置文件指针以便后续读取
+        
+        try:
+            # 优先使用 Qwen 2.5 VL Plus 模型直接从图像提取信息
+            try:
+                logging.info("尝试使用 Qwen 2.5 VL Plus 模型解析名片")
+                extracted_data = parse_text_with_qwen25VLplus(image_data)
+                logging.info("成功使用 Qwen 2.5 VL Plus 模型解析名片")
+                
+                return {
+                    'code': 200,
+                    'success': True,
+                    'message': '名片图片解析成功',
+                    'data': extracted_data
+                }
+            except Exception as qwen_error:
+                logging.warning(f"Qwen 模型解析失败,错误原因: {str(qwen_error)}")
+                return {
+                    'code': 500,
+                    'success': False,
+                    'message': f"名片图片解析失败: {str(qwen_error)}",
+                    'data': None
+                }
+        except Exception as e:
+            return {
+                'code': 500,
+                'success': False,
+                'message': f"名片解析失败: {str(e)}",
+                'data': None
+            }
+            
+    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': None
+        }
+
+
+def add_business_card(card_data, image_file=None):
+    """
+    添加名片记录(负责业务逻辑处理部分)
+    
+    Args:
+        card_data (dict): 名片信息数据
+        image_file (FileStorage, optional): 名片图片文件(用于上传到MinIO)
+        
+    Returns:
+        dict: 处理结果,包含保存的信息和状态
+    """
+    minio_path = None
+    
+    try:
+        # 检查必要的数据
+        if not card_data:
+            return {
+                'code': 400,
+                'success': False,
+                'message': '名片数据不能为空',
+                'data': None
+            }
+        
+        # 检查重复记录
+        try:
+            duplicate_check = check_duplicate_business_card(card_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)}'
+            }
+        
+        # 上传图片到MinIO(如果提供了图片文件)
+        if image_file:
+            try:
+                # 生成唯一的文件名
+                file_ext = os.path.splitext(image_file.filename)[1].lower()
+                if not file_ext:
+                    file_ext = '.jpg'  # 默认扩展名
+                
+                unique_filename = f"{uuid.uuid4().hex}{file_ext}"
+                minio_path = f"{unique_filename}"
+                
+                # 尝试上传到MinIO
+                minio_client = get_minio_client()
+                if minio_client:
+                    try:
+                        # 上传文件
+                        logging.info(f"上传文件到MinIO: {minio_path}")
+                        minio_client.put_object(
+                            Bucket=minio_bucket,
+                            Key=minio_path,
+                            Body=image_file,
+                            ContentType=image_file.content_type
+                        )
+                        logging.info(f"图片已上传到MinIO: {minio_path}")
+                    except Exception as upload_err:
+                        logging.error(f"上传文件到MinIO时出错: {str(upload_err)}")
+                        # 即使上传失败,仍继续处理,但路径为None
+                        minio_path = None
+                else:
+                    minio_path = None
+                    logging.warning("MinIO客户端未初始化,图片未上传")
+            except Exception as e:
+                logging.error(f"上传图片到MinIO失败: {str(e)}", exc_info=True)
+                minio_path = None
+        
+        try:
+            # 根据重复检查结果执行不同操作
+            if duplicate_check['action'] == 'update':
+                # 更新现有记录
+                existing_card = duplicate_check['existing_card']
+                
+                # 更新基本信息
+                existing_card.name_en = card_data.get('name_en', existing_card.name_en)
+                existing_card.title_zh = card_data.get('title_zh', existing_card.title_zh)
+                existing_card.title_en = card_data.get('title_en', existing_card.title_en)
+                existing_card.phone = card_data.get('phone', existing_card.phone)
+                existing_card.email = card_data.get('email', existing_card.email)
+                existing_card.hotel_zh = card_data.get('hotel_zh', existing_card.hotel_zh)
+                existing_card.hotel_en = card_data.get('hotel_en', existing_card.hotel_en)
+                existing_card.address_zh = card_data.get('address_zh', existing_card.address_zh)
+                existing_card.address_en = card_data.get('address_en', existing_card.address_en)
+                existing_card.postal_code_zh = card_data.get('postal_code_zh', existing_card.postal_code_zh)
+                existing_card.postal_code_en = card_data.get('postal_code_en', existing_card.postal_code_en)
+                existing_card.brand_zh = card_data.get('brand_zh', existing_card.brand_zh)
+                existing_card.brand_en = card_data.get('brand_en', existing_card.brand_en)
+                existing_card.affiliation_zh = card_data.get('affiliation_zh', existing_card.affiliation_zh)
+                existing_card.affiliation_en = card_data.get('affiliation_en', existing_card.affiliation_en)
+                existing_card.brand_group = card_data.get('brand_group', existing_card.brand_group)
+                existing_card.image_path = minio_path  # 更新为最新的图片路径
+                existing_card.updated_by = 'system'
+                
+                # 更新职业轨迹,传递图片路径
+                existing_card.career_path = update_career_path(existing_card, card_data, minio_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':
+                # 创建新记录作为主记录,并保存疑似重复记录信息
+                main_card, duplicate_record = create_main_card_with_duplicates(
+                    card_data, 
+                    minio_path, 
+                    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:
+                # 创建新记录
+                # 准备初始职业轨迹,包含当前名片信息和图片路径
+                initial_career_path = card_data.get('career_path', [])
+                if card_data.get('hotel_zh') or card_data.get('hotel_en') or card_data.get('title_zh') or card_data.get('title_en'):
+                    initial_entry = {
+                        'date': datetime.now().strftime('%Y-%m-%d'),
+                        'hotel_zh': card_data.get('hotel_zh', ''),
+                        'hotel_en': card_data.get('hotel_en', ''),
+                        'title_zh': card_data.get('title_zh', ''),
+                        'title_en': card_data.get('title_en', ''),
+                        'image_path': minio_path or '',  # 当前名片的图片路径
+                        'source': 'business_card_creation'
+                    }
+                    initial_career_path.append(initial_entry)
+                
+                business_card = BusinessCard(
+                    name_zh=card_data.get('name_zh', ''),
+                    name_en=card_data.get('name_en', ''),
+                    title_zh=card_data.get('title_zh', ''),
+                    title_en=card_data.get('title_en', ''),
+                    mobile=card_data.get('mobile', ''),
+                    phone=card_data.get('phone', ''),
+                    email=card_data.get('email', ''),
+                    hotel_zh=card_data.get('hotel_zh', ''),
+                    hotel_en=card_data.get('hotel_en', ''),
+                    address_zh=card_data.get('address_zh', ''),
+                    address_en=card_data.get('address_en', ''),
+                    postal_code_zh=card_data.get('postal_code_zh', ''),
+                    postal_code_en=card_data.get('postal_code_en', ''),
+                    brand_zh=card_data.get('brand_zh', ''),
+                    brand_en=card_data.get('brand_en', ''),
+                    affiliation_zh=card_data.get('affiliation_zh', ''),
+                    affiliation_en=card_data.get('affiliation_en', ''),
+                    image_path=minio_path,  # 最新的图片路径
+                    career_path=initial_career_path,  # 包含图片路径的职业轨迹
+                    brand_group=card_data.get('brand_group', ''),
+                    status='active',
+                    updated_by='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
+        }

+ 36 - 0
database/create_duplicate_business_cards_table.sql

@@ -0,0 +1,36 @@
+-- ================================================================
+-- 创建 duplicate_business_cards 表脚本
+-- 用于存储重复名片处理记录
+-- 创建日期: 2024年
+-- ================================================================
+
+-- 创建 duplicate_business_cards 表
+CREATE TABLE duplicate_business_cards (
+    id SERIAL PRIMARY KEY,
+    main_card_id INTEGER NOT NULL,
+    suspected_duplicates JSONB NOT NULL,
+    duplicate_reason VARCHAR(200) NOT NULL,
+    processing_status VARCHAR(20) DEFAULT 'pending',
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    processed_at TIMESTAMP,
+    processed_by VARCHAR(50),
+    processing_notes TEXT
+);
+
+-- 添加外键约束
+ALTER TABLE duplicate_business_cards 
+ADD CONSTRAINT fk_duplicate_business_cards_main_card_id 
+FOREIGN KEY (main_card_id) REFERENCES business_cards(id) ON DELETE CASCADE;
+
+-- 添加表和字段注释
+COMMENT ON TABLE duplicate_business_cards IS '重复名片处理记录表,用于存储发现的疑似重复名片信息和处理状态';
+
+COMMENT ON COLUMN duplicate_business_cards.id IS '主键ID,自增序列';
+COMMENT ON COLUMN duplicate_business_cards.main_card_id IS '新创建的主记录ID,关联business_cards表';
+COMMENT ON COLUMN duplicate_business_cards.suspected_duplicates IS '疑似重复记录列表,JSON格式存储';
+COMMENT ON COLUMN duplicate_business_cards.duplicate_reason IS '重复原因描述,最大200字符';
+COMMENT ON COLUMN duplicate_business_cards.processing_status IS '处理状态:pending(待处理)/processed(已处理)/ignored(已忽略)';
+COMMENT ON COLUMN duplicate_business_cards.created_at IS '记录创建时间';
+COMMENT ON COLUMN duplicate_business_cards.processed_at IS '处理时间,记录被处理时的时间戳';
+COMMENT ON COLUMN duplicate_business_cards.processed_by IS '处理人员标识,最大50字符';
+COMMENT ON COLUMN duplicate_business_cards.processing_notes IS '处理备注,记录处理过程中的详细说明';