Parcourir la source

修改business_cards表结构
修改名片保存字段。
修改元数据data_type字段。

maxiaolong il y a 1 semaine
Parent
commit
c5f171bd70

+ 32 - 0
alter_business_cards_simple.sql

@@ -0,0 +1,32 @@
+-- ===============================================
+-- 修改business_cards表,新增age和native_place字段 (简化版本)
+-- 执行日期: 请在执行前填写实际日期
+-- 修改说明: 为名片表新增年龄和籍贯字段
+-- ===============================================
+
+-- 步骤1: 添加age字段
+ALTER TABLE business_cards ADD COLUMN age INTEGER;
+
+-- 步骤2: 添加native_place字段
+ALTER TABLE business_cards ADD COLUMN native_place TEXT;
+
+-- 步骤3: 添加字段注释
+COMMENT ON COLUMN business_cards.age IS '年龄字段 - 存储人员年龄信息,取值范围1-150';
+COMMENT ON COLUMN business_cards.native_place IS '籍贯字段 - 存储人员籍贯或出生地信息';
+
+-- 步骤4: 验证字段是否添加成功
+SELECT column_name, data_type, is_nullable
+FROM information_schema.columns 
+WHERE table_name = 'business_cards' 
+  AND column_name IN ('age', 'native_place')
+ORDER BY column_name;
+
+-- 步骤5: 查看表结构(需要在psql客户端中执行)
+-- \d business_cards
+
+-- ===============================================
+-- 执行说明:
+-- 1. 一次执行一个语句,避免批量执行出现问题
+-- 2. 新增字段允许NULL值,不会影响现有数据
+-- 3. 执行前请备份数据库
+-- =============================================== 

+ 61 - 0
alter_business_cards_table.sql

@@ -0,0 +1,61 @@
+-- ===============================================
+-- 修改business_cards表,新增age和native_place字段
+-- 执行日期: 请在执行前填写实际日期
+-- 修改说明: 为名片表新增年龄和籍贯字段,支持更完整的人员信息管理
+-- ===============================================
+
+-- 1. 新增age字段 (年龄字段)
+ALTER TABLE business_cards 
+ADD COLUMN age INTEGER;
+
+-- 2. 新增native_place字段 (籍贯字段)  
+ALTER TABLE business_cards 
+ADD COLUMN native_place TEXT;
+
+-- 3. 为新增字段添加注释
+COMMENT ON COLUMN business_cards.age IS '年龄字段 - 存储人员年龄信息,取值范围1-150';
+COMMENT ON COLUMN business_cards.native_place IS '籍贯字段 - 存储人员籍贯或出生地信息';
+
+-- 4. 验证字段是否成功添加
+SELECT 
+    column_name,
+    data_type,
+    is_nullable,
+    column_default
+FROM information_schema.columns 
+WHERE table_name = 'business_cards' 
+    AND column_name IN ('age', 'native_place')
+ORDER BY column_name;
+
+-- 5. 查看字段注释
+SELECT 
+    a.attname AS column_name,
+    format_type(a.atttypid, a.atttypmod) AS data_type,
+    COALESCE(pg_catalog.col_description(a.attrelid, a.attnum), '无注释') AS description
+FROM 
+    pg_catalog.pg_attribute a
+JOIN 
+    pg_catalog.pg_class c ON a.attrelid = c.oid
+JOIN 
+    pg_catalog.pg_namespace n ON c.relnamespace = n.oid
+WHERE 
+    c.relname = 'business_cards' 
+    AND a.attname IN ('age', 'native_place')
+    AND a.attnum > 0 
+    AND NOT a.attisdropped
+ORDER BY a.attname;
+
+-- ===============================================
+-- 执行说明:
+-- 1. 请在生产环境执行前先在测试环境验证
+-- 2. 建议在业务低峰期执行此脚本
+-- 3. 执行前请备份相关数据
+-- 4. 新增字段允许NULL值,不会影响现有数据
+-- ===============================================
+
+-- 可选:如果需要为现有记录设置默认值,可以执行以下语句
+-- UPDATE business_cards SET age = NULL WHERE age IS NULL;
+-- UPDATE business_cards SET native_place = '' WHERE native_place IS NULL;
+
+-- 执行完成后检查表结构
+\d business_cards; 

+ 50 - 1
app/api/data_parse/routes.py

@@ -1,6 +1,6 @@
 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 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, fix_broken_duplicate_records
+from app.core.data_parse.parse import 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, search_business_cards_by_mobile, 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, fix_broken_duplicate_records
 # 导入新的名片图片解析函数和添加名片函数
 from app.core.data_parse.parse_card import process_business_card_image, add_business_card, delete_business_card
 from app.config.config import DevelopmentConfig, ProductionConfig
