From 16bb98131eb9ceb8688484e214b56eeddf9aeb23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=9D=B0=20Jie=20Zhao=20=EF=BC=88=E9=9B=84?= =?UTF-8?q?=E7=8B=AE=E6=B1=BD=E8=BD=A6=E7=A7=91=E6=8A=80=EF=BC=89?= <00061074@chery.local> Date: Mon, 22 Sep 2025 11:24:32 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E6=8F=90=E4=BA=A4=20?= =?UTF-8?q?-=20=E5=91=A8=E4=B8=80=202025/09/22=2011:24:32.93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/field_mapping_config.json | 334 +++++++++++++++ src/integrations/flexible_field_mapper.py | 447 ++++++++++++++++++++ src/integrations/workorder_sync.py | 128 ++++-- src/web/blueprints/feishu_sync.py | 126 +++++- src/web/static/js/dashboard.js | 248 +++++++++++ src/web/templates/dashboard.html | 34 ++ src/web/templates/field_mapping.html | 475 ++++++++++++++++++++++ 前端页面整理总结.md | 188 +++++++++ 灵活字段映射系统使用说明.md | 219 ++++++++++ 飞书同步灵活字段映射系统总结.md | 231 +++++++++++ 10 files changed, 2399 insertions(+), 31 deletions(-) create mode 100644 config/field_mapping_config.json create mode 100644 src/integrations/flexible_field_mapper.py create mode 100644 src/web/templates/field_mapping.html create mode 100644 前端页面整理总结.md create mode 100644 灵活字段映射系统使用说明.md create mode 100644 飞书同步灵活字段映射系统总结.md diff --git a/config/field_mapping_config.json b/config/field_mapping_config.json new file mode 100644 index 0000000..3c4821d --- /dev/null +++ b/config/field_mapping_config.json @@ -0,0 +1,334 @@ +{ + "field_mapping": { + "TR Number": "order_id", + "TR Description": "description", + "Type of problem": "category", + "TR Level": "priority", + "TR Status": "status", + "Source": "source", + "Date creation": "created_at", + "处理过程": "solution", + "TR tracking": "resolution", + "Created by": "created_by", + "Module(模块)": "module", + "Wilfulness(责任人)": "wilfulness", + "Date of close TR": "date_of_close", + "Vehicle Type01": "vehicle_type", + "VIN|sim": "vin_sim", + "App remote control version": "app_remote_control_version", + "HMI SW": "hmi_sw", + "父记录": "parent_record", + "Has it been updated on the same day": "has_updated_same_day", + "Operating time": "operating_time", + "AI建议": "ai_suggestion", + "Issue Start Time": "updated_at" + }, + "field_aliases": { + "order_id": [ + "TR Number", + "TR编号", + "工单号", + "Order ID", + "Ticket ID", + "工单编号", + "新字段1", + "新字段" + ], + "description": [ + "TR Description", + "TR描述", + "描述", + "Description", + "问题描述", + "详细描述" + ], + "category": [ + "Type of problem", + "问题类型", + "Category", + "分类", + "Problem Type", + "问题分类" + ], + "priority": [ + "TR Level", + "优先级", + "Priority", + "Level", + "紧急程度", + "重要程度" + ], + "status": [ + "TR Status", + "状态", + "Status", + "工单状态", + "处理状态" + ], + "source": [ + "Source", + "来源", + "Source Type", + "来源类型", + "提交来源" + ], + "created_at": [ + "Date creation", + "创建日期", + "Created At", + "Creation Date", + "创建时间" + ], + "solution": [ + "处理过程", + "Solution", + "解决方案", + "Process", + "处理方案" + ], + "resolution": [ + "TR tracking", + "Resolution", + "解决结果", + "跟踪", + "处理结果" + ], + "created_by": [ + "Created by", + "创建人", + "Creator", + "Created By", + "提交人" + ], + "vehicle_type": [ + "Vehicle Type01", + "车型", + "Vehicle Type", + "车辆类型", + "车款" + ], + "vin_sim": [ + "VIN|sim", + "VIN", + "车架号", + "SIM", + "VIN/SIM", + "车辆识别号" + ], + "module": [ + "Module(模块)", + "模块", + "Module", + "功能模块" + ], + "wilfulness": [ + "Wilfulness(责任人)", + "责任人", + "负责人", + "Assignee" + ], + "date_of_close": [ + "Date of close TR", + "关闭日期", + "Close Date", + "完成日期" + ], + "app_remote_control_version": [ + "App remote control version", + "应用远程控制版本", + "App Version", + "应用版本" + ], + "hmi_sw": [ + "HMI SW", + "HMI软件版本", + "HMI Software", + "人机界面软件" + ], + "parent_record": [ + "父记录", + "Parent Record", + "上级记录", + "关联记录" + ], + "has_updated_same_day": [ + "Has it been updated on the same day", + "是否同日更新", + "Same Day Update", + "当日更新" + ], + "operating_time": [ + "Operating time", + "操作时间", + "Operation Time", + "运行时间" + ], + "ai_suggestion": [ + "AI建议", + "AI Suggestion", + "AI建议", + "智能建议" + ], + "updated_at": [ + "Issue Start Time", + "问题开始时间", + "Start Time", + "更新时间" + ] + }, + "field_patterns": { + "order_id": [ + ".*number.*", + ".*id.*", + ".*编号.*", + ".*ticket.*", + ".*新.*" + ], + "description": [ + ".*description.*", + ".*描述.*", + ".*detail.*", + ".*内容.*" + ], + "category": [ + ".*type.*", + ".*category.*", + ".*分类.*", + ".*类型.*", + ".*problem.*" + ], + "priority": [ + ".*level.*", + ".*priority.*", + ".*优先级.*", + ".*urgent.*" + ], + "status": [ + ".*status.*", + ".*状态.*", + ".*state.*" + ], + "source": [ + ".*source.*", + ".*来源.*", + ".*origin.*" + ], + "created_at": [ + ".*creation.*", + ".*created.*", + ".*创建.*", + ".*date.*" + ], + "solution": [ + ".*solution.*", + ".*处理.*", + ".*解决.*", + ".*process.*" + ], + "resolution": [ + ".*resolution.*", + ".*tracking.*", + ".*跟踪.*", + ".*result.*" + ], + "created_by": [ + ".*created.*by.*", + ".*creator.*", + ".*创建人.*", + ".*author.*" + ], + "vehicle_type": [ + ".*vehicle.*type.*", + ".*车型.*", + ".*车辆.*", + ".*car.*" + ], + "vin_sim": [ + ".*vin.*", + ".*sim.*", + ".*车架.*", + ".*识别.*" + ], + "module": [ + ".*module.*", + ".*模块.*", + ".*功能.*" + ], + "wilfulness": [ + ".*wilfulness.*", + ".*责任人.*", + ".*负责人.*", + ".*assignee.*" + ], + "date_of_close": [ + ".*close.*", + ".*关闭.*", + ".*完成.*", + ".*finish.*" + ], + "app_remote_control_version": [ + ".*app.*version.*", + ".*应用.*版本.*", + ".*remote.*control.*" + ], + "hmi_sw": [ + ".*hmi.*", + ".*软件.*", + ".*software.*" + ], + "parent_record": [ + ".*parent.*", + ".*父.*", + ".*上级.*", + ".*关联.*" + ], + "has_updated_same_day": [ + ".*same.*day.*", + ".*同日.*", + ".*当日.*", + ".*updated.*same.*" + ], + "operating_time": [ + ".*operating.*time.*", + ".*操作.*时间.*", + ".*运行.*时间.*" + ], + "ai_suggestion": [ + ".*ai.*suggestion.*", + ".*ai.*建议.*", + ".*智能.*建议.*" + ], + "updated_at": [ + ".*start.*time.*", + ".*开始.*时间.*", + ".*updated.*at.*", + ".*更新时间.*" + ] + }, + "field_priorities": { + "order_id": 3, + "description": 1, + "category": 1, + "priority": 1, + "status": 1, + "created_at": 1, + "source": 2, + "solution": 2, + "resolution": 2, + "created_by": 2, + "vehicle_type": 2, + "vin_sim": 2, + "module": 3, + "wilfulness": 3, + "date_of_close": 3, + "app_remote_control_version": 3, + "hmi_sw": 3, + "parent_record": 3, + "has_updated_same_day": 3, + "operating_time": 3, + "ai_suggestion": 3, + "updated_at": 2 + }, + "auto_mapping_enabled": true, + "similarity_threshold": 0.6 +} \ No newline at end of file diff --git a/src/integrations/flexible_field_mapper.py b/src/integrations/flexible_field_mapper.py new file mode 100644 index 0000000..6bb3c8a --- /dev/null +++ b/src/integrations/flexible_field_mapper.py @@ -0,0 +1,447 @@ +# -*- coding: utf-8 -*- +""" +灵活字段映射器 +支持动态字段发现、智能映射和配置管理 +""" + +import json +import logging +from typing import Dict, List, Optional, Any, Tuple +from datetime import datetime +import difflib +from collections import defaultdict + +logger = logging.getLogger(__name__) + +class FlexibleFieldMapper: + """灵活字段映射器""" + + def __init__(self, config_file: str = "config/field_mapping_config.json"): + """ + 初始化字段映射器 + + Args: + config_file: 字段映射配置文件路径 + """ + self.config_file = config_file + self.field_mapping = {} + self.field_aliases = {} # 字段别名映射 + self.field_patterns = {} # 字段模式匹配 + self.field_priorities = {} # 字段优先级 + self.auto_mapping_enabled = True + self.similarity_threshold = 0.6 # 相似度阈值 + + # 加载配置 + self._load_config() + + # 初始化默认映射规则 + self._init_default_mappings() + + def _load_config(self): + """加载字段映射配置""" + try: + with open(self.config_file, 'r', encoding='utf-8') as f: + config = json.load(f) + self.field_mapping = config.get('field_mapping', {}) + self.field_aliases = config.get('field_aliases', {}) + self.field_patterns = config.get('field_patterns', {}) + self.field_priorities = config.get('field_priorities', {}) + self.auto_mapping_enabled = config.get('auto_mapping_enabled', True) + self.similarity_threshold = config.get('similarity_threshold', 0.6) + except FileNotFoundError: + logger.info(f"配置文件 {self.config_file} 不存在,将创建默认配置") + self._create_default_config() + except Exception as e: + logger.error(f"加载配置文件失败: {e}") + self._create_default_config() + + def _create_default_config(self): + """创建默认配置""" + default_config = { + "field_mapping": {}, + "field_aliases": {}, + "field_patterns": {}, + "field_priorities": {}, + "auto_mapping_enabled": True, + "similarity_threshold": 0.6 + } + self._save_config(default_config) + + def _save_config(self, config: Dict[str, Any]): + """保存配置到文件""" + try: + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(config, f, ensure_ascii=False, indent=2) + except Exception as e: + logger.error(f"保存配置文件失败: {e}") + + def _init_default_mappings(self): + """初始化默认字段映射规则""" + # 核心字段的别名和模式 + core_fields = { + 'order_id': { + 'aliases': ['TR Number', 'TR编号', '工单号', 'Order ID', 'Ticket ID'], + 'patterns': [r'.*number.*', r'.*id.*', r'.*编号.*'], + 'priority': 1 + }, + 'description': { + 'aliases': ['TR Description', 'TR描述', '描述', 'Description', '问题描述'], + 'patterns': [r'.*description.*', r'.*描述.*', r'.*detail.*'], + 'priority': 1 + }, + 'category': { + 'aliases': ['Type of problem', '问题类型', 'Category', '分类', 'Problem Type'], + 'patterns': [r'.*type.*', r'.*category.*', r'.*分类.*', r'.*类型.*'], + 'priority': 1 + }, + 'priority': { + 'aliases': ['TR Level', '优先级', 'Priority', 'Level', '紧急程度'], + 'patterns': [r'.*level.*', r'.*priority.*', r'.*优先级.*'], + 'priority': 1 + }, + 'status': { + 'aliases': ['TR Status', '状态', 'Status', '工单状态'], + 'patterns': [r'.*status.*', r'.*状态.*'], + 'priority': 1 + }, + 'source': { + 'aliases': ['Source', '来源', 'Source Type', '来源类型'], + 'patterns': [r'.*source.*', r'.*来源.*'], + 'priority': 2 + }, + 'created_at': { + 'aliases': ['Date creation', '创建日期', 'Created At', 'Creation Date'], + 'patterns': [r'.*creation.*', r'.*created.*', r'.*创建.*', r'.*date.*'], + 'priority': 1 + }, + 'solution': { + 'aliases': ['处理过程', 'Solution', '解决方案', 'Process'], + 'patterns': [r'.*solution.*', r'.*处理.*', r'.*解决.*'], + 'priority': 2 + }, + 'resolution': { + 'aliases': ['TR tracking', 'Resolution', '解决结果', '跟踪'], + 'patterns': [r'.*resolution.*', r'.*tracking.*', r'.*跟踪.*'], + 'priority': 2 + }, + 'created_by': { + 'aliases': ['Created by', '创建人', 'Creator', 'Created By'], + 'patterns': [r'.*created.*by.*', r'.*creator.*', r'.*创建人.*'], + 'priority': 2 + }, + 'vehicle_type': { + 'aliases': ['Vehicle Type01', '车型', 'Vehicle Type', '车辆类型'], + 'patterns': [r'.*vehicle.*type.*', r'.*车型.*', r'.*车辆.*'], + 'priority': 2 + }, + 'vin_sim': { + 'aliases': ['VIN|sim', 'VIN', '车架号', 'SIM', 'VIN/SIM'], + 'patterns': [r'.*vin.*', r'.*sim.*', r'.*车架.*'], + 'priority': 2 + } + } + + # 更新配置 + for field, config in core_fields.items(): + if field not in self.field_aliases: + self.field_aliases[field] = config['aliases'] + if field not in self.field_patterns: + self.field_patterns[field] = config['patterns'] + if field not in self.field_priorities: + self.field_priorities[field] = config['priority'] + + def discover_fields(self, feishu_fields: Dict[str, Any]) -> Dict[str, List[str]]: + """ + 发现飞书字段并尝试自动映射 + + Args: + feishu_fields: 飞书字段数据 + + Returns: + 字段发现结果,包含已映射、未映射和建议映射的字段 + """ + logger.info(f"开始发现字段: {list(feishu_fields.keys())}") + + result = { + 'mapped_fields': {}, # 已映射字段 + 'unmapped_fields': [], # 未映射字段 + 'suggested_mappings': {}, # 建议映射 + 'field_analysis': {} # 字段分析 + } + + # 分析每个飞书字段 + for feishu_field in feishu_fields.keys(): + analysis = self._analyze_field(feishu_field, feishu_fields[feishu_field]) + result['field_analysis'][feishu_field] = analysis + + # 尝试映射 + mapped_field = self.map_field(feishu_field) + if mapped_field: + result['mapped_fields'][feishu_field] = mapped_field + else: + result['unmapped_fields'].append(feishu_field) + + # 生成建议映射 + suggestions = self._suggest_mapping(feishu_field) + if suggestions: + result['suggested_mappings'][feishu_field] = suggestions + + logger.info(f"字段发现完成: 已映射 {len(result['mapped_fields'])}, " + f"未映射 {len(result['unmapped_fields'])}, " + f"建议映射 {len(result['suggested_mappings'])}") + + return result + + def _analyze_field(self, field_name: str, field_value: Any) -> Dict[str, Any]: + """ + 分析字段特征 + + Args: + field_name: 字段名 + field_value: 字段值 + + Returns: + 字段分析结果 + """ + analysis = { + 'name': field_name, + 'value_type': type(field_value).__name__, + 'value_length': len(str(field_value)) if field_value else 0, + 'is_empty': not field_value or str(field_value).strip() == '', + 'contains_chinese': any('\u4e00' <= char <= '\u9fff' for char in str(field_name)), + 'contains_numbers': any(char.isdigit() for char in str(field_name)), + 'contains_special_chars': any(char in '|()[]{}' for char in str(field_name)), + 'word_count': len(str(field_name).split()), + 'similarity_scores': {} + } + + # 计算与已知字段的相似度 + for local_field, aliases in self.field_aliases.items(): + max_similarity = 0 + for alias in aliases: + similarity = difflib.SequenceMatcher(None, field_name.lower(), alias.lower()).ratio() + max_similarity = max(max_similarity, similarity) + analysis['similarity_scores'][local_field] = max_similarity + + return analysis + + def _suggest_mapping(self, feishu_field: str) -> List[Dict[str, Any]]: + """ + 为未映射字段生成建议映射 + + Args: + feishu_field: 飞书字段名 + + Returns: + 建议映射列表 + """ + suggestions = [] + + # 基于相似度的建议 + for local_field, aliases in self.field_aliases.items(): + max_similarity = 0 + best_alias = "" + + for alias in aliases: + similarity = difflib.SequenceMatcher(None, feishu_field.lower(), alias.lower()).ratio() + if similarity > max_similarity: + max_similarity = similarity + best_alias = alias + + if max_similarity >= self.similarity_threshold: + suggestions.append({ + 'local_field': local_field, + 'similarity': max_similarity, + 'matched_alias': best_alias, + 'confidence': 'high' if max_similarity >= 0.8 else 'medium', + 'reason': f"与别名 '{best_alias}' 相似度 {max_similarity:.2f}" + }) + + # 基于模式匹配的建议 + for local_field, patterns in self.field_patterns.items(): + for pattern in patterns: + import re + if re.search(pattern, feishu_field.lower()): + suggestions.append({ + 'local_field': local_field, + 'similarity': 0.7, # 模式匹配给固定相似度 + 'matched_pattern': pattern, + 'confidence': 'medium', + 'reason': f"匹配模式 '{pattern}'" + }) + break + + # 按相似度和优先级排序 + suggestions.sort(key=lambda x: (x['similarity'], self.field_priorities.get(x['local_field'], 999)), reverse=True) + + return suggestions[:3] # 返回前3个建议 + + def map_field(self, feishu_field: str) -> Optional[str]: + """ + 映射飞书字段到本地字段 + + Args: + feishu_field: 飞书字段名 + + Returns: + 映射的本地字段名,如果没有映射则返回None + """ + # 1. 直接映射 + if feishu_field in self.field_mapping: + return self.field_mapping[feishu_field] + + # 2. 别名映射 + for local_field, aliases in self.field_aliases.items(): + if feishu_field in aliases: + return local_field + + # 3. 自动映射(如果启用) + if self.auto_mapping_enabled: + suggestions = self._suggest_mapping(feishu_field) + if suggestions and suggestions[0]['confidence'] == 'high': + return suggestions[0]['local_field'] + + return None + + def add_field_mapping(self, feishu_field: str, local_field: str, + aliases: List[str] = None, patterns: List[str] = None, + priority: int = 3) -> bool: + """ + 添加字段映射 + + Args: + feishu_field: 飞书字段名 + local_field: 本地字段名 + aliases: 别名列表 + patterns: 模式列表 + priority: 优先级 + + Returns: + 是否添加成功 + """ + try: + # 添加到直接映射 + self.field_mapping[feishu_field] = local_field + + # 添加别名 + if aliases: + if local_field not in self.field_aliases: + self.field_aliases[local_field] = [] + self.field_aliases[local_field].extend(aliases) + + # 添加模式 + if patterns: + if local_field not in self.field_patterns: + self.field_patterns[local_field] = [] + self.field_patterns[local_field].extend(patterns) + + # 设置优先级 + self.field_priorities[local_field] = priority + + # 保存配置 + self._save_current_config() + + logger.info(f"添加字段映射: {feishu_field} -> {local_field}") + return True + + except Exception as e: + logger.error(f"添加字段映射失败: {e}") + return False + + def remove_field_mapping(self, feishu_field: str) -> bool: + """ + 移除字段映射 + + Args: + feishu_field: 飞书字段名 + + Returns: + 是否移除成功 + """ + try: + if feishu_field in self.field_mapping: + del self.field_mapping[feishu_field] + self._save_current_config() + logger.info(f"移除字段映射: {feishu_field}") + return True + return False + except Exception as e: + logger.error(f"移除字段映射失败: {e}") + return False + + def get_mapping_status(self) -> Dict[str, Any]: + """ + 获取映射状态统计 + + Returns: + 映射状态信息 + """ + return { + 'total_mappings': len(self.field_mapping), + 'total_aliases': sum(len(aliases) for aliases in self.field_aliases.values()), + 'total_patterns': sum(len(patterns) for patterns in self.field_patterns.values()), + 'auto_mapping_enabled': self.auto_mapping_enabled, + 'similarity_threshold': self.similarity_threshold, + 'field_mapping': self.field_mapping, + 'field_aliases': self.field_aliases, + 'field_patterns': self.field_patterns, + 'field_priorities': self.field_priorities + } + + def _save_current_config(self): + """保存当前配置""" + config = { + 'field_mapping': self.field_mapping, + 'field_aliases': self.field_aliases, + 'field_patterns': self.field_patterns, + 'field_priorities': self.field_priorities, + 'auto_mapping_enabled': self.auto_mapping_enabled, + 'similarity_threshold': self.similarity_threshold + } + self._save_config(config) + + def convert_fields(self, feishu_fields: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[str, Any]]: + """ + 转换飞书字段到本地字段 + + Args: + feishu_fields: 飞书字段数据 + + Returns: + (转换后的本地字段, 转换统计信息) + """ + local_data = {} + conversion_stats = { + 'total_fields': len(feishu_fields), + 'mapped_fields': 0, + 'unmapped_fields': [], + 'mapping_details': {} + } + + logger.info(f"开始转换字段: {list(feishu_fields.keys())}") + + for feishu_field, value in feishu_fields.items(): + local_field = self.map_field(feishu_field) + + if local_field: + local_data[local_field] = value + conversion_stats['mapped_fields'] += 1 + conversion_stats['mapping_details'][feishu_field] = { + 'local_field': local_field, + 'mapped': True, + 'value': value + } + logger.info(f"映射字段 {feishu_field} -> {local_field}: {value}") + else: + conversion_stats['unmapped_fields'].append(feishu_field) + conversion_stats['mapping_details'][feishu_field] = { + 'mapped': False, + 'value': value, + 'suggestions': self._suggest_mapping(feishu_field) + } + logger.info(f"飞书字段 {feishu_field} 不存在于数据中") + + logger.info(f"字段转换完成: 已映射 {conversion_stats['mapped_fields']}, " + f"未映射 {len(conversion_stats['unmapped_fields'])}") + + return local_data, conversion_stats diff --git a/src/integrations/workorder_sync.py b/src/integrations/workorder_sync.py index 94347c3..a6c1f8d 100644 --- a/src/integrations/workorder_sync.py +++ b/src/integrations/workorder_sync.py @@ -10,6 +10,7 @@ from typing import Dict, List, Optional, Any from datetime import datetime from src.integrations.feishu_client import FeishuClient from src.integrations.ai_suggestion_service import AISuggestionService +from src.integrations.flexible_field_mapper import FlexibleFieldMapper from src.core.database import db_manager from src.core.models import WorkOrder # 工单状态和优先级枚举 @@ -44,7 +45,10 @@ class WorkOrderSyncService: self.table_id = table_id self.ai_service = AISuggestionService() - # 字段映射配置 - 根据实际飞书表格结构 + # 初始化灵活字段映射器 + self.field_mapper = FlexibleFieldMapper() + + # 保留原有的字段映射作为默认配置(向后兼容) self.field_mapping = { # 核心字段 "TR Number": "order_id", # TR编号映射到工单号 @@ -75,6 +79,9 @@ class WorkOrderSyncService: "Issue Start Time": "updated_at" # 问题开始时间作为更新时间 } + # 将原有映射添加到灵活映射器中 + self._init_flexible_mapper() + # 状态映射 - 根据飞书表格中的实际值 self.status_mapping = { "close": WorkOrderStatus.CLOSED, # 已关闭 @@ -93,6 +100,62 @@ class WorkOrderSyncService: "Urgent": WorkOrderPriority.URGENT } + def _init_flexible_mapper(self): + """初始化灵活映射器,将原有映射添加到其中""" + for feishu_field, local_field in self.field_mapping.items(): + self.field_mapper.add_field_mapping(feishu_field, local_field) + + def get_field_discovery_report(self, feishu_fields: Dict[str, Any]) -> Dict[str, Any]: + """ + 获取字段发现报告 + + Args: + feishu_fields: 飞书字段数据 + + Returns: + 字段发现报告 + """ + return self.field_mapper.discover_fields(feishu_fields) + + def add_field_mapping(self, feishu_field: str, local_field: str, + aliases: List[str] = None, patterns: List[str] = None, + priority: int = 3) -> bool: + """ + 添加字段映射 + + Args: + feishu_field: 飞书字段名 + local_field: 本地字段名 + aliases: 别名列表 + patterns: 模式列表 + priority: 优先级 + + Returns: + 是否添加成功 + """ + return self.field_mapper.add_field_mapping(feishu_field, local_field, aliases, patterns, priority) + + def remove_field_mapping(self, feishu_field: str) -> bool: + """ + 移除字段映射 + + Args: + feishu_field: 飞书字段名 + + Returns: + 是否移除成功 + """ + return self.field_mapper.remove_field_mapping(feishu_field) + + def get_mapping_status(self) -> Dict[str, Any]: + """ + 获取映射状态 + + Returns: + 映射状态信息 + """ + return self.field_mapper.get_mapping_status() + def sync_from_feishu(self, generate_ai_suggestions: bool = True, limit: int = 10) -> Dict[str, Any]: """ 从飞书同步数据到本地系统 @@ -387,37 +450,42 @@ class WorkOrderSyncService: def _convert_feishu_to_local(self, feishu_fields: Dict[str, Any]) -> Dict[str, Any]: """将飞书字段转换为本地工单字段""" - local_data = {} - logger.info(f"开始转换飞书字段: {feishu_fields}") - logger.info(f"字段映射配置: {self.field_mapping}") - for feishu_field, local_field in self.field_mapping.items(): - if feishu_field in feishu_fields: - value = feishu_fields[feishu_field] - logger.info(f"映射字段 {feishu_field} -> {local_field}: {value}") - - # 特殊字段处理 - if local_field == "status" and value in self.status_mapping: - value = self.status_mapping[value] - elif local_field == "priority" and value in self.priority_mapping: - value = self.priority_mapping[value] - elif local_field in ["created_at", "updated_at", "date_of_close"] and value: - try: - # 处理飞书时间戳(毫秒) - if isinstance(value, (int, float)): - # 飞书时间戳是毫秒,需要转换为秒 - value = datetime.fromtimestamp(value / 1000) - else: - # 处理ISO格式时间字符串 - value = datetime.fromisoformat(value.replace('Z', '+00:00')) - except Exception as e: - logger.warning(f"时间字段转换失败: {e}, 使用当前时间") - value = datetime.now() - - local_data[local_field] = value - else: - logger.info(f"飞书字段 {feishu_field} 不存在于数据中") + # 使用灵活映射器进行字段转换 + local_data, conversion_stats = self.field_mapper.convert_fields(feishu_fields) + + # 记录转换统计信息 + logger.info(f"字段转换统计: 总字段 {conversion_stats['total_fields']}, " + f"已映射 {conversion_stats['mapped_fields']}, " + f"未映射 {len(conversion_stats['unmapped_fields'])}") + + # 如果有未映射的字段,记录详细信息 + if conversion_stats['unmapped_fields']: + logger.warning(f"未映射字段: {conversion_stats['unmapped_fields']}") + for field in conversion_stats['unmapped_fields']: + suggestions = conversion_stats['mapping_details'][field].get('suggestions', []) + if suggestions: + logger.info(f"字段 '{field}' 的建议映射: {suggestions[0] if suggestions else '无'}") + + # 特殊字段处理 + for local_field, value in local_data.items(): + if local_field == "status" and value in self.status_mapping: + local_data[local_field] = self.status_mapping[value] + elif local_field == "priority" and value in self.priority_mapping: + local_data[local_field] = self.priority_mapping[value] + elif local_field in ["created_at", "updated_at", "date_of_close"] and value: + try: + # 处理飞书时间戳(毫秒) + if isinstance(value, (int, float)): + # 飞书时间戳是毫秒,需要转换为秒 + local_data[local_field] = datetime.fromtimestamp(value / 1000) + else: + # 处理ISO格式时间字符串 + local_data[local_field] = datetime.fromisoformat(value.replace('Z', '+00:00')) + except Exception as e: + logger.warning(f"时间字段转换失败: {e}, 使用当前时间") + local_data[local_field] = datetime.now() # 生成标题 - 使用TR Number和问题类型 tr_number = feishu_fields.get("TR Number", "") diff --git a/src/web/blueprints/feishu_sync.py b/src/web/blueprints/feishu_sync.py index 666448a..6794f81 100644 --- a/src/web/blueprints/feishu_sync.py +++ b/src/web/blueprints/feishu_sync.py @@ -4,7 +4,7 @@ 处理飞书多维表格与工单系统的同步 """ -from flask import Blueprint, request, jsonify +from flask import Blueprint, request, jsonify, render_template from src.integrations.feishu_client import FeishuClient from src.integrations.workorder_sync import WorkOrderSyncService from src.integrations.config_manager import config_manager @@ -203,6 +203,130 @@ def create_workorder_from_feishu(): logger.error(f"创建工单失败: {e}") return jsonify({"success": False, "message": str(e)}), 500 +@feishu_sync_bp.route('/field-mapping/status') +def get_field_mapping_status(): + """获取字段映射状态""" + try: + sync_service = get_sync_service() + status = sync_service.get_mapping_status() + + return jsonify({ + "success": True, + "status": status + }) + except Exception as e: + logger.error(f"获取字段映射状态失败: {e}") + return jsonify({"error": str(e)}), 500 + +@feishu_sync_bp.route('/field-mapping/discover', methods=['POST']) +def discover_fields(): + """发现字段并生成映射建议""" + try: + data = request.get_json() or {} + limit = data.get('limit', 5) # 默认分析5条记录 + + sync_service = get_sync_service() + + # 获取飞书记录进行分析 + feishu_client = sync_service.feishu_client + records = feishu_client.get_table_records(sync_service.app_token, sync_service.table_id, page_size=limit) + + if records.get("code") != 0: + raise Exception(f"获取飞书记录失败: {records.get('msg', '未知错误')}") + + items = records.get("data", {}).get("items", []) + if not items: + return jsonify({ + "success": True, + "message": "没有找到飞书记录", + "discovery_report": {} + }) + + # 分析第一条记录的字段 + first_record = items[0] + feishu_fields = feishu_client.parse_record_fields(first_record) + + # 生成字段发现报告 + discovery_report = sync_service.get_field_discovery_report(feishu_fields) + + return jsonify({ + "success": True, + "discovery_report": discovery_report, + "sample_record": feishu_fields + }) + + except Exception as e: + logger.error(f"字段发现失败: {e}") + return jsonify({"error": str(e)}), 500 + +@feishu_sync_bp.route('/field-mapping/add', methods=['POST']) +def add_field_mapping(): + """添加字段映射""" + try: + data = request.get_json() + feishu_field = data.get('feishu_field') + local_field = data.get('local_field') + aliases = data.get('aliases', []) + patterns = data.get('patterns', []) + priority = data.get('priority', 3) + + if not feishu_field or not local_field: + return jsonify({"error": "缺少必要参数"}), 400 + + sync_service = get_sync_service() + success = sync_service.add_field_mapping( + feishu_field=feishu_field, + local_field=local_field, + aliases=aliases, + patterns=patterns, + priority=priority + ) + + if success: + return jsonify({ + "success": True, + "message": f"字段映射 '{feishu_field}' -> '{local_field}' 添加成功" + }) + else: + return jsonify({"error": "添加字段映射失败"}), 500 + + except Exception as e: + logger.error(f"添加字段映射失败: {e}") + return jsonify({"error": str(e)}), 500 + +@feishu_sync_bp.route('/field-mapping/remove', methods=['POST']) +def remove_field_mapping(): + """移除字段映射""" + try: + data = request.get_json() + feishu_field = data.get('feishu_field') + + if not feishu_field: + return jsonify({"error": "缺少字段名参数"}), 400 + + sync_service = get_sync_service() + success = sync_service.remove_field_mapping(feishu_field) + + if success: + return jsonify({ + "success": True, + "message": f"字段映射 '{feishu_field}' 移除成功" + }) + else: + return jsonify({ + "success": False, + "message": f"字段映射 '{feishu_field}' 不存在或移除失败" + }) + + except Exception as e: + logger.error(f"移除字段映射失败: {e}") + return jsonify({"error": str(e)}), 500 + +@feishu_sync_bp.route('/field-mapping') +def field_mapping_page(): + """字段映射管理页面""" + return render_template('field_mapping.html') + @feishu_sync_bp.route('/preview-feishu-data') def preview_feishu_data(): """预览飞书数据""" diff --git a/src/web/static/js/dashboard.js b/src/web/static/js/dashboard.js index 4fd2401..39c17a0 100644 --- a/src/web/static/js/dashboard.js +++ b/src/web/static/js/dashboard.js @@ -4444,6 +4444,18 @@ class FeishuSyncManager { } } + // 打开字段映射管理页面 + openFieldMapping() { + const section = document.getElementById('fieldMappingSection'); + if (section.style.display === 'none') { + section.style.display = 'block'; + // 自动加载映射状态 + this.loadMappingStatus(); + } else { + section.style.display = 'none'; + } + } + async previewFeishuData() { try { this.showNotification('正在获取飞书数据预览...', 'info'); @@ -4653,6 +4665,242 @@ class FeishuSyncManager { } } + // 字段映射管理方法 + async discoverFields() { + try { + this.showNotification('正在发现字段...', 'info'); + + const response = await fetch('/api/feishu-sync/field-mapping/discover', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ limit: 5 }) + }); + + const data = await response.json(); + + if (data.success) { + this.displayDiscoveryResults(data.discovery_report); + this.showNotification('字段发现完成', 'success'); + } else { + this.showNotification('字段发现失败: ' + data.error, 'error'); + } + } catch (error) { + this.showNotification('字段发现失败: ' + error.message, 'error'); + } + } + + displayDiscoveryResults(report) { + const container = document.getElementById('fieldMappingContent'); + let html = ''; + + // 已映射字段 + if (report.mapped_fields && Object.keys(report.mapped_fields).length > 0) { + html += '
直接映射
+别名映射
+模式匹配
+自动映射
+