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
|