Files
wecom_it_smart_desk/backend/app/services/automation/mapping_resolver.py
T

154 lines
6.3 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智能服务台 — 阶段5 自动化 员工→终端映射
# =============================================================================
# 说明:把员工(企微 UserID)解析为终端信息,供 virus_dispose 等场景使用。
# 主源:联软(支持 strusername 直接映射)
# 兜底:北森 EHR(仅给资产/部门 hint,无法提供火绒 client_id
# 结果缓存在 auto_mapping_cacheTTL),降低外部系统压力。
# =============================================================================
from __future__ import annotations
import logging
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, Optional
from sqlalchemy import select
from app.constants import MAPPING_SOURCE_PRIORITY
from app.integrations.factory import build_ehr_client, build_lianruan_client
from app.models.automation import MappingCache
logger = logging.getLogger(__name__)
# 映射缓存 TTL(秒):联软数据 10 分钟内复用以降低外部压力
MAPPING_CACHE_TTL_SECONDS = 600
class MappingResolver:
"""员工→终端映射解析器。"""
def __init__(self, db: Any = None, audit: Any = None):
self.db = db
self.audit = audit
async def resolve(self, employee_id: str, scenario_key: str = "") -> Dict[str, Any]:
"""解析员工→终端映射。
Returns:
Dict: {
"employee_id", "source"(lianruan/ehr/None),
"terminals": [...], "client_ids": [...](仅联软可给)
}
"""
# 1. 查缓存(联软结果优先复用)
cached = await self._load_cache(employee_id)
if cached is not None:
logger.info(f"命中映射缓存 employee={employee_id}, source={cached.get('source')}")
return cached
terminals: list = []
source: Optional[str] = None
client_ids: list = []
# 2. 主源:联软(按员工账号直接映射)
lianruan = None
try:
lianruan = await build_lianruan_client(self.db, audit=self.audit)
except Exception as e: # noqa: BLE001
logger.warning(f"构建联软客户端失败: {e}")
if lianruan is not None:
try:
data = await lianruan.query_dev_by_params(strusername=employee_id)
items = data.get("items", []) if isinstance(data, dict) else []
if items:
terminals = [
{
"strdevname": getattr(t, "strdevname", ""),
"strdevip": getattr(t, "strdevip", ""),
"strusername": getattr(t, "strusername", ""),
"strdeptname": getattr(t, "strdeptname", ""),
}
for t in items
]
# 火绒隔离以「终端标识」为目标;联软返回的是 hostname/ip
# 真实环境需按 hostname 做跨系统资产对齐(见交付说明假设)。
client_ids = [
(t.get("strdevname") or t.get("strdevip"))
for t in terminals
if (t.get("strdevname") or t.get("strdevip"))
]
source = "lianruan"
except Exception as e: # noqa: BLE001
logger.warning(f"联软映射失败 employee={employee_id}: {e}")
# 3. 兜底:北森 EHR(仅 hint,无火绒 client_id
if not source:
ehr = None
try:
ehr = await build_ehr_client(audit=self.audit)
except Exception as e: # noqa: BLE001
logger.warning(f"构建 EHR 客户端失败: {e}")
if ehr is not None:
try:
hint = await ehr.get_terminal_by_employee(employee_id)
if hint:
terminals = [hint]
source = "ehr"
except Exception as e: # noqa: BLE001
logger.warning(f"EHR 映射兜底失败 employee={employee_id}: {e}")
result: Dict[str, Any] = {
"employee_id": employee_id,
"source": source,
"terminals": terminals,
"client_ids": client_ids,
}
# 4. 写缓存(仅联软结果值得缓存,EHR hint 不长期缓存)
if source == "lianruan":
await self._save_cache(employee_id, result)
return result
async def _load_cache(self, employee_id: str) -> Optional[Dict[str, Any]]:
"""读取未过期的映射缓存。"""
if self.db is None:
return None
try:
stmt = select(MappingCache).where(MappingCache.employee_id == employee_id)
row = (await self.db.execute(stmt)).scalar_one_or_none()
if row is None:
return None
if row.expires_at is not None and row.expires_at < datetime.now(timezone.utc):
return None
return row.mapped_data
except Exception as e: # noqa: BLE001
logger.debug(f"读映射缓存失败: {e}")
return None
async def _save_cache(self, employee_id: str, data: Dict[str, Any]) -> None:
"""写入映射缓存。"""
if self.db is None:
return
try:
stmt = select(MappingCache).where(MappingCache.employee_id == employee_id)
row = (await self.db.execute(stmt)).scalar_one_or_none()
now = datetime.now(timezone.utc)
if row is None:
row = MappingCache(
employee_id=employee_id,
source=data.get("source", "lianruan"),
mapped_data=data,
expires_at=now + timedelta(seconds=MAPPING_CACHE_TTL_SECONDS),
)
self.db.add(row)
else:
row.mapped_data = data
row.source = data.get("source", row.source)
row.expires_at = now + timedelta(seconds=MAPPING_CACHE_TTL_SECONDS)
await self.db.flush()
except Exception as e: # noqa: BLE001
logger.warning(f"写映射缓存失败: {e}")