@@ -702,6 +702,55 @@ def get_business_card_route(card_id):
     
     return jsonify(result), status_code
 
+@bp.route('/search-business-cards-by-mobile', methods=['GET'])
+def search_business_cards_by_mobile_route():
+    """
+    根据手机号码搜索名片记录的API接口
+    
+    查询参数:
+        - mobile: 要搜索的手机号码
+        
+    返回:
+        - JSON: 包含搜索到的名片记录列表和处理状态
+        
+    示例:
+        GET /search-business-cards-by-mobile?mobile=13800138000
+    """
+    try:
+        # 获取查询参数
+        mobile_number = request.args.get('mobile', '').strip()
+        
+        if not mobile_number:
+            return jsonify({
+                'success': False,
+                'message': '请提供要搜索的手机号码',
+                'data': []
+            }), 400
+        
+        # 调用业务逻辑函数搜索名片记录
+        result = search_business_cards_by_mobile(mobile_number)
+        
+        # 根据处理结果设置HTTP状态码
+        if result['code'] == 200:
+            status_code = 200
+        elif result['code'] == 400:
+            status_code = 400
+        else:
+            status_code = 500
+        
+        return jsonify(result), 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': []
+        }), 500
+
 @bp.route('/get-hotel-positions-list', methods=['GET'])
 def get_hotel_positions_list_route():
     """

+ 2 - 2
app/api/data_resource/routes.py

@@ -221,7 +221,7 @@ def data_resource_delete():
     try:
         # 获取资源ID
         resource_id = request.json.get('id')
-        if not resource_id:
+        if resource_id is None:
             return jsonify(failed("资源ID不能为空"))
         
         with neo4j_driver.get_session() as session:
@@ -461,7 +461,7 @@ def id_data_save():
         resource_id = request.json.get('id')
         metadata_list = request.json.get('data', [])
         
-        if not resource_id:
+        if resource_id is None:
             return jsonify(failed("资源ID不能为空"))
             
         if not metadata_list:

+ 17 - 9
app/api/meta_data/routes.py

@@ -173,7 +173,7 @@ def meta_node_edit():
                 "en_name": node_data.get("en_name", ""),
                 "time": node_data.get("updateTime", ""),
                 "status": node_data.get("status", "true") == "true",
-                "type": node_data.get("type", ""),
+                "data_type": node_data.get("data_type", ""),
                 "tag": {
                     "name": tag["t"].get("name", "") if tag and tag["t"] else None,
                     "id": tag["t"].id if tag and tag["t"] else None
@@ -197,7 +197,7 @@ def meta_node_add():
     try:
         # 从请求中获取节点信息
         node_name = request.json.get('name')
-        node_type = request.json.get('type')
+        node_type = request.json.get('data_type')
         node_category = request.json.get('category')
         node_alias = request.json.get('alias')
         node_affiliation = request.json.get('affiliation')
@@ -217,7 +217,7 @@ def meta_node_add():
         with neo4j_driver.get_session() as session:
             cypher = """
             MERGE (n:DataMeta {name: $name})
-            ON CREATE SET n.type = $type,
+            ON CREATE SET n.data_type = $data_type,
                          n.category = $category,
                          n.alias = $alias,
                          n.affiliation = $affiliation,
@@ -226,7 +226,7 @@ def meta_node_add():
                          n.updateTime = $update_time,
                          n.status = $status,
                          n.en_name = $en_name
-            ON MATCH SET n.type = $type,
+            ON MATCH SET n.data_type = $data_type,
                         n.category = $category,
                         n.alias = $alias,
                         n.affiliation = $affiliation,
@@ -240,7 +240,7 @@ def meta_node_add():
             result = session.run(
                 cypher, 
                 name=node_name,
-                type=node_type,
+                data_type=node_type,
                 category=node_category,
                 alias=node_alias,
                 affiliation=node_affiliation,
@@ -743,7 +743,7 @@ def meta_node_update():
                 'category': request.json.get('category'),
                 'alias': request.json.get('alias'),
                 'affiliation': request.json.get('affiliation'),
-                'type': request.json.get('type'),
+                'data_type': request.json.get('data_type'),
                 'describe': request.json.get('describe'),
                 'status': request.json.get('status')
             }
@@ -751,12 +751,20 @@ def meta_node_update():
             # 只更新提供了新值的字段
             for field, new_value in fields_to_update.items():
                 if new_value is not None:
