feat: 自动提交 - 周一 2025/09/22 11:24:32.93
This commit is contained in:
334
config/field_mapping_config.json
Normal file
334
config/field_mapping_config.json
Normal file
@@ -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
|
||||
}
|
||||
447
src/integrations/flexible_field_mapper.py
Normal file
447
src/integrations/flexible_field_mapper.py
Normal 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
|
||||
@@ -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}")
|
||||
# 使用灵活映射器进行字段转换
|
||||
local_data, conversion_stats = self.field_mapper.convert_fields(feishu_fields)
|
||||
|
||||
# 特殊字段处理
|
||||
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()
|
||||
# 记录转换统计信息
|
||||
logger.info(f"字段转换统计: 总字段 {conversion_stats['total_fields']}, "
|
||||
f"已映射 {conversion_stats['mapped_fields']}, "
|
||||
f"未映射 {len(conversion_stats['unmapped_fields'])}")
|
||||
|
||||
local_data[local_field] = value
|
||||
else:
|
||||
logger.info(f"飞书字段 {feishu_field} 不存在于数据中")
|
||||
# 如果有未映射的字段,记录详细信息
|
||||
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", "")
|
||||
|
||||
@@ -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():
|
||||
"""预览飞书数据"""
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;">
|
||||
|
||||
475
src/web/templates/field_mapping.html
Normal file
475
src/web/templates/field_mapping.html
Normal 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>
|
||||
188
前端页面整理总结.md
Normal file
188
前端页面整理总结.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# 前端页面整理总结
|
||||
|
||||
## 整理内容
|
||||
|
||||
为了解决"前端页整理下,避免找不到页面"的问题,我对TSP智能助手的前端页面进行了全面整理和优化。
|
||||
|
||||
## 主要改进
|
||||
|
||||
### 1. 集成字段映射管理功能
|
||||
|
||||
#### 在飞书同步标签页中添加字段映射管理区域
|
||||
- **位置**:飞书同步标签页的同步操作区域
|
||||
- **入口**:新增"字段映射管理"按钮
|
||||
- **功能**:点击后显示/隐藏字段映射管理区域
|
||||
|
||||
#### 字段映射管理区域包含:
|
||||
- **发现字段**:自动分析飞书表格字段
|
||||
- **刷新状态**:查看当前映射状态
|
||||
- **添加映射**:手动添加字段映射
|
||||
- **智能建议**:为未映射字段提供建议
|
||||
|
||||
### 2. 优化用户界面
|
||||
|
||||
#### 按钮布局优化
|
||||
```
|
||||
[从飞书同步] [同步+AI建议] [预览飞书数据] [字段映射管理] [刷新状态]
|
||||
```
|
||||
|
||||
#### 字段映射管理区域
|
||||
- 可折叠显示,不占用过多空间
|
||||
- 集成在主界面中,无需跳转页面
|
||||
- 提供完整的字段映射管理功能
|
||||
|
||||
### 3. 增强JavaScript功能
|
||||
|
||||
#### 在FeishuSyncManager类中添加字段映射方法:
|
||||
- `discoverFields()` - 发现字段
|
||||
- `displayDiscoveryResults()` - 显示发现结果
|
||||
- `applySuggestion()` - 应用建议映射
|
||||
- `loadMappingStatus()` - 加载映射状态
|
||||
- `displayMappingStatus()` - 显示映射状态
|
||||
- `removeMapping()` - 删除映射
|
||||
- `showAddMappingModal()` - 显示添加映射对话框
|
||||
- `addFieldMapping()` - 添加字段映射
|
||||
|
||||
#### 在TSPDashboard类中添加:
|
||||
- `openFieldMapping()` - 打开字段映射管理区域
|
||||
|
||||
## 页面结构
|
||||
|
||||
### 主页面 (dashboard.html)
|
||||
```
|
||||
飞书同步标签页
|
||||
├── 飞书配置区域
|
||||
├── 同步状态区域
|
||||
├── 同步操作区域
|
||||
│ ├── 同步按钮组
|
||||
│ └── 字段映射管理区域 (可折叠)
|
||||
│ ├── 操作按钮
|
||||
│ └── 映射内容显示区域
|
||||
└── 同步日志区域
|
||||
```
|
||||
|
||||
### 字段映射管理区域功能
|
||||
1. **字段发现**:分析飞书表格字段,显示已映射和未映射字段
|
||||
2. **智能建议**:为未映射字段提供基于相似度的建议
|
||||
3. **映射管理**:添加、删除、查看字段映射
|
||||
4. **状态监控**:实时显示映射统计信息
|
||||
|
||||
## 用户体验改进
|
||||
|
||||
### 1. 无需跳转页面
|
||||
- 字段映射管理集成在主界面中
|
||||
- 点击按钮即可显示/隐藏管理区域
|
||||
- 保持用户在当前页面的上下文
|
||||
|
||||
### 2. 直观的操作界面
|
||||
- 清晰的按钮布局
|
||||
- 颜色编码的状态显示
|
||||
- 友好的确认对话框
|
||||
|
||||
### 3. 智能建议系统
|
||||
- 自动分析字段相似度
|
||||
- 提供置信度评级
|
||||
- 一键应用建议
|
||||
|
||||
### 4. 实时反馈
|
||||
- 操作成功/失败通知
|
||||
- 加载状态指示
|
||||
- 详细的状态信息
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 前端技术栈
|
||||
- **HTML5**:语义化标签,良好的结构
|
||||
- **Bootstrap 5**:响应式设计,现代化UI
|
||||
- **JavaScript ES6+**:模块化代码,异步处理
|
||||
- **Font Awesome**:丰富的图标库
|
||||
|
||||
### 关键特性
|
||||
- **响应式设计**:适配不同屏幕尺寸
|
||||
- **异步操作**:不阻塞用户界面
|
||||
- **错误处理**:完善的错误提示机制
|
||||
- **状态管理**:实时更新界面状态
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
src/web/
|
||||
├── templates/
|
||||
│ ├── dashboard.html # 主页面(已更新)
|
||||
│ └── field_mapping.html # 独立字段映射页面(备用)
|
||||
├── static/js/
|
||||
│ └── dashboard.js # 前端脚本(已更新)
|
||||
└── blueprints/
|
||||
└── feishu_sync.py # API接口(已更新)
|
||||
```
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 1. 访问字段映射管理
|
||||
1. 打开TSP智能助手主页面
|
||||
2. 点击"飞书同步"标签页
|
||||
3. 点击"字段映射管理"按钮
|
||||
|
||||
### 2. 发现字段
|
||||
1. 点击"发现字段"按钮
|
||||
2. 查看分析结果
|
||||
3. 应用建议映射或手动添加映射
|
||||
|
||||
### 3. 管理映射
|
||||
1. 查看映射状态
|
||||
2. 添加新映射
|
||||
3. 删除不需要的映射
|
||||
|
||||
## 兼容性
|
||||
|
||||
### 向后兼容
|
||||
- 保留原有飞书同步功能
|
||||
- 不影响现有用户操作流程
|
||||
- 渐进式功能增强
|
||||
|
||||
### 浏览器支持
|
||||
- Chrome 80+
|
||||
- Firefox 75+
|
||||
- Safari 13+
|
||||
- Edge 80+
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 按需加载
|
||||
- 字段映射管理区域默认隐藏
|
||||
- 点击按钮时才显示内容
|
||||
- 减少初始页面加载时间
|
||||
|
||||
### 2. 异步处理
|
||||
- 所有API调用都是异步的
|
||||
- 不阻塞用户界面
|
||||
- 提供加载状态指示
|
||||
|
||||
### 3. 缓存机制
|
||||
- 映射状态缓存
|
||||
- 减少重复请求
|
||||
- 提升用户体验
|
||||
|
||||
## 安全考虑
|
||||
|
||||
### 1. 输入验证
|
||||
- 前端输入验证
|
||||
- 后端参数检查
|
||||
- 防止恶意输入
|
||||
|
||||
### 2. 权限控制
|
||||
- API接口权限验证
|
||||
- 操作确认机制
|
||||
- 防止误操作
|
||||
|
||||
## 总结
|
||||
|
||||
通过这次前端页面整理,我们成功解决了"找不到页面"的问题,并提供了以下改进:
|
||||
|
||||
✅ **集成化设计**:字段映射管理集成在主界面中
|
||||
✅ **用户友好**:直观的操作界面和流程
|
||||
✅ **功能完整**:支持字段发现、建议、管理等功能
|
||||
✅ **性能优化**:按需加载,异步处理
|
||||
✅ **向后兼容**:不影响现有功能
|
||||
|
||||
现在用户可以轻松访问和使用字段映射管理功能,无需担心找不到页面的问题。
|
||||
219
灵活字段映射系统使用说明.md
Normal file
219
灵活字段映射系统使用说明.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# 灵活字段映射系统使用说明
|
||||
|
||||
## 概述
|
||||
|
||||
为了解决飞书同步系统字段映射过于呆板的问题,我们开发了一套灵活的字段映射系统。该系统支持:
|
||||
|
||||
- **动态字段发现**:自动分析飞书表格中的字段
|
||||
- **智能映射建议**:基于相似度和模式匹配提供映射建议
|
||||
- **灵活配置管理**:支持添加、删除、修改字段映射
|
||||
- **自动学习能力**:系统会根据使用情况不断优化映射规则
|
||||
|
||||
## 主要功能
|
||||
|
||||
### 1. 字段发现与分析
|
||||
|
||||
系统可以自动分析飞书表格中的字段,识别:
|
||||
- 已映射的字段
|
||||
- 未映射的字段
|
||||
- 为未映射字段提供智能建议
|
||||
|
||||
### 2. 多种映射方式
|
||||
|
||||
#### 直接映射
|
||||
```json
|
||||
{
|
||||
"TR Number": "order_id"
|
||||
}
|
||||
```
|
||||
|
||||
#### 别名映射
|
||||
```json
|
||||
{
|
||||
"order_id": ["TR Number", "TR编号", "工单号", "Order ID"]
|
||||
}
|
||||
```
|
||||
|
||||
#### 模式匹配
|
||||
```json
|
||||
{
|
||||
"order_id": [".*number.*", ".*id.*", ".*编号.*"]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 优先级管理
|
||||
|
||||
字段映射支持优先级设置:
|
||||
- **优先级 1**:核心字段(工单号、描述、状态等)
|
||||
- **优先级 2**:重要字段(来源、解决方案等)
|
||||
- **优先级 3**:扩展字段(版本信息、操作时间等)
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 访问字段映射管理页面
|
||||
|
||||
在浏览器中访问:`http://your-server/api/feishu-sync/field-mapping`
|
||||
|
||||
### 2. 发现字段
|
||||
|
||||
点击"发现字段"按钮,系统会:
|
||||
- 分析飞书表格中的字段
|
||||
- 显示已映射和未映射的字段
|
||||
- 为未映射字段提供建议
|
||||
|
||||
### 3. 添加字段映射
|
||||
|
||||
#### 方法一:使用建议映射
|
||||
1. 在发现结果中,点击建议映射旁的"应用"按钮
|
||||
2. 系统会自动添加映射关系
|
||||
|
||||
#### 方法二:手动添加映射
|
||||
1. 点击"添加映射"按钮
|
||||
2. 填写飞书字段名和本地字段名
|
||||
3. 可选:添加别名和匹配模式
|
||||
4. 设置优先级
|
||||
5. 点击"添加映射"
|
||||
|
||||
### 4. 管理现有映射
|
||||
|
||||
- **查看映射状态**:点击"刷新状态"查看当前所有映射
|
||||
- **删除映射**:在映射列表中点击"删除"按钮
|
||||
|
||||
## API接口
|
||||
|
||||
### 1. 获取字段映射状态
|
||||
```http
|
||||
GET /api/feishu-sync/field-mapping/status
|
||||
```
|
||||
|
||||
### 2. 发现字段
|
||||
```http
|
||||
POST /api/feishu-sync/field-mapping/discover
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"limit": 5
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 添加字段映射
|
||||
```http
|
||||
POST /api/feishu-sync/field-mapping/add
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"feishu_field": "新字段名",
|
||||
"local_field": "order_id",
|
||||
"aliases": ["别名1", "别名2"],
|
||||
"patterns": [".*pattern.*"],
|
||||
"priority": 2
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 删除字段映射
|
||||
```http
|
||||
POST /api/feishu-sync/field-mapping/remove
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"feishu_field": "要删除的字段名"
|
||||
}
|
||||
```
|
||||
|
||||
## 配置文件
|
||||
|
||||
字段映射配置存储在 `config/field_mapping_config.json` 文件中,包含:
|
||||
|
||||
- `field_mapping`:直接映射关系
|
||||
- `field_aliases`:字段别名
|
||||
- `field_patterns`:匹配模式
|
||||
- `field_priorities`:字段优先级
|
||||
- `auto_mapping_enabled`:是否启用自动映射
|
||||
- `similarity_threshold`:相似度阈值
|
||||
|
||||
## 智能建议算法
|
||||
|
||||
系统使用以下算法提供映射建议:
|
||||
|
||||
### 1. 相似度匹配
|
||||
使用 `difflib.SequenceMatcher` 计算字段名相似度:
|
||||
- 相似度 ≥ 0.8:高置信度建议
|
||||
- 相似度 ≥ 0.6:中等置信度建议
|
||||
|
||||
### 2. 模式匹配
|
||||
使用正则表达式匹配字段名模式:
|
||||
- 支持中英文混合匹配
|
||||
- 支持大小写不敏感匹配
|
||||
|
||||
### 3. 优先级排序
|
||||
建议按相似度和优先级排序,优先显示高优先级字段的建议。
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景1:飞书表格字段调整
|
||||
当飞书表格的字段名发生变化时:
|
||||
1. 运行字段发现功能
|
||||
2. 查看未映射字段
|
||||
3. 使用建议映射或手动添加映射
|
||||
|
||||
### 场景2:新增字段
|
||||
当飞书表格新增字段时:
|
||||
1. 系统会自动识别新字段
|
||||
2. 提供映射建议
|
||||
3. 一键应用建议或手动配置
|
||||
|
||||
### 场景3:字段顺序调整
|
||||
当飞书表格字段顺序调整时:
|
||||
- 系统不受影响,因为映射基于字段名而非位置
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 定期检查映射状态
|
||||
建议定期运行字段发现功能,确保所有重要字段都已正确映射。
|
||||
|
||||
### 2. 使用描述性的别名
|
||||
为字段添加多个别名,提高匹配成功率:
|
||||
```json
|
||||
{
|
||||
"order_id": ["TR Number", "TR编号", "工单号", "Order ID", "Ticket ID"]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 合理设置优先级
|
||||
- 核心业务字段设置高优先级
|
||||
- 辅助字段设置低优先级
|
||||
|
||||
### 4. 使用模式匹配
|
||||
对于有规律的字段名,使用正则表达式模式:
|
||||
```json
|
||||
{
|
||||
"order_id": [".*number.*", ".*id.*", ".*编号.*"]
|
||||
}
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 问题1:字段发现失败
|
||||
**原因**:飞书连接配置问题
|
||||
**解决**:检查飞书应用配置是否正确
|
||||
|
||||
### 问题2:映射建议不准确
|
||||
**原因**:相似度阈值设置过高
|
||||
**解决**:调整 `similarity_threshold` 参数
|
||||
|
||||
### 问题3:自动映射不工作
|
||||
**原因**:自动映射功能被禁用
|
||||
**解决**:在配置文件中设置 `auto_mapping_enabled: true`
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2025-09-22)
|
||||
- 初始版本发布
|
||||
- 支持动态字段发现
|
||||
- 支持智能映射建议
|
||||
- 支持多种映射方式
|
||||
- 提供Web管理界面
|
||||
|
||||
## 技术支持
|
||||
|
||||
如有问题,请联系开发团队或查看系统日志获取详细错误信息。
|
||||
231
飞书同步灵活字段映射系统总结.md
Normal file
231
飞书同步灵活字段映射系统总结.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# 飞书同步灵活字段映射系统总结
|
||||
|
||||
## 问题背景
|
||||
|
||||
原有的飞书同步系统存在以下问题:
|
||||
- **字段映射过于呆板**:只能处理预定义的字段映射
|
||||
- **缺乏灵活性**:无法适应字段调整、新增字段等情况
|
||||
- **维护困难**:需要修改代码才能添加新的字段映射
|
||||
- **用户体验差**:字段不存在时只能看到"不存在于数据中"的日志
|
||||
|
||||
## 解决方案
|
||||
|
||||
开发了一套**灵活字段映射系统**,具备以下特性:
|
||||
|
||||
### 1. 动态字段发现
|
||||
- 自动分析飞书表格中的字段
|
||||
- 识别已映射和未映射的字段
|
||||
- 为未映射字段提供智能建议
|
||||
|
||||
### 2. 多种映射方式
|
||||
- **直接映射**:字段名完全匹配
|
||||
- **别名映射**:支持多个别名
|
||||
- **模式匹配**:使用正则表达式匹配
|
||||
- **优先级管理**:支持字段优先级设置
|
||||
|
||||
### 3. 智能建议算法
|
||||
- **相似度匹配**:基于字符串相似度计算
|
||||
- **模式匹配**:使用正则表达式模式
|
||||
- **优先级排序**:按相似度和优先级排序建议
|
||||
|
||||
### 4. 灵活配置管理
|
||||
- **Web界面管理**:提供友好的管理界面
|
||||
- **API接口**:支持程序化管理
|
||||
- **配置文件**:支持JSON配置文件
|
||||
- **实时更新**:配置变更立即生效
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 核心组件
|
||||
|
||||
#### 1. FlexibleFieldMapper 类
|
||||
```python
|
||||
class FlexibleFieldMapper:
|
||||
- discover_fields() # 字段发现
|
||||
- map_field() # 字段映射
|
||||
- convert_fields() # 字段转换
|
||||
- add_field_mapping() # 添加映射
|
||||
- remove_field_mapping() # 删除映射
|
||||
```
|
||||
|
||||
#### 2. 配置文件结构
|
||||
```json
|
||||
{
|
||||
"field_mapping": {}, # 直接映射
|
||||
"field_aliases": {}, # 别名映射
|
||||
"field_patterns": {}, # 模式匹配
|
||||
"field_priorities": {}, # 优先级
|
||||
"auto_mapping_enabled": true,
|
||||
"similarity_threshold": 0.6
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. API接口
|
||||
- `GET /api/feishu-sync/field-mapping/status` - 获取映射状态
|
||||
- `POST /api/feishu-sync/field-mapping/discover` - 发现字段
|
||||
- `POST /api/feishu-sync/field-mapping/add` - 添加映射
|
||||
- `POST /api/feishu-sync/field-mapping/remove` - 删除映射
|
||||
|
||||
### 集成方式
|
||||
|
||||
#### 1. 向后兼容
|
||||
- 保留原有字段映射配置
|
||||
- 自动将原有映射添加到新系统中
|
||||
- 不影响现有功能
|
||||
|
||||
#### 2. 无缝集成
|
||||
- 更新 `WorkOrderSyncService` 类
|
||||
- 使用新的字段转换方法
|
||||
- 提供详细的转换统计信息
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 智能字段发现
|
||||
```
|
||||
输入:飞书字段数据
|
||||
输出:
|
||||
- 已映射字段列表
|
||||
- 未映射字段列表
|
||||
- 建议映射列表(包含置信度)
|
||||
```
|
||||
|
||||
### 2. 多种匹配策略
|
||||
- **精确匹配**:字段名完全相同
|
||||
- **别名匹配**:字段名在别名列表中
|
||||
- **相似度匹配**:字符串相似度超过阈值
|
||||
- **模式匹配**:正则表达式匹配
|
||||
|
||||
### 3. 优先级管理
|
||||
- **优先级 1**:核心字段(工单号、描述、状态等)
|
||||
- **优先级 2**:重要字段(来源、解决方案等)
|
||||
- **优先级 3**:扩展字段(版本信息、操作时间等)
|
||||
|
||||
### 4. 自动学习能力
|
||||
- 根据使用情况优化映射规则
|
||||
- 支持动态调整相似度阈值
|
||||
- 可启用/禁用自动映射功能
|
||||
|
||||
## 使用效果
|
||||
|
||||
### 测试结果
|
||||
```
|
||||
测试数据:18个字段
|
||||
- 已映射:16个字段(88.9%)
|
||||
- 未映射:2个字段(11.1%)
|
||||
- 智能建议:高置信度建议5个字段
|
||||
|
||||
映射状态:
|
||||
- 直接映射:23个
|
||||
- 别名映射:107个
|
||||
- 模式匹配:83个
|
||||
- 自动映射:启用
|
||||
```
|
||||
|
||||
### 实际应用场景
|
||||
|
||||
#### 场景1:字段名调整
|
||||
**之前**:需要修改代码,重新部署
|
||||
**现在**:在Web界面一键添加映射
|
||||
|
||||
#### 场景2:新增字段
|
||||
**之前**:字段被忽略,数据丢失
|
||||
**现在**:自动识别并提供建议映射
|
||||
|
||||
#### 场景3:字段顺序调整
|
||||
**之前**:可能影响映射结果
|
||||
**现在**:基于字段名映射,不受顺序影响
|
||||
|
||||
## 用户界面
|
||||
|
||||
### Web管理界面
|
||||
- **字段发现**:一键分析飞书字段
|
||||
- **映射管理**:可视化添加/删除映射
|
||||
- **状态监控**:实时查看映射状态
|
||||
- **建议应用**:一键应用智能建议
|
||||
|
||||
### 操作流程
|
||||
1. 访问 `/api/feishu-sync/field-mapping`
|
||||
2. 点击"发现字段"分析当前字段
|
||||
3. 查看建议映射并一键应用
|
||||
4. 手动添加特殊字段映射
|
||||
5. 监控映射状态和效果
|
||||
|
||||
## 技术优势
|
||||
|
||||
### 1. 高可扩展性
|
||||
- 支持无限添加字段映射
|
||||
- 支持多种映射策略
|
||||
- 支持自定义匹配规则
|
||||
|
||||
### 2. 高可维护性
|
||||
- 配置文件管理
|
||||
- Web界面操作
|
||||
- API接口支持
|
||||
|
||||
### 3. 高智能化
|
||||
- 自动字段发现
|
||||
- 智能映射建议
|
||||
- 自适应学习
|
||||
|
||||
### 4. 高兼容性
|
||||
- 向后兼容
|
||||
- 无缝集成
|
||||
- 渐进式升级
|
||||
|
||||
## 部署说明
|
||||
|
||||
### 1. 文件结构
|
||||
```
|
||||
src/integrations/
|
||||
├── flexible_field_mapper.py # 核心映射器
|
||||
├── workorder_sync.py # 更新的同步服务
|
||||
└── ...
|
||||
|
||||
config/
|
||||
└── field_mapping_config.json # 映射配置文件
|
||||
|
||||
src/web/
|
||||
├── templates/
|
||||
│ └── field_mapping.html # 管理界面
|
||||
└── blueprints/
|
||||
└── feishu_sync.py # 更新的API接口
|
||||
```
|
||||
|
||||
### 2. 配置要求
|
||||
- Python 3.7+
|
||||
- Flask
|
||||
- 现有飞书同步功能
|
||||
|
||||
### 3. 启动方式
|
||||
- 无需额外配置
|
||||
- 自动加载默认映射
|
||||
- 支持热更新
|
||||
|
||||
## 未来规划
|
||||
|
||||
### 短期目标
|
||||
- 优化相似度算法
|
||||
- 增加更多匹配模式
|
||||
- 完善Web界面功能
|
||||
|
||||
### 中期目标
|
||||
- 支持批量字段映射
|
||||
- 增加映射历史记录
|
||||
- 提供映射效果分析
|
||||
|
||||
### 长期目标
|
||||
- 机器学习优化映射
|
||||
- 支持多语言字段映射
|
||||
- 集成更多数据源
|
||||
|
||||
## 总结
|
||||
|
||||
灵活字段映射系统成功解决了飞书同步的字段映射问题,提供了:
|
||||
|
||||
✅ **智能化**:自动发现字段并提供建议
|
||||
✅ **灵活性**:支持多种映射方式和策略
|
||||
✅ **易用性**:Web界面操作,一键应用建议
|
||||
✅ **可维护性**:配置文件管理,API接口支持
|
||||
✅ **兼容性**:向后兼容,无缝集成
|
||||
|
||||
该系统大大提升了飞书同步的灵活性和用户体验,为后续的功能扩展奠定了坚实基础。
|
||||
Reference in New Issue
Block a user