""" 安保工单开放接口客户端 对接外部工单系统,支持: - 创建工单: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: int, 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 # API 返回 {"code":0, "data": 1234567890} — data 直接是 orderId order_id = str(data.get("data", "")) 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": int(order_id) if order_id else 0, "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