-                    update_cypher += f", n.{field} = ${field}\n"
+                    # 特殊处理 type 字段映射到 data_type
+                    if field == 'data_type':
+                        update_cypher += f", n.data_type = ${field}\n"
+                    else:
+                        update_cypher += f", n.{field} = ${field}\n"
                     update_params[field] = new_value
                 else:
                     # 如果字段没有提供新值,使用当前值
-                    update_params[field] = current_properties.get(field)
-                    update_cypher += f", n.{field} = ${field}\n"
+                    if field == 'data_type':
+                        update_params[field] = current_properties.get('data_type')
+                        update_cypher += f", n.data_type = ${field}\n"
+                    else:
+                        update_params[field] = current_properties.get(field)
+                        update_cypher += f", n.{field} = ${field}\n"
             
             # 处理英文名称
             if request.json.get('name'):

+ 202 - 19
app/core/data_parse/parse.py

@@ -25,7 +25,7 @@ class BusinessCard(db.Model):
     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))
+    mobile = db.Column(db.String(100))
     phone = db.Column(db.String(50))
     email = db.Column(db.String(100))
     hotel_zh = db.Column(db.String(200))
@@ -39,6 +39,8 @@ class BusinessCard(db.Model):
     affiliation_zh = db.Column(db.String(200))
     affiliation_en = db.Column(db.String(200))
     birthday = db.Column(db.Date)  # 生日,存储年月日
+    age = db.Column(db.Integer)  # 年龄字段
+    native_place = db.Column(db.Text)  # 籍贯字段
     residence = db.Column(db.Text)  # 居住地
     image_path = db.Column(db.String(255))  # MinIO中存储的路径
     career_path = db.Column(db.JSON)  # 职业轨迹,JSON格式
@@ -69,6 +71,8 @@ class BusinessCard(db.Model):
             'affiliation_zh': self.affiliation_zh,
             'affiliation_en': self.affiliation_en,
             'birthday': self.birthday.strftime('%Y-%m-%d') if self.birthday else None,
+            'age': self.age,
+            'native_place': self.native_place,
             'residence': self.residence,
             'image_path': self.image_path,
             'career_path': self.career_path,
@@ -113,6 +117,81 @@ class DuplicateBusinessCard(db.Model):
 
 # 名片解析功能模块
 
