feat: 自动提交 - 周一 2025/09/22 11:24:32.93

This commit is contained in:
赵杰 Jie Zhao (雄狮汽车科技)
2025-09-22 11:24:32 +01:00
parent 059eefd961
commit 16bb98131e
10 changed files with 2399 additions and 31 deletions

View File

@@ -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

View File

@@ -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", "")

View File

@@ -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():
"""预览飞书数据"""

View File

@@ -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 += '<div class="mb-3"><h6 class="text-success"><i class="fas fa-check-circle"></i> 已映射字段</h6>';
for (const [feishuField, localField] of Object.entries(report.mapped_fields)) {
html += `<div class="alert alert-success py-2">
<strong>${feishuField}</strong> → <span class="badge bg-success">${localField}</span>
</div>`;
}
html += '</div>';
}
// 未映射字段和建议
if (report.unmapped_fields && report.unmapped_fields.length > 0) {
html += '<div class="mb-3"><h6 class="text-warning"><i class="fas fa-exclamation-triangle"></i> 未映射字段</h6>';
for (const field of report.unmapped_fields) {
html += `<div class="alert alert-warning py-2">
<strong>${field}</strong>`;
const suggestions = report.suggested_mappings[field] || [];
if (suggestions.length > 0) {
html += '<div class="mt-2"><small class="text-muted">建议映射:</small>';
suggestions.slice(0, 2).forEach(suggestion => {
html += `<div class="mt-1">
<span class="badge bg-${suggestion.confidence === 'high' ? 'success' : 'warning'}">${suggestion.local_field}</span>
<small class="text-muted">(${suggestion.reason})</small>
<button class="btn btn-sm btn-outline-primary ms-2" onclick="feishuSync.applySuggestion('${field}', '${suggestion.local_field}')">应用</button>
</div>`;
});
html += '</div>';
}
html += '</div>';
}
html += '</div>';
}
container.innerHTML = html;
}
async applySuggestion(feishuField, localField) {
if (confirm(`确定要将 "${feishuField}" 映射到 "${localField}" 吗?`)) {
try {
const response = await fetch('/api/feishu-sync/field-mapping/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
feishu_field: feishuField,
local_field: localField,
priority: 3
})
});
const data = await response.json();
if (data.success) {
this.showNotification('映射添加成功!', 'success');
this.discoverFields(); // 重新发现字段
} else {
this.showNotification('添加映射失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('请求失败: ' + error.message, 'error');
}
}
}
async loadMappingStatus() {
try {
const response = await fetch('/api/feishu-sync/field-mapping/status');
const data = await response.json();
if (data.success) {
this.displayMappingStatus(data.status);
} else {
this.showNotification('获取映射状态失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('请求失败: ' + error.message, 'error');
}
}
displayMappingStatus(status) {
const container = document.getElementById('fieldMappingContent');
let html = '';
html += `<div class="row mb-3">
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title text-primary">${status.total_mappings}</h5>
<p class="card-text">直接映射</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title text-info">${status.total_aliases}</h5>
<p class="card-text">别名映射</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title text-warning">${status.total_patterns}</h5>
<p class="card-text">模式匹配</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title ${status.auto_mapping_enabled ? 'text-success' : 'text-danger'}">
${status.auto_mapping_enabled ? '启用' : '禁用'}
</h5>
<p class="card-text">自动映射</p>
</div>
</div>
</div>
</div>`;
// 显示当前映射
if (status.field_mapping && Object.keys(status.field_mapping).length > 0) {
html += '<h6>当前字段映射:</h6><div class="row">';
for (const [feishuField, localField] of Object.entries(status.field_mapping)) {
html += `<div class="col-md-6 mb-2">
<div class="alert alert-info py-2">
<strong>${feishuField}</strong> → <span class="badge bg-primary">${localField}</span>
<button class="btn btn-sm btn-outline-danger float-end" onclick="feishuSync.removeMapping('${feishuField}')">删除</button>
</div>
</div>`;
}
html += '</div>';
}
container.innerHTML = html;
}
async removeMapping(feishuField) {
if (confirm(`确定要删除映射 "${feishuField}" 吗?`)) {
try {
const response = await fetch('/api/feishu-sync/field-mapping/remove', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
feishu_field: feishuField
})
});
const data = await response.json();
if (data.success) {
this.showNotification('映射删除成功!', 'success');
this.loadMappingStatus(); // 刷新状态
} else {
this.showNotification('删除映射失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('请求失败: ' + error.message, 'error');
}
}
}
showAddMappingModal() {
// 简单的添加映射功能
const feishuField = prompt('请输入飞书字段名:');
if (!feishuField) return;
const localField = prompt('请输入本地字段名 (如: order_id, description, category):');
if (!localField) return;
this.addFieldMapping(feishuField, localField);
}
async addFieldMapping(feishuField, localField) {
try {
const response = await fetch('/api/feishu-sync/field-mapping/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
feishu_field: feishuField,
local_field: localField,
priority: 3
})
});
const data = await response.json();
if (data.success) {
this.showNotification('映射添加成功!', 'success');
this.loadMappingStatus(); // 刷新状态
} else {
this.showNotification('添加映射失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('请求失败: ' + error.message, 'error');
}
}
showNotification(message, type = 'info') {
const container = document.getElementById('notificationContainer');
const alert = document.createElement('div');

View File

@@ -1145,11 +1145,45 @@
<button class="btn btn-info" onclick="feishuSync.previewFeishuData()">
<i class="fas fa-eye me-1"></i>预览飞书数据
</button>
<button class="btn btn-warning" onclick="openFieldMapping()">
<i class="fas fa-exchange-alt me-1"></i>字段映射管理
</button>
<button class="btn btn-secondary" onclick="feishuSync.refreshStatus()">
<i class="fas fa-refresh me-1"></i>刷新状态
</button>
</div>
<!-- 字段映射管理区域 -->
<div class="row mb-4" id="fieldMappingSection" style="display: none;">
<div class="col-12">
<div class="card">
<div class="card-header">
<h6 class="card-title mb-0">
<i class="fas fa-exchange-alt me-2"></i>字段映射管理
</h6>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<button class="btn btn-primary btn-sm" onclick="feishuSync.discoverFields()">
<i class="fas fa-search"></i> 发现字段
</button>
<button class="btn btn-info btn-sm" onclick="feishuSync.loadMappingStatus()">
<i class="fas fa-sync"></i> 刷新状态
</button>
</div>
<div class="col-md-6 text-end">
<button class="btn btn-success btn-sm" onclick="feishuSync.showAddMappingModal()">
<i class="fas fa-plus"></i> 添加映射
</button>
</div>
</div>
<div id="fieldMappingContent"></div>
</div>
</div>
</div>
</div>
<div class="mb-3">
<label for="syncLimit" class="form-label">同步数量限制:</label>
<select class="form-select" id="syncLimit" style="width: auto; display: inline-block;">

View File

@@ -0,0 +1,475 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>字段映射管理 - TSP智能助手</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="{{ url_for('static', filename='css/dashboard.css') }}" rel="stylesheet">
<style>
.field-mapping-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.field-mapping-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
border-radius: 8px 8px 0 0;
}
.mapping-item {
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 15px;
margin-bottom: 10px;
background: #f8f9fa;
}
.suggestion-item {
border: 1px solid #d1ecf1;
border-radius: 6px;
padding: 12px;
margin-bottom: 8px;
background: #d1ecf1;
}
.confidence-high {
border-color: #28a745;
background: #d4edda;
}
.confidence-medium {
border-color: #ffc107;
background: #fff3cd;
}
.unmapped-field {
border-color: #dc3545;
background: #f8d7da;
}
.status-badge {
font-size: 0.8em;
padding: 4px 8px;
}
.loading {
display: none;
}
.loading.show {
display: block;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="field-mapping-card">
<div class="field-mapping-header">
<div class="d-flex justify-content-between align-items-center">
<div>
<h4><i class="fas fa-exchange-alt"></i> 字段映射管理</h4>
<p class="mb-0">管理飞书字段与本地工单字段的映射关系</p>
</div>
<div>
<button class="btn btn-light btn-sm" onclick="window.close()">
<i class="fas fa-times"></i> 关闭
</button>
</div>
</div>
</div>
<div class="card-body">
<!-- 操作按钮 -->
<div class="row mb-4">
<div class="col-md-6">
<button class="btn btn-primary" onclick="discoverFields()">
<i class="fas fa-search"></i> 发现字段
</button>
<button class="btn btn-info" onclick="loadMappingStatus()">
<i class="fas fa-sync"></i> 刷新状态
</button>
</div>
<div class="col-md-6 text-end">
<button class="btn btn-success" onclick="showAddMappingModal()">
<i class="fas fa-plus"></i> 添加映射
</button>
</div>
</div>
<!-- 加载状态 -->
<div class="loading text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p>正在处理中...</p>
</div>
<!-- 字段发现结果 -->
<div id="discovery-results" style="display: none;">
<h5><i class="fas fa-lightbulb"></i> 字段发现结果</h5>
<div id="discovery-content"></div>
</div>
<!-- 映射状态 -->
<div id="mapping-status" style="display: none;">
<h5><i class="fas fa-list"></i> 当前映射状态</h5>
<div id="mapping-content"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 添加映射模态框 -->
<div class="modal fade" id="addMappingModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加字段映射</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addMappingForm">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">飞书字段名</label>
<input type="text" class="form-control" id="feishuField" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">本地字段名</label>
<select class="form-select" id="localField" required>
<option value="">请选择本地字段</option>
<option value="order_id">工单号 (order_id)</option>
<option value="description">描述 (description)</option>
<option value="category">分类 (category)</option>
<option value="priority">优先级 (priority)</option>
<option value="status">状态 (status)</option>
<option value="source">来源 (source)</option>
<option value="created_at">创建时间 (created_at)</option>
<option value="solution">解决方案 (solution)</option>
<option value="resolution">解决结果 (resolution)</option>
<option value="created_by">创建人 (created_by)</option>
<option value="vehicle_type">车型 (vehicle_type)</option>
<option value="vin_sim">车架号 (vin_sim)</option>
<option value="module">模块 (module)</option>
<option value="wilfulness">责任人 (wilfulness)</option>
<option value="date_of_close">关闭日期 (date_of_close)</option>
<option value="app_remote_control_version">应用版本 (app_remote_control_version)</option>
<option value="hmi_sw">HMI软件 (hmi_sw)</option>
<option value="parent_record">父记录 (parent_record)</option>
<option value="has_updated_same_day">同日更新 (has_updated_same_day)</option>
<option value="operating_time">操作时间 (operating_time)</option>
<option value="ai_suggestion">AI建议 (ai_suggestion)</option>
<option value="updated_at">更新时间 (updated_at)</option>
</select>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">别名 (可选,用逗号分隔)</label>
<input type="text" class="form-control" id="aliases" placeholder="例如: 工单号,订单号,Ticket ID">
</div>
<div class="mb-3">
<label class="form-label">匹配模式 (可选,用逗号分隔)</label>
<input type="text" class="form-control" id="patterns" placeholder="例如: .*number.*,.*id.*">
</div>
<div class="mb-3">
<label class="form-label">优先级</label>
<select class="form-select" id="priority">
<option value="1">高 (1)</option>
<option value="2">中 (2)</option>
<option value="3" selected>低 (3)</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="addFieldMapping()">添加映射</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 发现字段
async function discoverFields() {
showLoading(true);
try {
const response = await fetch('/api/feishu-sync/field-mapping/discover', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ limit: 5 })
});
const result = await response.json();
if (result.success) {
displayDiscoveryResults(result.discovery_report);
} else {
alert('字段发现失败: ' + result.error);
}
} catch (error) {
alert('请求失败: ' + error.message);
} finally {
showLoading(false);
}
}
// 显示发现结果
function displayDiscoveryResults(report) {
const container = document.getElementById('discovery-content');
let html = '';
// 已映射字段
if (report.mapped_fields && Object.keys(report.mapped_fields).length > 0) {
html += '<div class="mb-4"><h6 class="text-success"><i class="fas fa-check-circle"></i> 已映射字段</h6>';
for (const [feishuField, localField] of Object.entries(report.mapped_fields)) {
html += `<div class="mapping-item">
<strong>${feishuField}</strong> → <span class="badge bg-success">${localField}</span>
</div>`;
}
html += '</div>';
}
// 未映射字段和建议
if (report.unmapped_fields && report.unmapped_fields.length > 0) {
html += '<div class="mb-4"><h6 class="text-warning"><i class="fas fa-exclamation-triangle"></i> 未映射字段</h6>';
for (const field of report.unmapped_fields) {
html += `<div class="unmapped-field">
<strong>${field}</strong>
<div class="mt-2">`;
const suggestions = report.suggested_mappings[field] || [];
if (suggestions.length > 0) {
html += '<small class="text-muted">建议映射:</small>';
suggestions.forEach(suggestion => {
const confidenceClass = suggestion.confidence === 'high' ? 'confidence-high' : 'confidence-medium';
html += `<div class="suggestion-item ${confidenceClass}">
<strong>${suggestion.local_field}</strong>
<span class="badge status-badge bg-${suggestion.confidence === 'high' ? 'success' : 'warning'}">${suggestion.confidence}</span>
<br><small>${suggestion.reason}</small>
<button class="btn btn-sm btn-outline-primary float-end" onclick="applySuggestion('${field}', '${suggestion.local_field}')">应用</button>
</div>`;
});
} else {
html += '<small class="text-muted">暂无建议映射</small>';
}
html += '</div></div>';
}
html += '</div>';
}
container.innerHTML = html;
document.getElementById('discovery-results').style.display = 'block';
}
// 应用建议映射
async function applySuggestion(feishuField, localField) {
if (confirm(`确定要将 "${feishuField}" 映射到 "${localField}" 吗?`)) {
try {
const response = await fetch('/api/feishu-sync/field-mapping/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
feishu_field: feishuField,
local_field: localField,
priority: 3
})
});
const result = await response.json();
if (result.success) {
alert('映射添加成功!');
discoverFields(); // 重新发现字段
} else {
alert('添加映射失败: ' + result.error);
}
} catch (error) {
alert('请求失败: ' + error.message);
}
}
}
// 加载映射状态
async function loadMappingStatus() {
showLoading(true);
try {
const response = await fetch('/api/feishu-sync/field-mapping/status');
const result = await response.json();
if (result.success) {
displayMappingStatus(result.status);
} else {
alert('获取映射状态失败: ' + result.error);
}
} catch (error) {
alert('请求失败: ' + error.message);
} finally {
showLoading(false);
}
}
// 显示映射状态
function displayMappingStatus(status) {
const container = document.getElementById('mapping-content');
let html = '';
html += `<div class="row mb-3">
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title text-primary">${status.total_mappings}</h5>
<p class="card-text">直接映射</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title text-info">${status.total_aliases}</h5>
<p class="card-text">别名映射</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title text-warning">${status.total_patterns}</h5>
<p class="card-text">模式匹配</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title ${status.auto_mapping_enabled ? 'text-success' : 'text-danger'}">
${status.auto_mapping_enabled ? '启用' : '禁用'}
</h5>
<p class="card-text">自动映射</p>
</div>
</div>
</div>
</div>`;
// 显示当前映射
if (status.field_mapping && Object.keys(status.field_mapping).length > 0) {
html += '<h6>当前字段映射:</h6><div class="row">';
for (const [feishuField, localField] of Object.entries(status.field_mapping)) {
html += `<div class="col-md-6 mb-2">
<div class="mapping-item">
<strong>${feishuField}</strong> → <span class="badge bg-primary">${localField}</span>
<button class="btn btn-sm btn-outline-danger float-end" onclick="removeMapping('${feishuField}')">删除</button>
</div>
</div>`;
}
html += '</div>';
}
container.innerHTML = html;
document.getElementById('mapping-status').style.display = 'block';
}
// 显示添加映射模态框
function showAddMappingModal() {
const modal = new bootstrap.Modal(document.getElementById('addMappingModal'));
modal.show();
}
// 添加字段映射
async function addFieldMapping() {
const feishuField = document.getElementById('feishuField').value;
const localField = document.getElementById('localField').value;
const aliases = document.getElementById('aliases').value.split(',').map(s => s.trim()).filter(s => s);
const patterns = document.getElementById('patterns').value.split(',').map(s => s.trim()).filter(s => s);
const priority = parseInt(document.getElementById('priority').value);
if (!feishuField || !localField) {
alert('请填写飞书字段名和本地字段名');
return;
}
try {
const response = await fetch('/api/feishu-sync/field-mapping/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
feishu_field: feishuField,
local_field: localField,
aliases: aliases,
patterns: patterns,
priority: priority
})
});
const result = await response.json();
if (result.success) {
alert('映射添加成功!');
bootstrap.Modal.getInstance(document.getElementById('addMappingModal')).hide();
document.getElementById('addMappingForm').reset();
loadMappingStatus(); // 刷新状态
} else {
alert('添加映射失败: ' + result.error);
}
} catch (error) {
alert('请求失败: ' + error.message);
}
}
// 删除映射
async function removeMapping(feishuField) {
if (confirm(`确定要删除映射 "${feishuField}" 吗?`)) {
try {
const response = await fetch('/api/feishu-sync/field-mapping/remove', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
feishu_field: feishuField
})
});
const result = await response.json();
if (result.success) {
alert('映射删除成功!');
loadMappingStatus(); // 刷新状态
} else {
alert('删除映射失败: ' + result.error);
}
} catch (error) {
alert('请求失败: ' + error.message);
}
}
}
// 显示/隐藏加载状态
function showLoading(show) {
const loading = document.querySelector('.loading');
if (show) {
loading.classList.add('show');
} else {
loading.classList.remove('show');
}
}
// 页面加载时自动加载映射状态
document.addEventListener('DOMContentLoaded', function() {
loadMappingStatus();
});
</script>
</body>
</html>