Files
wecom_it_smart_desk/backend/app/services/itsm_service.py
T
Simon bea288e414 feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) ==
- 代办事项真实数据源集成 (企微审批API 8bug修复链)
- H5/坐席端 Logo样式统一+绿色背景
- 视频引导页修复 (localStorage key v2)
- 坐席端 v9 Vue版本修复 (ElMessage._context)
- 截图按钮 v10 修复 (getDisplayMedia user gesture)
- 扫码样式恢复+H5扫码登录跳转修复
- H5截图快捷键提示

== 代码完成待部署 (3项) ==
- 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查)
- 会议室预定-小鱼易联终端 (40文件, 40/40测试通过)
- IT资产升级审批推送 (asset_service.py)

== 需求文档 (2项) ==
- 坐席端AI辅助消息框-PRD (4项新功能确认)
- 坐席端布局优化建议 v2.0 (7天计划)

== 新增文档 ==
- 日报-2026-07-11.md
- 知识迭代Bug修复报告-20260711.md
- 会议室预定-部署指南.md
- CHANGELOG.md 更新

== 测试 ==
- test_todo_integration.py: 40/40
- test_meetingroom.py: 40/40
- test_bugfix_ki_suggestions.py: 21/21
2026-07-11 23:13:10 +08:00

261 lines
9.0 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.
# =============================================================================
# 企微IT智能服务台 — ITSM 运维平台待办数据源
# =============================================================================
# 说明:ITSM 一站式运维平台 OpenAPI 数据源实现。
# 通过 ITSMSigner 进行 SHA1 签名认证,调用 ITSM OpenAPI 获取工单待办。
#
# 当前状态:
# - get_todo_list(): ITSM 列表 API 尚未提供,返回空列表 + 日志告警
# - get_todo_detail(): 已实现工单详情查询(调 workitem/detail API
#
# 待 ITSM 列表 API 到位后,只需实现 get_todo_list() 内部逻辑即可。
# =============================================================================
import logging
from typing import Any, Dict, List, Optional
import httpx
import redis.asyncio as aioredis
from app.config import settings
from app.services.todo_source_service import TodoSourceService
from app.utils.itsm_signer import ITSMSigner
logger = logging.getLogger(__name__)
# ITSM OpenAPI 工单详情端点
ITSM_WORKITEM_DETAIL_PATH = "/openapi/v1/process/workitem/detail"
# ITSM API 成功响应码
ITSM_SUCCESS_CODE = 20000
# ITSM HTTP 请求超时
ITSM_TIMEOUT = httpx.Timeout(timeout=30.0, connect=10.0, read=30.0)
def _itsm_priority_to_todo(itsm_priority: Any) -> str:
"""将 ITSM 优先级映射到统一的 todo 优先级(urgent/high/normal)。
Args:
itsm_priority: ITSM 返回的优先级字段(可能是字符串或数字)
Returns:
str: 统一优先级 urgent/high/normal
"""
if itsm_priority is None:
return "normal"
priority_str = str(itsm_priority).strip().lower()
# 紧急 / urgent / 1
if priority_str in ("urgent", "紧急", "1", "critical", "p0", "p1"):
return "urgent"
# 高 / high / 2
if priority_str in ("high", "", "2", "p2"):
return "high"
return "normal"
def _itsm_status_to_todo(itsm_status: Any) -> str:
"""将 ITSM 状态映射到统一的 todo 状态(pending/processing/resolved)。
Args:
itsm_status: ITSM 返回的状态字段
Returns:
str: 统一状态 pending/processing/resolved
"""
if itsm_status is None:
return "pending"
status_str = str(itsm_status).strip().lower()
# 已完成/已关闭类
if status_str in ("resolved", "closed", "done", "完成", "已关闭", "已完成", "resolved", "3"):
return "resolved"
# 处理中
if status_str in ("processing", "in_progress", "处理中", "2"):
return "processing"
# 默认待处理
return "pending"
class ITSMService(TodoSourceService):
"""ITSM 运维平台待办数据源实现。
通过 ITSM OpenAPI 获取当前坐席的代办工单。
签名认证使用 ITSMSignerSHA1 签名),请求头携带 appId/timestamp/sign。
Attributes:
agent_userid: 当前坐席的企微 userid
redis: Redis 异步客户端(预留,后续 SSO 认证可能需要)
base_url: ITSM API 基址
app_id: ITSM OpenAPI app_id
app_secret: ITSM OpenAPI app_secret
"""
def __init__(self, agent_userid: str, redis: aioredis.Redis):
"""初始化 ITSM 数据源服务。
从 settings 读取 ITSM 配置。如果 itsm_app_id 为空,
所有方法将返回空结果并记日志告警。
Args:
agent_userid: 当前坐席的企微 userid
redis: Redis 异步客户端实例
"""
self.agent_userid = agent_userid
self.redis = redis
self.base_url = settings.itsm_base_url.rstrip("/")
self.app_id = settings.itsm_app_id
self.app_secret = settings.itsm_app_secret
# ------------------------------------------------------------------
# 公开接口
# ------------------------------------------------------------------
async def get_todo_list(self) -> List[Dict[str, Any]]:
"""获取 ITSM 代办工单列表。
⚠️ ITSM 列表 API 尚未提供,当前返回空列表 + 日志告警。
待 API 到位后实现列表查询逻辑。
Returns:
List[Dict[str, Any]]: 空列表(API 待实现)
"""
if not self.app_id:
logger.warning("ITSM app_id 未配置,代办列表返回空")
return []
logger.warning(
"ITSM 代办列表 API 尚未实现,返回空列表。"
"待 ITSM 平台方提供列表 API 端点后补充实现。"
)
return []
async def get_todo_detail(self, item_id: str) -> Optional[Dict[str, Any]]:
"""获取 ITSM 工单详情。
通过 ITSM OpenAPI workitem/detail 端点获取工单详情,
并映射为统一 TodoItemData 格式。
Args:
item_id: 工单的 process_instance_id(不含 "ticket:" 前缀)
Returns:
Optional[Dict[str, Any]]: TodoItemData 格式的工单详情
"""
if not self.app_id:
logger.warning("ITSM app_id 未配置,无法查询工单详情")
return None
try:
# 调用 ITSM workitem detail API
detail = await self._get_workitem_detail(item_id, self.agent_userid)
if not detail:
return None
return self._map_to_todo_item(detail)
except Exception as e:
logger.error(
f"获取 ITSM 工单详情失败: item_id={item_id}, error={e}",
exc_info=True,
)
return None
# ------------------------------------------------------------------
# 私有方法
# ------------------------------------------------------------------
async def _do_post(self, url: str, body: Dict[str, Any]) -> Optional[dict]:
"""发送带签名的 POST 请求到 ITSM API。
使用 ITSMSigner 生成签名请求头,发送 JSON POST 请求。
解析 ITSM 标准响应格式 {code: 20000, data: {...}, message: "..."}。
Args:
url: 完整的 ITSM API URL
body: 请求体(业务数据)
Returns:
Optional[dict]: ITSM 响应中的 data 字段,失败返回 None
"""
headers = ITSMSigner.get_headers(self.app_id, self.app_secret, body)
async with httpx.AsyncClient(timeout=ITSM_TIMEOUT) as client:
response = await client.post(url, json=body, headers=headers)
result = response.json()
code = result.get("code")
if code != ITSM_SUCCESS_CODE:
logger.error(
f"ITSM API 调用失败: url={url}, code={code}, "
f"message={result.get('message', '')}"
)
return None
return result.get("data")
async def _get_workitem_detail(
self, process_instance_id: str, executor: str
) -> Optional[dict]:
"""调用 ITSM 工单详情 API。
POST {base_url}/openapi/v1/process/workitem/detail
Args:
process_instance_id: 工单流程实例 ID
executor: 当前执行人(坐席 userid
Returns:
Optional[dict]: ITSM 返回的工单详情数据
"""
url = f"{self.base_url}{ITSM_WORKITEM_DETAIL_PATH}"
body = {
"process_instance_id": process_instance_id,
"executor": executor,
}
return await self._do_post(url, body)
def _map_to_todo_item(self, detail: dict) -> Dict[str, Any]:
"""将 ITSM 工单详情映射为统一 TodoItemData 格式。
映射规则参考系统设计文档 §8.2:
- id: "ticket:{process_instance_id}"
- type: "ticket"
- title: 工单标题
- priority: ITSM 优先级映射到 urgent/high/normal
- status: ITSM 状态映射到 pending/processing/resolved
Args:
detail: ITSM API 返回的工单详情
Returns:
Dict[str, Any]: TodoItemData 格式的待办事项
"""
process_instance_id = detail.get("process_instance_id", "")
title = detail.get("title", "")
itsm_priority = detail.get("priority", "normal")
itsm_status = detail.get("status", "pending")
creator = detail.get("creator", "")
executor = detail.get("executor", "")
created_at = detail.get("created_at", "")
updated_at = detail.get("updated_at", "")
return {
"id": f"ticket:{process_instance_id}",
"type": "ticket",
"title": title or "ITSM 工单",
"priority": _itsm_priority_to_todo(itsm_priority),
"description": {
"process_instance_id": process_instance_id,
"executor": executor,
"status": itsm_status,
"creator": creator,
"itsm_priority": itsm_priority,
"title": title,
},
"status": _itsm_status_to_todo(itsm_status),
"assigned_agent_id": self.agent_userid,
"corp_id": "",
"created_at": created_at,
"updated_at": updated_at,
}