+def normalize_mobile_numbers(mobile_str):
+    """
+    标准化手机号码字符串,去重并限制最多3个
+    
+    Args:
+        mobile_str (str): 手机号码字符串,可能包含多个手机号码,用逗号分隔
+        
+    Returns:
+        str: 标准化后的手机号码字符串,最多3个,用逗号分隔
+    """
+    if not mobile_str or not mobile_str.strip():
+        return ''
+    
+    # 按逗号分割并清理每个手机号码
+    mobiles = []
+    for mobile in mobile_str.split(','):
+        mobile = mobile.strip()
+        if mobile and mobile not in mobiles:  # 去重
+            mobiles.append(mobile)
+    
+    # 限制最多3个手机号码
+    return ','.join(mobiles[:3])
+
+
+def mobile_numbers_overlap(mobile1, mobile2):
+    """
+    检查两个手机号码字符串是否有重叠
+    
+    Args:
+        mobile1 (str): 第一个手机号码字符串
+        mobile2 (str): 第二个手机号码字符串
+        
+    Returns:
+        bool: 是否有重叠的手机号码
+    """
+    if not mobile1 or not mobile2:
+        return False
+    
+    mobiles1 = set(mobile.strip() for mobile in mobile1.split(',') if mobile.strip())
+    mobiles2 = set(mobile.strip() for mobile in mobile2.split(',') if mobile.strip())
+    
+    return bool(mobiles1 & mobiles2)  # 检查交集
+
+
+def merge_mobile_numbers(existing_mobile, new_mobile):
+    """
+    合并手机号码,去重并限制最多3个
+    
+    Args:
+        existing_mobile (str): 现有手机号码字符串
+        new_mobile (str): 新手机号码字符串
+        
+    Returns:
+        str: 合并后的手机号码字符串,最多3个,用逗号分隔
+    """
+    mobiles = []
+    
+    # 添加现有手机号码
+    if existing_mobile:
+        for mobile in existing_mobile.split(','):
+            mobile = mobile.strip()
+            if mobile and mobile not in mobiles:
+                mobiles.append(mobile)
+    
+    # 添加新手机号码
+    if new_mobile:
+        for mobile in new_mobile.split(','):
+            mobile = mobile.strip()
+            if mobile and mobile not in mobiles:
+                mobiles.append(mobile)
+    
+    # 限制最多3个手机号码
+    return ','.join(mobiles[:3])
+
+
 def check_duplicate_business_card(extracted_data):
     """
     检查是否存在重复的名片记录
@@ -133,7 +212,7 @@ def check_duplicate_business_card(extracted_data):
     try:
         # 获取提取的中文姓名和手机号码
         name_zh = extracted_data.get('name_zh', '').strip()
-        mobile = extracted_data.get('mobile', '').strip()
+        mobile = normalize_mobile_numbers(extracted_data.get('mobile', ''))
         
         if not name_zh:
             return {
@@ -158,21 +237,21 @@ def check_duplicate_business_card(extracted_data):
         
         # 如果找到同名记录,进一步检查手机号码
         if mobile:
-            # 有手机号码的情况
+            # 有手机号码的情况,检查是否有重叠的手机号码
             for existing_card in existing_cards:
-                existing_mobile = existing_card.mobile.strip() if existing_card.mobile else ''
+                existing_mobile = existing_card.mobile if existing_card.mobile else ''
                 
-                if existing_mobile == mobile:
-                    # 手机号码相同,更新现有记录
+                if mobile_numbers_overlap(existing_mobile, mobile):
+                    # 手机号码有重叠,更新现有记录
                     return {
                         'is_duplicate': True,
                         'action': 'update',
                         'existing_card': existing_card,
                         'suspected_duplicates': [],
-                        'reason': f'姓名和手机号码均相同:{name_zh} - {mobile}'
+                        'reason': f'姓名相同且手机号码有重叠:{name_zh} - 现有手机号:{existing_mobile}, 新手机号:{mobile}'
                     }
             
-            # 有手机号码但与现有记录不匹配,创建新记录并标记疑似重复
+            # 有手机号码但与现有记录无重叠,创建新记录并标记疑似重复
             suspected_list = []
             for card in existing_cards:
                 suspected_list.append({
@@ -192,7 +271,7 @@ def check_duplicate_business_card(extracted_data):
                 'action': 'create_with_duplicates',
                 'existing_card': None,
                 'suspected_duplicates': suspected_list,
-                'reason': f'姓名相同但手机号码不同:{name_zh},新手机号:{mobile},发现{len(suspected_list)}条疑似重复记录'
+                'reason': f'姓名相同但手机号码无重叠:{name_zh},新手机号:{mobile},发现{len(suspected_list)}条疑似重复记录'
             }
         else:
             # 无手机号码的情况,创建新记录并标记疑似重复
@@ -307,12 +386,22 @@ def create_main_card_with_duplicates(extracted_data, minio_path, suspected_dupli
             }
         initial_career_path = [initial_entry]
         
+        # 处理年龄字段,确保是有效的整数或None
+        age_value = None
+        if extracted_data.get('age'):
+            try:
+                age_value = int(extracted_data.get('age'))
+                if age_value <= 0 or age_value > 150:  # 合理的年龄范围检查
+                    age_value = None
+            except (ValueError, TypeError):
+                age_value = None
+        
         main_card = BusinessCard(
             name_zh=extracted_data.get('name_zh', ''),
             name_en=extracted_data.get('name_en', ''),
             title_zh=extracted_data.get('title_zh', ''),
             title_en=extracted_data.get('title_en', ''),
-            mobile=extracted_data.get('mobile', ''),
+            mobile=normalize_mobile_numbers(extracted_data.get('mobile', '')),
             phone=extracted_data.get('phone', ''),
             email=extracted_data.get('email', ''),
             hotel_zh=extracted_data.get('hotel_zh', ''),
@@ -326,6 +415,8 @@ def create_main_card_with_duplicates(extracted_data, minio_path, suspected_dupli
             affiliation_zh=extracted_data.get('affiliation_zh', ''),
             affiliation_en=extracted_data.get('affiliation_en', ''),
             birthday=datetime.strptime(extracted_data.get('birthday'), '%Y-%m-%d').date() if extracted_data.get('birthday') else None,
+            age=age_value,
+            native_place=extracted_data.get('native_place', ''),
             residence=extracted_data.get('residence', ''),
             image_path=minio_path,  # 最新的图片路径
             career_path=initial_career_path,  # 包含图片路径的职业轨迹
@@ -645,6 +736,8 @@ def extract_fields_from_text(text):
         'affiliation_zh': '',
         'affiliation_en': '',
         'birthday': '',
+        'age': 0,
+        'native_place': '',
         'residence': ''
     }
     
@@ -773,7 +866,7 @@ def parse_text_with_qwen25VLplus(image_data):
 4. 英文职位/头衔 (title_en)
 5. 中文酒店/公司名称 (hotel_zh)
 6. 英文酒店/公司名称 (hotel_en)
-7. 手机号码 (mobile) - 如有多个,使用逗号分隔
+7. 手机号码 (mobile) - 如有多个手机号码,使用逗号分隔,最多提取3个
 8. 固定电话 (phone) - 如有多个,使用逗号分隔
 9. 电子邮箱 (email)
 10. 中文地址 (address_zh)
@@ -781,10 +874,12 @@ def parse_text_with_qwen25VLplus(image_data):
 12. 中文邮政编码 (postal_code_zh)
 13. 英文邮政编码 (postal_code_en)
 14. 生日 (birthday) - 格式为YYYY-MM-DD,如1990-01-01
-15. 居住地 (residence) - 个人居住地址信息
-16. 品牌组合 (brand_group) - 如有多个品牌,使用逗号分隔
-17. 职业轨迹 (career_path) - 如能从名片中推断,以JSON数组格式返回,包含当前日期,公司名称和职位。自动生成当前日期。
-18. 隶属关系 (affiliation) - 如能从名片中推断,以JSON数组格式返回,包含公司名称和隶属集团名称
+15. 年龄 (age) - 数字格式,如30
+16. 籍贯 (native_place) - 出生地或户籍所在地信息
+17. 居住地 (residence) - 个人居住地址信息
+18. 品牌组合 (brand_group) - 如有多个品牌,使用逗号分隔
+19. 职业轨迹 (career_path) - 如能从名片中推断,以JSON数组格式返回,包含当前日期,公司名称和职位。自动生成当前日期。
+20. 隶属关系 (affiliation) - 如能从名片中推断,以JSON数组格式返回,包含公司名称和隶属集团名称
 ## 输出格式
 请以严格的JSON格式返回结果,不要添加任何额外解释文字。JSON格式如下:
 ```json
@@ -803,6 +898,8 @@ def parse_text_with_qwen25VLplus(image_data):
   "postal_code_zh": "",
   "postal_code_en": "",
   "birthday": "",
+  "age": 0,
+  "native_place": "",
   "residence": "",
   "brand_group": "",
   "career_path": [],
@@ -846,13 +943,18 @@ def parse_text_with_qwen25VLplus(image_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', 'residence',
+            'postal_code_zh', 'postal_code_en', 'birthday', 'age', 'native_place', 'residence',
             'brand_group', 'career_path'
         ]
         
         for field in required_fields:
             if field not in extracted_data:
-                extracted_data[field] = [] if field == 'career_path' else ""
+                if field == 'career_path':
+                    extracted_data[field] = []
+                elif field == 'age':
+                    extracted_data[field] = 0
+                else:
+                    extracted_data[field] = ""
         
         # 为career_path增加一条记录
         if extracted_data.get('hotel_zh') or extracted_data.get('hotel_en') or extracted_data.get('title_zh') or extracted_data.get('title_en'):
@@ -905,7 +1007,17 @@ def update_business_card(card_id, data):
         card.name_en = data.get('name_en', card.name_en)
         card.title_zh = data.get('title_zh', card.title_zh)
         card.title_en = data.get('title_en', card.title_en)
-        card.mobile = data.get('mobile', card.mobile)
+        
+        # 处理手机号码字段,支持多个手机号码
+        if 'mobile' in data:
+            new_mobile = normalize_mobile_numbers(data.get('mobile', ''))
+            if new_mobile:
+                # 如果有新的手机号码,合并到现有手机号码中
+                card.mobile = merge_mobile_numbers(card.mobile, new_mobile)
+            elif data.get('mobile') == '':
+                # 如果明确传入空字符串,则清空手机号码
+                card.mobile = ''
+        
         card.phone = data.get('phone', card.phone)
         card.email = data.get('email', card.email)
         card.hotel_zh = data.get('hotel_zh', card.hotel_zh)
@@ -928,6 +1040,19 @@ def update_business_card(card_id, data):
                     card.birthday = None
             else:
                 card.birthday = None
+        
+        # 处理年龄字段
+        if 'age' in data:
+            try:
+                if data['age'] is not None and str(data['age']).strip():
+                    card.age = int(data['age'])
+                else:
+                    card.age = None
+            except (ValueError, TypeError):
+                # 如果年龄格式不正确,保持原值
+                pass
+        
+        card.native_place = data.get('native_place', card.native_place)
         card.residence = data.get('residence', card.residence)
         card.career_path = data.get('career_path', card.career_path)  # 更新职业轨迹
         card.brand_group = data.get('brand_group', card.brand_group)  # 更新品牌组合
@@ -1942,6 +2067,60 @@ def talent_update_tags(data):
             'data': None
         }
 
+def search_business_cards_by_mobile(mobile_number):
+    """
+    根据手机号码搜索名片记录
+    
+    Args:
+        mobile_number (str): 要搜索的手机号码
+        
+    Returns:
+        dict: 包含操作结果和名片列表的字典
+    """
+    try:
+        if not mobile_number or not mobile_number.strip():
+            return {
+                'code': 400,
+                'success': False,
+                'message': '手机号码不能为空',
+                'data': []
+            }
+        
+        mobile_number = mobile_number.strip()
+        
+        # 查询包含该手机号码的名片记录
+        # 使用LIKE查询来匹配逗号分隔的手机号码字段
+        cards = BusinessCard.query.filter(
+            db.or_(
+                BusinessCard.mobile == mobile_number,  # 完全匹配
+                BusinessCard.mobile.like(f'{mobile_number},%'),  # 开头匹配
+                BusinessCard.mobile.like(f'%,{mobile_number},%'),  # 中间匹配
+                BusinessCard.mobile.like(f'%,{mobile_number}')  # 结尾匹配
+            )
+        ).all()
+        
+        # 将所有记录转换为字典格式
+        cards_data = [card.to_dict() for card in cards]
+        
+        return {
+            'code': 200,
+            'success': True,
+            'message': f'搜索到{len(cards_data)}条包含手机号码{mobile_number}的名片记录',
+            'data': cards_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': []
+        }
+
+
 def get_business_card(card_id):
     """
     根据ID从PostgreSQL数据库中获取名片记录
