462 lines
19 KiB
Python
462 lines
19 KiB
Python
|
|
# -*- 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.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_mapping = {
|
|||
|
|
# 飞书字段名 -> 本地字段名
|
|||
|
|
"TR Number": "order_id", # TR编号映射到工单号
|
|||
|
|
"TR Description": "title", # TR描述作为标题(问题描述)
|
|||
|
|
"Type of problem": "category", # 问题类型作为分类
|
|||
|
|
"TR Level": "priority", # TR Level作为优先级
|
|||
|
|
"TR Status": "status", # TR Status作为状态(修正字段名)
|
|||
|
|
"Source": "assignee", # 来源信息
|
|||
|
|
"Date creation": "created_at", # 创建日期
|
|||
|
|
"处理过程": "description", # 处理过程作为描述
|
|||
|
|
"TR tracking": "solution", # TR跟踪作为解决方案
|
|||
|
|
"AI建议": "ai_suggestion", # AI建议字段
|
|||
|
|
"Issue Start Time": "updated_at" # 问题开始时间作为更新时间
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 状态映射 - 根据飞书表格中的实际值
|
|||
|
|
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 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]:
|
|||
|
|
"""将飞书字段转换为本地工单字段"""
|
|||
|
|
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"] 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} 不存在于数据中")
|
|||
|
|
|
|||
|
|
# 设置默认值
|
|||
|
|
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" # 根据表格中最常见的问题类型
|
|||
|
|
if "title" not in local_data or not local_data["title"]:
|
|||
|
|
local_data["title"] = "TR工单" # 默认标题
|
|||
|
|
|
|||
|
|
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)}
|