Files
wecom_it_smart_desk/backend/app/services/xylink_service.py
T

219 lines
7.5 KiB
Python
Raw Normal View History

# =============================================================================
# 企微IT智能服务台 — 小鱼易联终端管理服务
# =============================================================================
# 说明:封装小鱼易联开放平台API,提供:
# 1. API签名生成(支持签名1.0和2.0)
# 2. 终端状态查询
# 3. 推送自定义页面URL到终端(方案B预留)
# 4. 终端列表查询
#
# 参考:小鱼易联开放平台 https://openapi.xylink.com/
# =============================================================================
import hashlib
import hmac
import logging
from typing import Any, Dict, List, Optional
import httpx
logger = logging.getLogger(__name__)
class XyLinkService:
"""小鱼易联终端管理服务。
封装小鱼易联开放平台API调用,包括终端状态查询和URL推送。
P0阶段终端通过浏览器直接访问URL(方案A),此服务为方案B预留。
"""
def __init__(
self,
enterprise_id: str,
client_id: str,
client_secret: str,
api_base: str = "https://sdk.xylink.com/api/rest/external/v1/",
ext_id: str = "",
) -> None:
"""初始化小鱼易联API客户端。
Args:
enterprise_id: 小鱼易联企业ID
client_id: SDK客户端ID
client_secret: SDK客户端密钥
api_base: API基础URL
ext_id: 企业ID(扩展ID
"""
self.enterprise_id = enterprise_id
self.client_id = client_id
self.client_secret = client_secret
self.api_base = api_base.rstrip("/") + "/"
self.ext_id = ext_id
# 创建httpx异步客户端
self.client = httpx.AsyncClient(
timeout=httpx.Timeout(connect=5.0, read=15.0, write=10.0, pool=5.0)
)
# ==========================================================================
# 签名生成
# ==========================================================================
def _generate_signature(self, params: Dict[str, Any], timestamp: str) -> str:
"""生成API签名(签名2.0 — HMAC-SHA256)。
小鱼易联API签名算法:
1. 将参数按key排序
2. 拼接为 key1=value1&key2=value2... 格式
3. 追加 &timestamp={timestamp}
4. 使用 client_secret 作为密钥,HMAC-SHA256 计算签名
Args:
params: 请求参数
timestamp: 时间戳(毫秒)
Returns:
str: 签名字符串(hex)
"""
# 按key排序并拼接
sorted_keys = sorted(params.keys())
sign_str = "&".join(f"{k}={params[k]}" for k in sorted_keys)
sign_str += f"&timestamp={timestamp}"
# HMAC-SHA256 签名
signature = hmac.new(
self.client_secret.encode("utf-8"),
sign_str.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return signature
def _build_headers(self, params: Dict[str, Any]) -> Dict[str, str]:
"""构建请求头(含签名认证)。
Args:
params: 请求参数
Returns:
Dict[str, str]: 请求头字典
"""
import time
timestamp = str(int(time.time() * 1000))
signature = self._generate_signature(params, timestamp)
return {
"Content-Type": "application/json",
"xy-EnterpriseId": self.enterprise_id,
"xy-ClientId": self.client_id,
"xy-Timestamp": timestamp,
"xy-Signature": signature,
}
# ==========================================================================
# 终端管理 API
# ==========================================================================
async def get_terminal_status(self, sn: str) -> Dict[str, Any]:
"""查询终端在线状态。
Args:
sn: 终端序列号
Returns:
Dict[str, Any]: 终端状态信息(含 online/status 等字段)
Raises:
Exception: 查询失败
"""
url = f"{self.api_base}terminal/{sn}/status"
params = {"sn": sn}
headers = self._build_headers(params)
try:
response = await self.client.get(url, params=params, headers=headers)
result = response.json()
if response.status_code != 200:
logger.error(f"查询终端状态失败: sn={sn}, status={response.status_code}")
raise Exception(f"小鱼易联API错误: HTTP {response.status_code}")
logger.info(f"查询终端状态成功: sn={sn}")
return result
except httpx.HTTPError as e:
logger.error(f"查询终端状态网络错误: sn={sn}, error={e}")
raise Exception(f"小鱼易联API网络错误: {e}") from e
async def push_url_to_terminal(self, sn: str, url: str) -> Dict[str, Any]:
"""推送自定义页面URL到终端(方案B预留)。
通过小鱼易联API将指定URL推送到终端设备,
终端收到后自动打开该URL(用于远程推送会议室状态页面)。
Args:
sn: 终端序列号
url: 要推送的页面URL
Returns:
Dict[str, Any]: 推送结果
Raises:
Exception: 推送失败
"""
api_url = f"{self.api_base}terminal/{sn}/push-url"
params = {"sn": sn, "url": url}
headers = self._build_headers(params)
try:
response = await self.client.post(api_url, json=params, headers=headers)
result = response.json()
if response.status_code != 200:
logger.error(f"推送URL到终端失败: sn={sn}, status={response.status_code}")
raise Exception(f"小鱼易联API错误: HTTP {response.status_code}")
logger.info(f"推送URL到终端成功: sn={sn}, url={url}")
return result
except httpx.HTTPError as e:
logger.error(f"推送URL到终端网络错误: sn={sn}, error={e}")
raise Exception(f"小鱼易联API网络错误: {e}") from e
async def list_terminals(self) -> List[Dict[str, Any]]:
"""查询终端列表。
Returns:
List[Dict[str, Any]]: 终端列表,每项含 sn/name/status 等
Raises:
Exception: 查询失败
"""
url = f"{self.api_base}terminals"
params: Dict[str, Any] = {}
headers = self._build_headers(params)
try:
response = await self.client.get(url, params=params, headers=headers)
result = response.json()
if response.status_code != 200:
logger.error(f"查询终端列表失败: status={response.status_code}")
raise Exception(f"小鱼易联API错误: HTTP {response.status_code}")
terminals = result.get("terminals", [])
logger.info(f"查询终端列表成功: count={len(terminals)}")
return terminals
except httpx.HTTPError as e:
logger.error(f"查询终端列表网络错误: {e}")
raise Exception(f"小鱼易联API网络错误: {e}") from e
# ==========================================================================
# 资源清理
# ==========================================================================
async def close(self) -> None:
"""关闭 HTTP 客户端连接池。"""
await self.client.aclose()
logger.info("XyLinkService HTTP 客户端已关闭")