""" 重新建立模板和字段的关联关系 根据模板名称,重新建立 f_polic_file_field 表的关联关系 不再依赖 input_data 和 template_code 字段 """ import pymysql import os import json from typing import Dict, List, Set, Optional from datetime import datetime from collections import defaultdict # 数据库连接配置 DB_CONFIG = { 'host': os.getenv('DB_HOST', '152.136.177.240'), 'port': int(os.getenv('DB_PORT', 5012)), 'user': os.getenv('DB_USER', 'finyx'), 'password': os.getenv('DB_PASSWORD', '6QsGK6MpePZDE57Z'), 'database': os.getenv('DB_NAME', 'finyx'), 'charset': 'utf8mb4' } TENANT_ID = 615873064429507639 CREATED_BY = 655162080928945152 UPDATED_BY = 655162080928945152 # 模板名称到字段编码的映射(根据业务逻辑定义) # 格式:{模板名称: {'input_fields': [字段编码列表], 'output_fields': [字段编码列表]}} TEMPLATE_FIELD_MAPPING = { # 初步核实审批表 '初步核实审批表': { 'input_fields': ['clue_info', 'target_basic_info_clue'], 'output_fields': [ 'target_name', 'target_organization_and_position', 'target_organization', 'target_position', 'target_gender', 'target_date_of_birth', 'target_age', 'target_education_level', 'target_political_status', 'target_professional_rank', 'clue_source', 'target_issue_description', 'department_opinion', 'filler_name' ] }, # 谈话前安全风险评估表 '谈话前安全风险评估表': { 'input_fields': ['clue_info', 'target_basic_info_clue'], 'output_fields': [ 'target_family_situation', 'target_social_relations', 'target_health_status', 'target_personality', 'target_tolerance', 'target_issue_severity', 'target_other_issues_possibility', 'target_previous_investigation', 'target_negative_events', 'target_other_situation', 'risk_level' ] }, # 请示报告卡 '请示报告卡': { 'input_fields': ['clue_info'], 'output_fields': ['target_name', 'target_organization_and_position', 'report_card_request_time'] }, # 初核方案 '初核方案': { 'input_fields': ['clue_info', 'target_basic_info_clue'], 'output_fields': [ 'target_name', 'target_organization_and_position', 'target_work_basic_info', 'target_issue_description', 'investigation_unit_name', 'investigation_team_leader_name', 'investigation_team_member_names', 'investigation_location' ] }, # 谈话通知书 '谈话通知书': { 'input_fields': ['target_basic_info_clue'], 'output_fields': [ 'target_name', 'target_organization_and_position', 'target_id_number', 'appointment_time', 'appointment_location', 'approval_time', 'handling_department', 'handler_name', 'notification_time', 'notification_location' ] }, # 谈话通知书第一联 '谈话通知书第一联': { 'input_fields': ['target_basic_info_clue'], 'output_fields': [ 'target_name', 'target_organization_and_position', 'target_id_number', 'appointment_time', 'appointment_location', 'approval_time', 'handling_department', 'handler_name', 'notification_time', 'notification_location' ] }, # 谈话通知书第二联 '谈话通知书第二联': { 'input_fields': ['target_basic_info_clue'], 'output_fields': [ 'target_name', 'target_organization_and_position', 'target_id_number', 'appointment_time', 'appointment_location', 'approval_time', 'handling_department', 'handler_name', 'notification_time', 'notification_location' ] }, # 谈话通知书第三联 '谈话通知书第三联': { 'input_fields': ['target_basic_info_clue'], 'output_fields': [ 'target_name', 'target_organization_and_position', 'target_id_number', 'appointment_time', 'appointment_location', 'approval_time', 'handling_department', 'handler_name', 'notification_time', 'notification_location' ] }, # 谈话笔录 '谈话笔录': { 'input_fields': ['clue_info', 'target_basic_info_clue'], 'output_fields': [ 'target_name', 'target_organization_and_position', 'target_gender', 'target_date_of_birth_full', 'target_political_status', 'target_address', 'target_registered_address', 'target_contact', 'target_place_of_origin', 'target_ethnicity', 'target_id_number', 'investigation_team_code' ] }, # 谈话后安全风险评估表 '谈话后安全风险评估表': { 'input_fields': ['clue_info', 'target_basic_info_clue'], 'output_fields': [ 'target_name', 'target_organization_and_position', 'target_gender', 'target_date_of_birth_full', 'target_political_status', 'target_address', 'target_registered_address', 'target_contact', 'target_place_of_origin', 'target_ethnicity', 'target_id_number', 'investigation_team_code' ] }, # XXX初核情况报告 'XXX初核情况报告': { 'input_fields': ['clue_info', 'target_basic_info_clue'], 'output_fields': [ 'target_name', 'target_organization_and_position', 'target_issue_description', 'target_work_basic_info', 'investigation_unit_name', 'investigation_team_leader_name' ] }, # 走读式谈话审批 '走读式谈话审批': { 'input_fields': ['target_basic_info_clue'], 'output_fields': [ 'target_name', 'target_organization_and_position', 'target_id_number', 'appointment_time', 'appointment_location', 'approval_time', 'handling_department', 'handler_name' ] }, # 走读式谈话流程 '走读式谈话流程': { 'input_fields': ['target_basic_info_clue'], 'output_fields': [ 'target_name', 'target_organization_and_position', 'target_id_number', 'appointment_time', 'appointment_location', 'approval_time', 'handling_department', 'handler_name' ] }, # 谈话审批 / 谈话审批表 '谈话审批': { 'input_fields': ['target_basic_info_clue'], 'output_fields': [ 'target_name', 'target_organization_and_position', 'target_id_number', 'appointment_time', 'appointment_location', 'approval_time', 'handling_department', 'handler_name' ] }, '谈话审批表': { 'input_fields': ['clue_info', 'target_basic_info_clue'], 'output_fields': [ 'target_name', 'target_organization_and_position', 'target_gender', 'target_date_of_birth_full', 'target_political_status', 'target_address', 'target_registered_address', 'target_contact', 'target_place_of_origin', 'target_ethnicity', 'target_id_number', 'investigation_team_code' ] }, } # 模板名称的标准化映射(处理不同的命名方式) TEMPLATE_NAME_NORMALIZE = { '1.请示报告卡(XXX)': '请示报告卡', '2.初步核实审批表(XXX)': '初步核实审批表', '3.附件初核方案(XXX)': '初核方案', '8.XXX初核情况报告': 'XXX初核情况报告', '2.谈话审批': '谈话审批', '2谈话审批表': '谈话审批表', } def generate_id(): """生成ID(使用时间戳+随机数的方式,模拟雪花算法)""" import time import random timestamp = int(time.time() * 1000) random_part = random.randint(100000, 999999) return timestamp * 1000 + random_part def normalize_template_name(name: str) -> str: """标准化模板名称""" # 先检查映射表 if name in TEMPLATE_NAME_NORMALIZE: return TEMPLATE_NAME_NORMALIZE[name] # 移除常见的后缀和前缀 name = name.strip() # 移除括号内容 import re name = re.sub(r'[((].*?[))]', '', name) name = name.strip() # 移除数字前缀和点号 name = re.sub(r'^\d+\.', '', name) name = name.strip() return name def get_all_templates(conn) -> Dict: """获取所有模板配置""" cursor = conn.cursor(pymysql.cursors.DictCursor) sql = """ SELECT id, name, parent_id, state FROM f_polic_file_config WHERE tenant_id = %s ORDER BY name """ cursor.execute(sql, (TENANT_ID,)) templates = cursor.fetchall() result = {} for template in templates: name = template['name'] normalized_name = normalize_template_name(name) # 处理state字段(可能是二进制格式) state = template['state'] if isinstance(state, bytes): state = int.from_bytes(state, byteorder='big') elif isinstance(state, (int, str)): state = int(state) else: state = 0 result[template['id']] = { 'id': template['id'], 'name': name, 'normalized_name': normalized_name, 'parent_id': template['parent_id'], 'state': state } cursor.close() return result def get_all_fields(conn) -> Dict: """获取所有字段定义""" cursor = conn.cursor(pymysql.cursors.DictCursor) sql = """ SELECT id, name, filed_code, field_type, state FROM f_polic_field WHERE tenant_id = %s ORDER BY field_type, filed_code """ cursor.execute(sql, (TENANT_ID,)) fields = cursor.fetchall() result = { 'by_code': {}, 'by_name': {}, 'input_fields': [], 'output_fields': [] } for field in fields: field_code = field['filed_code'] field_name = field['name'] field_type = field['field_type'] result['by_code'][field_code] = field result['by_name'][field_name] = field if field_type == 1: result['input_fields'].append(field) elif field_type == 2: result['output_fields'].append(field) cursor.close() return result def get_existing_relations(conn) -> Set[tuple]: """获取现有的关联关系""" cursor = conn.cursor(pymysql.cursors.DictCursor) sql = """ SELECT file_id, filed_id FROM f_polic_file_field WHERE tenant_id = %s """ cursor.execute(sql, (TENANT_ID,)) relations = cursor.fetchall() result = {(rel['file_id'], rel['filed_id']) for rel in relations} cursor.close() return result def rebuild_template_relations(conn, template_id: int, template_name: str, normalized_name: str, field_mapping: Dict, dry_run: bool = True) -> Dict: """重建单个模板的关联关系""" cursor = conn.cursor() # 查找模板对应的字段配置 template_config = None # 优先精确匹配标准化名称 if normalized_name in TEMPLATE_FIELD_MAPPING: template_config = TEMPLATE_FIELD_MAPPING[normalized_name] else: # 尝试模糊匹配 for name, config in TEMPLATE_FIELD_MAPPING.items(): if name == normalized_name or name in normalized_name or normalized_name in name: template_config = config break # 也检查原始名称 if name in template_name or template_name in name: template_config = config break if not template_config: return { 'template_id': template_id, 'template_name': template_name, 'status': 'skipped', 'reason': '未找到字段配置映射', 'input_count': 0, 'output_count': 0 } input_field_codes = template_config.get('input_fields', []) output_field_codes = template_config.get('output_fields', []) # 查找字段ID input_field_ids = [] output_field_ids = [] for field_code in input_field_codes: field = field_mapping['by_code'].get(field_code) if field: if field['field_type'] == 1: input_field_ids.append(field['id']) else: print(f" ⚠ 警告: 字段 {field_code} 应该是输入字段,但实际类型为 {field['field_type']}") else: print(f" ⚠ 警告: 字段 {field_code} 不存在") for field_code in output_field_codes: field = field_mapping['by_code'].get(field_code) if field: if field['field_type'] == 2: output_field_ids.append(field['id']) else: print(f" ⚠ 警告: 字段 {field_code} 应该是输出字段,但实际类型为 {field['field_type']}") else: print(f" ⚠ 警告: 字段 {field_code} 不存在") # 删除旧的关联关系 if not dry_run: delete_sql = """ DELETE FROM f_polic_file_field WHERE tenant_id = %s AND file_id = %s """ cursor.execute(delete_sql, (TENANT_ID, template_id)) deleted_count = cursor.rowcount else: deleted_count = 0 # 创建新的关联关系 created_count = 0 all_field_ids = input_field_ids + output_field_ids for field_id in all_field_ids: if not dry_run: # 检查是否已存在(虽然已经删除了,但为了安全还是检查一下) check_sql = """ SELECT id FROM f_polic_file_field WHERE tenant_id = %s AND file_id = %s AND filed_id = %s """ cursor.execute(check_sql, (TENANT_ID, template_id, field_id)) existing = cursor.fetchone() if not existing: relation_id = generate_id() insert_sql = """ INSERT INTO f_polic_file_field (id, tenant_id, file_id, filed_id, created_time, created_by, updated_time, updated_by, state) VALUES (%s, %s, %s, %s, NOW(), %s, NOW(), %s, %s) """ cursor.execute(insert_sql, ( relation_id, TENANT_ID, template_id, field_id, CREATED_BY, UPDATED_BY, 1 # state=1 表示启用 )) created_count += 1 else: created_count += 1 if not dry_run: conn.commit() return { 'template_id': template_id, 'template_name': template_name, 'normalized_name': normalized_name, 'status': 'success', 'deleted_count': deleted_count, 'input_count': len(input_field_ids), 'output_count': len(output_field_ids), 'created_count': created_count } def main(dry_run: bool = True): """主函数""" print("="*80) print("重新建立模板和字段的关联关系") print("="*80) if dry_run: print("\n[DRY RUN模式 - 不会实际修改数据库]") else: print("\n[实际执行模式 - 将修改数据库]") try: conn = pymysql.connect(**DB_CONFIG) print("✓ 数据库连接成功\n") # 获取所有模板 print("1. 获取所有模板配置...") templates = get_all_templates(conn) print(f" 找到 {len(templates)} 个模板") # 获取所有字段 print("\n2. 获取所有字段定义...") field_mapping = get_all_fields(conn) print(f" 输入字段: {len(field_mapping['input_fields'])} 个") print(f" 输出字段: {len(field_mapping['output_fields'])} 个") print(f" 总字段数: {len(field_mapping['by_code'])} 个") # 获取现有关联关系 print("\n3. 获取现有关联关系...") existing_relations = get_existing_relations(conn) print(f" 现有关联关系: {len(existing_relations)} 条") # 重建关联关系 print("\n4. 重建模板和字段的关联关系...") print("="*80) results = [] for template_id, template_info in templates.items(): template_name = template_info['name'] normalized_name = template_info['normalized_name'] state = template_info['state'] # 处理所有模板(包括未启用的,因为可能需要建立关联) # 但可以记录状态 status_note = f" (state={state})" if state != 1 else "" if state != 1: print(f"\n处理未启用的模板: {template_name}{status_note}") print(f"\n处理模板: {template_name}") print(f" 标准化名称: {normalized_name}") result = rebuild_template_relations( conn, template_id, template_name, normalized_name, field_mapping, dry_run=dry_run ) results.append(result) if result['status'] == 'success': print(f" ✓ 成功: 删除 {result['deleted_count']} 条旧关联, " f"创建 {result['created_count']} 条新关联 " f"(输入字段: {result['input_count']}, 输出字段: {result['output_count']})") else: print(f" ⚠ {result['status']}: {result.get('reason', '')}") # 统计信息 print("\n" + "="*80) print("处理结果统计") print("="*80) success_count = sum(1 for r in results if r['status'] == 'success') skipped_count = sum(1 for r in results if r['status'] == 'skipped') total_input = sum(r.get('input_count', 0) for r in results) total_output = sum(r.get('output_count', 0) for r in results) total_created = sum(r.get('created_count', 0) for r in results) print(f"\n成功处理: {success_count} 个模板") print(f"跳过: {skipped_count} 个模板") print(f"总输入字段关联: {total_input} 条") print(f"总输出字段关联: {total_output} 条") print(f"总关联关系: {total_created} 条") # 显示详细结果 print("\n详细结果:") for result in results: if result['status'] == 'success': print(f" - {result['template_name']}: " f"输入字段 {result['input_count']} 个, " f"输出字段 {result['output_count']} 个") else: print(f" - {result['template_name']}: {result['status']} - {result.get('reason', '')}") print("\n" + "="*80) if dry_run: print("\n这是DRY RUN模式,未实际修改数据库。") print("要实际执行,请运行: python rebuild_template_field_relations.py --execute") else: print("\n✓ 关联关系已更新完成") except Exception as e: print(f"\n✗ 发生错误: {e}") import traceback traceback.print_exc() if not dry_run: conn.rollback() finally: conn.close() print("\n数据库连接已关闭") if __name__ == '__main__': import sys dry_run = '--execute' not in sys.argv if not dry_run: print("\n⚠ 警告: 这将修改数据库!") response = input("确认要继续吗? (yes/no): ") if response.lower() != 'yes': print("操作已取消") sys.exit(0) main(dry_run=dry_run)