- 新增 work_order_client.py:SHA256签名的工单API客户端(创建/自动结单) - 企微卡片改为两步交互:确认接单→[已处理完成/标记误报]→终态 - 告警通知后自动创建工单,orderId存入alarm_event_ext - 边缘端resolve支持先到先得:终态不可被覆盖 - 边缘端resolve后异步触发工单自动结单+卡片终态更新 - 新增WorkOrderConfig配置项 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
171 lines
4.8 KiB
Python
171 lines
4.8 KiB
Python
"""
|
||
安保工单开放接口客户端
|
||
|
||
对接外部工单系统,支持:
|
||
- 创建工单:POST /open-api/ops/security/order/create
|
||
- 自动结单:POST /open-api/ops/security/order/auto-complete
|
||
- SHA256 签名认证
|
||
"""
|
||
|
||
import hashlib
|
||
import json
|
||
import time
|
||
import uuid
|
||
from typing import Optional
|
||
|
||
import httpx
|
||
|
||
from app.utils.logger import logger
|
||
|
||
|
||
class WorkOrderClient:
|
||
"""安保工单 API 客户端(单例)"""
|
||
|
||
def __init__(self):
|
||
self._enabled = False
|
||
self._base_url = ""
|
||
self._app_id = ""
|
||
self._app_secret = ""
|
||
self._timeout = 10
|
||
|
||
def init(self, config):
|
||
"""初始化工单配置"""
|
||
self._enabled = config.enabled and bool(config.base_url) and bool(config.app_secret)
|
||
self._base_url = config.base_url.rstrip("/")
|
||
self._app_id = config.app_id
|
||
self._app_secret = config.app_secret
|
||
self._timeout = getattr(config, "timeout", 10)
|
||
|
||
if self._enabled:
|
||
logger.info(f"工单客户端已启用: base_url={self._base_url}")
|
||
else:
|
||
logger.info("工单客户端未启用")
|
||
|
||
@property
|
||
def enabled(self) -> bool:
|
||
return self._enabled
|
||
|
||
def _sign(self, body_json: str, nonce: str, timestamp: str) -> str:
|
||
"""
|
||
SHA256 签名
|
||
|
||
签名算法:SHA256(body_json + "appId=" + appId + "&nonce=" + nonce + "×tamp=" + timestamp + appSecret)
|
||
"""
|
||
raw = (
|
||
body_json
|
||
+ "appId=" + self._app_id
|
||
+ "&nonce=" + nonce
|
||
+ "×tamp=" + timestamp
|
||
+ self._app_secret
|
||
)
|
||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
||
|
||
def _build_headers(self, body_json: str) -> dict:
|
||
"""构造请求头(含签名)"""
|
||
nonce = uuid.uuid4().hex[:16]
|
||
timestamp = str(int(time.time() * 1000))
|
||
sign = self._sign(body_json, nonce, timestamp)
|
||
|
||
return {
|
||
"Content-Type": "application/json",
|
||
"appId": self._app_id,
|
||
"nonce": nonce,
|
||
"timestamp": timestamp,
|
||
"sign": sign,
|
||
}
|
||
|
||
async def create_order(
|
||
self,
|
||
title: str,
|
||
area_id: str,
|
||
alarm_id: str,
|
||
alarm_type: str,
|
||
) -> Optional[str]:
|
||
"""
|
||
创建安保工单
|
||
|
||
Returns:
|
||
orderId 字符串,失败返回 None
|
||
"""
|
||
if not self._enabled:
|
||
logger.debug("工单客户端未启用,跳过创建")
|
||
return None
|
||
|
||
body = {
|
||
"title": title,
|
||
"areaId": area_id,
|
||
"alarmId": alarm_id,
|
||
"alarmType": alarm_type,
|
||
}
|
||
body_json = json.dumps(body, ensure_ascii=False, separators=(",", ":"))
|
||
|
||
try:
|
||
headers = self._build_headers(body_json)
|
||
url = f"{self._base_url}/open-api/ops/security/order/create"
|
||
|
||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||
resp = await client.post(url, content=body_json, headers=headers)
|
||
data = resp.json()
|
||
|
||
if data.get("code") != 0:
|
||
logger.error(f"创建工单失败: {data}")
|
||
return None
|
||
|
||
order_id = data.get("data", {}).get("orderId", "")
|
||
logger.info(f"工单已创建: orderId={order_id}, alarmId={alarm_id}")
|
||
return order_id
|
||
|
||
except Exception as e:
|
||
logger.error(f"创建工单异常: {e}")
|
||
return None
|
||
|
||
async def auto_complete_order(
|
||
self,
|
||
order_id: str,
|
||
remark: str = "",
|
||
) -> bool:
|
||
"""
|
||
自动结单
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
if not self._enabled:
|
||
return False
|
||
|
||
body = {
|
||
"orderId": order_id,
|
||
"remark": remark,
|
||
}
|
||
body_json = json.dumps(body, ensure_ascii=False, separators=(",", ":"))
|
||
|
||
try:
|
||
headers = self._build_headers(body_json)
|
||
url = f"{self._base_url}/open-api/ops/security/order/auto-complete"
|
||
|
||
async with httpx.AsyncClient(timeout=self._timeout) as client:
|
||
resp = await client.post(url, content=body_json, headers=headers)
|
||
data = resp.json()
|
||
|
||
if data.get("code") != 0:
|
||
logger.error(f"自动结单失败: orderId={order_id}, resp={data}")
|
||
return False
|
||
|
||
logger.info(f"工单已自动结单: orderId={order_id}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"自动结单异常: orderId={order_id}, error={e}")
|
||
return False
|
||
|
||
|
||
# 全局单例
|
||
_work_order_client: Optional[WorkOrderClient] = None
|
||
|
||
|
||
def get_work_order_client() -> WorkOrderClient:
|
||
global _work_order_client
|
||
if _work_order_client is None:
|
||
_work_order_client = WorkOrderClient()
|
||
return _work_order_client
|