# ============================================================================= # 企微IT智能服务台 — 阶段5 自动化 员工→终端映射 # ============================================================================= # 说明:把员工(企微 UserID)解析为终端信息,供 virus_dispose 等场景使用。 # 主源:联软(支持 strusername 直接映射) # 兜底:北森 EHR(仅给资产/部门 hint,无法提供火绒 client_id) # 结果缓存在 auto_mapping_cache(TTL),降低外部系统压力。 # ============================================================================= 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}")