Files
assist/src/integrations/workorder_sync.py
赵杰 Jie Zhao (雄狮汽车科技) 16bb98131e feat: 自动提交 - 周一 2025/09/22 11:24:32.93
2025-09-22 11:24:32 +01:00

553 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
工单同步服务
实现飞书多维表格与本地工单系统的双向同步
"""
import json
import logging
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
# 工单状态和优先级枚举
class WorkOrderStatus:
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
CLOSED = "closed"
class WorkOrderPriority:
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
URGENT = "urgent"
logger = logging.getLogger(__name__)
class WorkOrderSyncService:
"""工单同步服务"""
def __init__(self, feishu_client: FeishuClient, app_token: str, table_id: str):
"""
初始化同步服务
Args:
feishu_client: 飞书客户端
app_token: 多维表格应用token
table_id: 表格ID
"""
self.feishu_client = feishu_client
self.app_token = app_token
self.table_id = table_id
self.ai_service = AISuggestionService()
# 初始化灵活字段映射器
self.field_mapper = FlexibleFieldMapper()
# 保留原有的字段映射作为默认配置(向后兼容)
self.field_mapping = {
# 核心字段
"TR Number": "order_id", # TR编号映射到工单号
"TR Description": "description", # TR描述作为详细描述
"Type of problem": "category", # 问题类型作为分类
"TR Level": "priority", # TR Level作为优先级
"TR Status": "status", # TR Status作为状态
"Source": "source", # 来源信息Mail, Telegram bot等
"Date creation": "created_at", # 创建日期
"处理过程": "solution", # 处理过程作为解决方案
"TR tracking": "resolution", # TR跟踪作为解决方案详情
# 扩展字段
"Created by": "created_by", # 创建人
"Module模块": "module", # 模块
"Wilfulness责任人": "wilfulness", # 责任人
"Date of close TR": "date_of_close", # 关闭日期
"Vehicle Type01": "vehicle_type", # 车型
"VIN|sim": "vin_sim", # 车架号/SIM
"App remote control version": "app_remote_control_version", # 应用远程控制版本
"HMI SW": "hmi_sw", # HMI软件版本
"父记录": "parent_record", # 父记录
"Has it been updated on the same day": "has_updated_same_day", # 是否同日更新
"Operating time": "operating_time", # 操作时间
# AI建议字段
"AI建议": "ai_suggestion", # AI建议字段
"Issue Start Time": "updated_at" # 问题开始时间作为更新时间
}
# 将原有映射添加到灵活映射器中
self._init_flexible_mapper()
# 状态映射 - 根据飞书表格中的实际值
self.status_mapping = {
"close": WorkOrderStatus.CLOSED, # 已关闭
"temporary close": WorkOrderStatus.IN_PROGRESS, # 临时关闭对应处理中
"OTA": WorkOrderStatus.IN_PROGRESS, # OTA状态对应处理中
"open": WorkOrderStatus.PENDING, # 开放状态对应待处理
"pending": WorkOrderStatus.PENDING, # 待处理
"completed": WorkOrderStatus.COMPLETED # 已完成
}
# 优先级映射 - 根据飞书表格中的实际值
self.priority_mapping = {
"Low": WorkOrderPriority.LOW,
"Medium": WorkOrderPriority.MEDIUM,
"High": WorkOrderPriority.HIGH,
"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]:
"""
从飞书同步数据到本地系统
Args:
generate_ai_suggestions: 是否生成AI建议
limit: 处理记录数量限制
Returns:
同步结果统计
"""
try:
logger.info("开始从飞书同步工单数据...")
# 获取飞书表格记录(限制数量)
records = self.feishu_client.get_table_records(self.app_token, self.table_id, page_size=limit)
if records.get("code") != 0:
raise Exception(f"获取飞书记录失败: {records.get('msg', '未知错误')}")
items = records.get("data", {}).get("items", [])
logger.info(f"从飞书获取到 {len(items)} 条记录")
# 生成AI建议
if generate_ai_suggestions:
logger.info("开始生成AI建议...")
items = self.ai_service.batch_generate_suggestions(items, limit)
# 将AI建议更新回飞书表格
for item in items:
if "ai_suggestion" in item:
try:
self.feishu_client.update_table_record(
self.app_token,
self.table_id,
item["record_id"],
{"AI建议": item["ai_suggestion"]}
)
logger.info(f"更新飞书记录 {item['record_id']} 的AI建议")
except Exception as e:
logger.error(f"更新飞书AI建议失败: {e}")
synced_count = 0
updated_count = 0
created_count = 0
errors = []
with db_manager.get_session() as session:
for record in items:
try:
# 解析飞书记录
parsed_fields = self.feishu_client.parse_record_fields(record)
feishu_id = record.get("record_id")
# 查找本地是否存在对应记录
existing_workorder = session.query(WorkOrder).filter(
WorkOrder.feishu_record_id == feishu_id
).first()
# 转换为本地工单格式
workorder_data = self._convert_feishu_to_local(parsed_fields)
workorder_data["feishu_record_id"] = feishu_id
if existing_workorder:
# 更新现有记录
for key, value in workorder_data.items():
if key != "feishu_record_id":
setattr(existing_workorder, key, value)
existing_workorder.updated_at = datetime.now()
updated_count += 1
else:
# 创建新记录
workorder_data["created_at"] = datetime.now()
workorder_data["updated_at"] = datetime.now()
new_workorder = WorkOrder(**workorder_data)
session.add(new_workorder)
created_count += 1
synced_count += 1
except Exception as e:
error_msg = f"处理记录 {record.get('record_id', 'unknown')} 失败: {str(e)}"
logger.error(error_msg)
errors.append(error_msg)
session.commit()
result = {
"success": True,
"total_records": len(items),
"synced_count": synced_count,
"created_count": created_count,
"updated_count": updated_count,
"ai_suggestions_generated": generate_ai_suggestions,
"errors": errors
}
logger.info(f"飞书同步完成: {result}")
return result
except Exception as e:
logger.error(f"飞书同步失败: {e}")
return {
"success": False,
"error": str(e)
}
def sync_to_feishu(self, workorder_id: int) -> Dict[str, Any]:
"""
将本地工单同步到飞书
Args:
workorder_id: 工单ID
Returns:
同步结果
"""
try:
with db_manager.get_session() as session:
workorder = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not workorder:
return {"success": False, "error": "工单不存在"}
# 转换为飞书格式
feishu_fields = self._convert_local_to_feishu(workorder)
if workorder.feishu_record_id:
# 更新飞书记录
result = self.feishu_client.update_table_record(
self.app_token, self.table_id, workorder.feishu_record_id, feishu_fields
)
else:
# 创建新飞书记录
result = self.feishu_client.create_table_record(
self.app_token, self.table_id, feishu_fields
)
if result.get("code") == 0:
# 保存飞书记录ID到本地
workorder.feishu_record_id = result["data"]["record"]["record_id"]
session.commit()
if result.get("code") == 0:
return {"success": True, "message": "同步成功"}
else:
return {"success": False, "error": result.get("msg", "同步失败")}
except Exception as e:
logger.error(f"同步到飞书失败: {e}")
return {"success": False, "error": str(e)}
def create_workorder_from_feishu_record(self, record_id: str) -> Dict[str, Any]:
"""
从飞书单条记录创建工单
Args:
record_id: 飞书记录ID
Returns:
创建结果
"""
try:
logger.info(f"从飞书记录 {record_id} 创建工单")
# 获取单条飞书记录
feishu_data = self.feishu_client.get_table_record(
self.app_token,
self.table_id,
record_id
)
if feishu_data.get("code") != 0:
return {
"success": False,
"message": f"获取飞书记录失败: {feishu_data.get('msg', '未知错误')}"
}
record = feishu_data.get("data", {}).get("record")
if not record:
return {
"success": False,
"message": "飞书记录不存在"
}
fields = record.get("fields", {})
# 转换为本地工单格式
local_data = self._convert_feishu_to_local(fields)
local_data["feishu_record_id"] = record_id
# 检查是否已存在
existing_workorder = self._find_existing_workorder(record_id)
if existing_workorder:
return {
"success": False,
"message": f"工单已存在: {existing_workorder.order_id}"
}
# 创建新工单
workorder = self._create_workorder(local_data)
return {
"success": True,
"message": f"工单创建成功: {local_data.get('order_id')}",
"workorder_id": workorder.id,
"order_id": local_data.get('order_id')
}
except Exception as e:
logger.error(f"从飞书记录创建工单失败: {e}")
return {
"success": False,
"message": f"创建工单失败: {str(e)}"
}
def _find_existing_workorder(self, feishu_record_id: str) -> Optional[WorkOrder]:
"""查找已存在的工单"""
try:
with db_manager.get_session() as session:
return session.query(WorkOrder).filter(
WorkOrder.feishu_record_id == feishu_record_id
).first()
except Exception as e:
logger.error(f"查找现有工单失败: {e}")
return None
def _create_workorder(self, local_data: Dict[str, Any]) -> WorkOrder:
"""创建新工单"""
try:
with db_manager.get_session() as session:
workorder = WorkOrder(
order_id=local_data.get("order_id"),
title=local_data.get("title"),
description=local_data.get("description"),
category=local_data.get("category"),
priority=local_data.get("priority"),
status=local_data.get("status"),
created_at=local_data.get("created_at"),
updated_at=local_data.get("updated_at"),
resolution=local_data.get("solution"),
feishu_record_id=local_data.get("feishu_record_id"),
assignee=local_data.get("assignee"),
solution=local_data.get("solution"),
ai_suggestion=local_data.get("ai_suggestion")
)
session.add(workorder)
session.commit()
session.refresh(workorder)
logger.info(f"创建工单成功: {workorder.order_id}")
return workorder
except Exception as e:
logger.error(f"创建工单失败: {e}")
raise
def _update_workorder(self, workorder: WorkOrder, local_data: Dict[str, Any]) -> WorkOrder:
"""更新现有工单"""
try:
with db_manager.get_session() as session:
workorder.title = local_data.get("title", workorder.title)
workorder.description = local_data.get("description", workorder.description)
workorder.category = local_data.get("category", workorder.category)
workorder.priority = local_data.get("priority", workorder.priority)
workorder.status = local_data.get("status", workorder.status)
workorder.updated_at = local_data.get("updated_at", workorder.updated_at)
workorder.resolution = local_data.get("solution", workorder.resolution)
workorder.assignee = local_data.get("assignee", workorder.assignee)
workorder.solution = local_data.get("solution", workorder.solution)
workorder.ai_suggestion = local_data.get("ai_suggestion", workorder.ai_suggestion)
session.commit()
session.refresh(workorder)
logger.info(f"更新工单成功: {workorder.order_id}")
return workorder
except Exception as e:
logger.error(f"更新工单失败: {e}")
raise
def _update_feishu_ai_suggestion(self, record_id: str, ai_suggestion: str) -> bool:
"""更新飞书表格中的AI建议"""
try:
result = self.feishu_client.update_record(
self.app_token,
self.table_id,
record_id,
{"AI建议": ai_suggestion}
)
return result.get("code") == 0
except Exception as e:
logger.error(f"更新飞书AI建议失败: {e}")
return False
def _convert_feishu_to_local(self, feishu_fields: Dict[str, Any]) -> Dict[str, Any]:
"""将飞书字段转换为本地工单字段"""
logger.info(f"开始转换飞书字段: {feishu_fields}")
# 使用灵活映射器进行字段转换
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", "")
problem_type = feishu_fields.get("Type of problem", "")
if tr_number and problem_type:
local_data["title"] = f"{tr_number} - {problem_type}"
elif tr_number:
local_data["title"] = f"{tr_number} - TR工单"
else:
local_data["title"] = "TR工单"
# 设置默认值
if "status" not in local_data:
local_data["status"] = WorkOrderStatus.PENDING
if "priority" not in local_data:
local_data["priority"] = WorkOrderPriority.MEDIUM
if "category" not in local_data:
local_data["category"] = "Remote control" # 根据表格中最常见的问题类型
return local_data
def _convert_local_to_feishu(self, workorder: WorkOrder) -> Dict[str, Any]:
"""将本地工单字段转换为飞书字段"""
feishu_fields = {}
# 反向映射
reverse_mapping = {v: k for k, v in self.field_mapping.items()}
for local_field, feishu_field in reverse_mapping.items():
value = getattr(workorder, local_field, None)
if value is not None:
# 特殊字段处理
if local_field == "status":
# 反向状态映射
reverse_status = {v: k for k, v in self.status_mapping.items()}
value = reverse_status.get(value, str(value))
elif local_field == "priority":
# 反向优先级映射
reverse_priority = {v: k for k, v in self.priority_mapping.items()}
value = reverse_priority.get(value, str(value))
elif local_field in ["created_at", "updated_at"] and isinstance(value, datetime):
value = value.isoformat()
feishu_fields[feishu_field] = value
return feishu_fields
def get_sync_status(self) -> Dict[str, Any]:
"""获取同步状态统计"""
try:
with db_manager.get_session() as session:
total_local = session.query(WorkOrder).count()
synced_count = session.query(WorkOrder).filter(
WorkOrder.feishu_record_id.isnot(None)
).count()
return {
"total_local_workorders": total_local,
"synced_workorders": synced_count,
"unsynced_workorders": total_local - synced_count
}
except Exception as e:
logger.error(f"获取同步状态失败: {e}")
return {"error": str(e)}