@@ -2831,7 +3010,11 @@ def process_duplicate_record(duplicate_id, action, selected_duplicate_id=None, p
             target_card.name_en = main_card.name_en or target_card.name_en
             target_card.title_zh = main_card.title_zh or target_card.title_zh
             target_card.title_en = main_card.title_en or target_card.title_en
-            target_card.mobile = main_card.mobile or target_card.mobile
+            
+            # 合并手机号码,避免重复
+            if main_card.mobile:
+                target_card.mobile = merge_mobile_numbers(target_card.mobile, main_card.mobile)
+            
             target_card.phone = main_card.phone or target_card.phone
             target_card.email = main_card.email or target_card.email
             target_card.hotel_zh = main_card.hotel_zh or target_card.hotel_zh

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

@@ -195,10 +195,24 @@ def add_business_card(card_data, image_file=None):
                 # 更新现有记录
                 existing_card = duplicate_check['existing_card']
                 
+                # 导入手机号码处理函数
+                from app.core.data_parse.parse import normalize_mobile_numbers, merge_mobile_numbers
+                
                 # 更新基本信息
                 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)
+                
+                # 处理手机号码字段,支持多个手机号码
+                if 'mobile' in card_data:
+                    new_mobile = normalize_mobile_numbers(card_data.get('mobile', ''))
+                    if new_mobile:
+                        # 如果有新的手机号码,合并到现有手机号码中
+                        existing_card.mobile = merge_mobile_numbers(existing_card.mobile, new_mobile)
+                    elif card_data.get('mobile') == '':
+                        # 如果明确传入空字符串,则清空手机号码
+                        existing_card.mobile = ''
+                
                 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)
