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

154 lines
6.3 KiB
Python
Raw Normal View History

# =============================================================================
# 企微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}")