@@ -218,6 +232,21 @@ def add_business_card(card_data, image_file=None):
                     except ValueError:
                         # 如果日期格式不正确,保持原值
                         pass
+                
+                # 处理年龄字段
+                if 'age' in card_data:
+                    try:
+                        if card_data['age'] is not None and str(card_data['age']).strip():
+                            age_value = int(card_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 = card_data.get('native_place', existing_card.native_place)
                 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  # 更新为最新的图片路径
@@ -276,12 +305,25 @@ def add_business_card(card_data, image_file=None):
                     }
                 initial_career_path = [initial_entry]
                 
+                # 导入手机号码处理函数
+                from app.core.data_parse.parse import normalize_mobile_numbers
+                
+                # 处理年龄字段,确保是有效的整数或None
+                age_value = None
+                if card_data.get('age'):
+                    try:
+                        age_value = int(card_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=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', ''),
+                    mobile=normalize_mobile_numbers(card_data.get('mobile', '')),
                     phone=card_data.get('phone', ''),
                     email=card_data.get('email', ''),
                     hotel_zh=card_data.get('hotel_zh', ''),
@@ -295,6 +337,8 @@ def add_business_card(card_data, image_file=None):
                     affiliation_zh=card_data.get('affiliation_zh', ''),
                     affiliation_en=card_data.get('affiliation_en', ''),
                     birthday=datetime.strptime(card_data.get('birthday'), '%Y-%m-%d').date() if card_data.get('birthday') else None,
+                    age=age_value,
+                    native_place=card_data.get('native_place', ''),
                     residence=card_data.get('residence', ''),
                     image_path=minio_path,  # 最新的图片路径
                     career_path=initial_career_path,  # 包含图片路径的职业轨迹

+ 118 - 0
check_business_cards_table.sql

@@ -0,0 +1,118 @@
+-- ===============================================
+-- 检查business_cards表结构和字段状态
+-- 用途: 验证age和native_place字段的添加情况
+-- ===============================================
+
+-- 1. 检查表是否存在
+SELECT 
+    table_name,
+    table_type,
+    table_schema
+FROM information_schema.tables 
+WHERE table_name = 'business_cards';
+
+-- 2. 查看完整的表结构
+SELECT 
+    column_name,
+    data_type,
+    character_maximum_length,
+    is_nullable,
+    column_default,
+    ordinal_position
+FROM information_schema.columns 
+WHERE table_name = 'business_cards'
+ORDER BY ordinal_position;
+
+-- 3. 专门检查age和native_place字段
+SELECT 
+    column_name,
+    data_type,
+    character_maximum_length,
+    is_nullable,
+    column_default,
+    CASE 
+        WHEN column_name = 'age' THEN '年龄字段'
+        WHEN column_name = 'native_place' THEN '籍贯字段'
+        ELSE '其他字段'
+    END as field_description
+FROM information_schema.columns 
+WHERE table_name = 'business_cards' 
+    AND column_name IN ('age', 'native_place')
+ORDER BY column_name;
+
+-- 4. 查看字段注释
+SELECT 
+    a.attname AS column_name,
+    format_type(a.atttypid, a.atttypmod) AS data_type,
+    COALESCE(pg_catalog.col_description(a.attrelid, a.attnum), '无注释') AS comment
+FROM 
+    pg_catalog.pg_attribute a
+JOIN 
+    pg_catalog.pg_class c ON a.attrelid = c.oid
+JOIN 
+    pg_catalog.pg_namespace n ON c.relnamespace = n.oid
+WHERE 
+    c.relname = 'business_cards' 
+    AND a.attname IN ('age', 'native_place')
+    AND a.attnum > 0 
+    AND NOT a.attisdropped
+ORDER BY a.attname;
+
+-- 5. 检查是否有数据使用了新字段
+SELECT 
+    COUNT(*) as total_records,
+    COUNT(age) as records_with_age,
+    COUNT(native_place) as records_with_native_place,
+    COUNT(CASE WHEN age IS NOT NULL THEN 1 END) as non_null_age,
+    COUNT(CASE WHEN native_place IS NOT NULL AND native_place != '' THEN 1 END) as non_empty_native_place
+FROM business_cards;
+
+-- 6. 如果有数据,显示样本
+SELECT 
+    id,
+    name_zh,
+    age,
+    native_place,
+    created_at
+FROM business_cards 
+WHERE age IS NOT NULL OR (native_place IS NOT NULL AND native_place != '')
+LIMIT 5;
+
+-- 7. 检查age字段的数据范围(如果有数据)
+SELECT 
+    MIN(age) as min_age,
+    MAX(age) as max_age,
+    AVG(age) as avg_age,
+    COUNT(DISTINCT age) as distinct_age_values
+FROM business_cards 
+WHERE age IS NOT NULL;
+
+-- 8. 检查native_place字段的数据统计(如果有数据)
+SELECT 
+    COUNT(DISTINCT native_place) as distinct_native_places,
+    LENGTH(MAX(native_place)) as max_length,
+    LENGTH(MIN(native_place)) as min_length
+FROM business_cards 
+WHERE native_place IS NOT NULL AND native_place != '';
+
+-- 9. 使用psql命令查看表结构(需要在psql中执行)
+-- \d business_cards
+
+-- 10. 检查表的所有约束
+SELECT 
+    tc.constraint_name,
+    tc.constraint_type,
+    tc.table_name,
+    kcu.column_name
+FROM information_schema.table_constraints tc
+JOIN information_schema.key_column_usage kcu 
+    ON tc.constraint_name = kcu.constraint_name
+WHERE tc.table_name = 'business_cards'
+ORDER BY tc.constraint_type, tc.constraint_name;
+
+-- ===============================================
+-- 说明:
+-- 1. 此脚本用于验证字段是否正确添加
+-- 2. 可以多次执行,不会修改数据
+-- 3. 帮助确认数据库结构变更是否成功
+-- =============================================== 

+ 52 - 0
rollback_business_cards_table.sql

@@ -0,0 +1,52 @@
+-- ===============================================
+-- 回滚business_cards表的age和native_place字段修改
+-- 执行日期: 请在执行前填写实际日期
+-- 回滚说明: 删除之前新增的age和native_place字段
+-- ===============================================
+
+-- 警告:执行此脚本将永久删除age和native_place字段及其数据
+-- 请确保已备份相关数据!
+
+-- 1. 检查字段是否存在
+SELECT 
+    column_name,
+    data_type,
+    is_nullable
+FROM information_schema.columns 
+WHERE table_name = 'business_cards' 
+    AND column_name IN ('age', 'native_place')
+ORDER BY column_name;
+
+-- 2. 如果需要保留数据,可先导出这些字段的数据
+-- SELECT id, name_zh, age, native_place 
+-- FROM business_cards 
+-- WHERE age IS NOT NULL OR native_place IS NOT NULL;
+
+-- 3. 删除native_place字段
+ALTER TABLE business_cards 
+DROP COLUMN IF EXISTS native_place;
+
+-- 4. 删除age字段  
+ALTER TABLE business_cards 
+DROP COLUMN IF EXISTS age;
+
+-- 5. 验证字段是否已删除
+SELECT 
+    column_name,
+    data_type,
+    is_nullable
+FROM information_schema.columns 
+WHERE table_name = 'business_cards' 
+    AND column_name IN ('age', 'native_place')
+ORDER BY column_name;
+
+-- 6. 检查表结构
+\d business_cards;
+
+-- ===============================================
+-- 执行说明:
+-- 1. 此操作不可逆,请谨慎执行
+-- 2. 执行前请确保已备份相关数据
+-- 3. 建议在业务低峰期执行
+-- 4. 如果有应用程序依赖这些字段,请先更新应用代码
+-- =============================================== 

+ 61 - 0
step_by_step_alter.sql

@@ -0,0 +1,61 @@
+-- ===============================================
+-- 分步骤修改business_cards表 - 逐步执行
+-- 说明:请一步一步执行,每执行一步后检查结果
+-- ===============================================
+
+-- 第1步:检查表是否存在
+SELECT table_name FROM information_schema.tables WHERE table_name = 'business_cards';
+-- 期望结果:应该返回 business_cards
+
+-- 第2步:查看当前表结构
+SELECT column_name, data_type FROM information_schema.columns 
+WHERE table_name = 'business_cards' ORDER BY ordinal_position;
+-- 查看当前字段列表
+
+-- 第3步:添加age字段
+ALTER TABLE business_cards ADD COLUMN age INTEGER;
+-- 执行后应该返回:ALTER TABLE
+
+-- 第4步:验证age字段是否添加成功
+SELECT column_name, data_type FROM information_schema.columns 
+WHERE table_name = 'business_cards' AND column_name = 'age';
+-- 期望结果:age | integer
+
+-- 第5步:添加native_place字段
+ALTER TABLE business_cards ADD COLUMN native_place TEXT;
+-- 执行后应该返回:ALTER TABLE
+
+-- 第6步:验证native_place字段是否添加成功
+SELECT column_name, data_type FROM information_schema.columns 
+WHERE table_name = 'business_cards' AND column_name = 'native_place';
+-- 期望结果:native_place | text
+
+-- 第7步:为age字段添加注释
+COMMENT ON COLUMN business_cards.age IS '年龄字段 - 存储人员年龄信息,取值范围1-150';
+-- 执行后应该返回:COMMENT
+
+-- 第8步:为native_place字段添加注释
+COMMENT ON COLUMN business_cards.native_place IS '籍贯字段 - 存储人员籍贯或出生地信息';
+-- 执行后应该返回:COMMENT
+
+-- 第9步:最终验证 - 查看两个新字段
+SELECT column_name, data_type, is_nullable 
+FROM information_schema.columns 
+WHERE table_name = 'business_cards' 
+  AND column_name IN ('age', 'native_place')
+ORDER BY column_name;
+-- 期望结果:
+-- age          | integer | YES
+-- native_place | text    | YES
+
+-- 第10步:测试插入数据(可选)
+-- INSERT INTO business_cards (name_zh, age, native_place) 
+-- VALUES ('测试', 30, '北京市') RETURNING id, name_zh, age, native_place;
+
+-- ===============================================
+-- 执行指南:
+-- 1. 复制每个步骤单独执行
+-- 2. 检查每步的执行结果
+-- 3. 如果某步失败,停止并检查错误原因
+-- 4. 全部完成后,新字段就可以正常使用了
+-- ===============================================