# ExternalSystemAdapter 抽象层设计文档

> 版本：V1.0 | 日期：2026-06-11 | 作者：智能IT支持服务台项目组

---

## 一、设计目标

为联软、火绒、aTrust、eHR 四个外部系统提供**统一适配层**，实现：

1. **接口统一**：上层业务代码只依赖抽象接口，不感知底层系统差异
2. **可替换性**：Mock数据开发 → 真实API无缝切换，只需改配置
3. **缓存透明**：外部数据自动缓存+定时刷新，业务层无感
4. **降级安全**：外部系统不可用时自动降级，不阻断主流程
5. **横向扩展**：新增系统只需实现一个 Adapter，零改动业务层

---

## 二、系统角色与优先级

| 系统 | 角色 | 核心能力 | 认证方式 | 凭证状态 |
|------|------|---------|---------|---------|
| 联软LV7000 | 主映射源(P0) | 终端查询(含strusername)、硬件详情、在线状态 | IP白名单+账号密码+Token | 明天可拿 |
| 火绒企业版 | 安全源(P0) | 终端列表、漏洞/病毒事件、一键隔离 | HMAC-SHA1 AccessKey | 现在可拿 |
| aTrust | VPN源(P1) | 在线用户+VPN IP、终端查询、踢出用户 | HMAC-SHA256签名 | 约一周 |
| 北森eHR | 辅助静态数据(P2) | 员工基础信息、任职信息 | OAuth2.0 | 待对接HR |

---

## 三、架构分层

```
┌─────────────────────────────────────────────────┐
│              上层业务代码（AI Wingman等）          │
├─────────────────────────────────────────────────┤
│           ExternalSystemService（统一门面）        │
│    ┌──────────┐ ┌──────────┐ ┌──────────┐       │
│    │ 缓存层   │ │ 降级策略 │ │ 配置管理 │       │
│    └──────────┘ └──────────┘ └──────────┘       │
├─────────────────────────────────────────────────┤
│           ExternalSystemAdapter（抽象基类）        │
│    ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────┐│
│    │ LianRuan │ │ HuoRong  │ │ aTrust   │ │eHR ││
│    │ Adapter  │ │ Adapter  │ │ Adapter  │ │ 适配││
│    └──────────┘ └──────────┘ └──────────┘ └────┘│
├─────────────────────────────────────────────────┤
│               MockAdapter（开发期）               │
└─────────────────────────────────────────────────┘
```

---

## 四、核心抽象接口

### 4.1 数据模型（统一DTO）

```python
class TerminalInfo(BaseModel):
    """统一终端信息模型 — 所有Adapter返回同一结构"""
    source_system: str           # 数据来源系统标识
    computer_name: str           # 计算机名
    ip_addresses: List[str]      # IP地址列表（含VPN虚拟IP）
    mac_addresses: List[str]      # MAC地址列表
    os_version: Optional[str]     # 操作系统版本
    is_online: bool               # 是否在线
    logged_in_user: Optional[str] # 当前登录用户账号（映射核心字段）
    logged_in_user_name: Optional[str]  # 用户姓名
    department: Optional[str]     # 所属部门
    hardware_summary: Optional[Dict]    # 硬件摘要（CPU/内存/磁盘）
    last_seen: Optional[datetime]       # 最后在线时间
    raw_data: Optional[Dict]            # 原始响应（调试用，生产可关闭）

class SecurityStatus(BaseModel):
    """统一安全状态模型"""
    source_system: str
    terminal_id: str
    virus_events: Optional[Dict]     # 病毒事件统计
    vulnerabilities: Optional[List]    # 高危漏洞列表
    is_isolated: bool                 # 是否被隔离
    isolation_source: Optional[str]   # 隔离来源系统

class VpnSession(BaseModel):
    """VPN会话模型（仅aTrust）"""
    source_system: str = "atrust"
    username: str
    display_name: Optional[str]
    remote_ip: str
    vpn_ip: Optional[str]       # 虚拟内网IP
    is_trusted: bool
    last_login: Optional[datetime]
```

### 4.2 Adapter抽象基类

```python
from abc import ABC, abstractmethod
from typing import Optional, List

class ExternalSystemAdapter(ABC):
    """外部系统适配器抽象基类
    
    每个外部系统实现此接口，上层业务只依赖此接口。
    """
    
    @property
    @abstractmethod
    def system_name(self) -> str:
        """系统标识名称，如 'lianruan' / 'huorong' / 'atrust' / 'ehr'"""
        ...
    
    @property
    @abstractmethod
    def is_available(self) -> bool:
        """当前系统是否可用（凭证已配置+网络可达）"""
        ...
    
    @abstractmethod
    async def health_check(self) -> bool:
        """健康检查 — 验证凭证和网络连通性"""
        ...
    
    # ── 终端查询能力 ──
    
    async def get_terminal_by_user(self, username: str) -> Optional[TerminalInfo]:
        """通过员工账号查询终端信息（映射核心方法）
        
        联软：queryDevByParams(strusername=xxx)
        火绒：_list(ip=xxx) 需配合联软IP交叉匹配
        aTrust：queryAll(bindUserList) 终端绑定用户
        eHR：不提供终端数据，返回None
        """
        return None  # 默认不支持，子类按需覆写
    
    async def get_terminal_by_computer(self, computer_name: str) -> Optional[TerminalInfo]:
        """通过计算机名查询终端信息"""
        return None
    
    async def get_terminal_detail(self, terminal_id: str) -> Optional[TerminalInfo]:
        """查询终端详细信息（硬件/软件/网络配置）"""
        return None
    
    # ── 安全能力 ──
    
    async def get_security_status(self, terminal_id: str) -> Optional[SecurityStatus]:
        """获取终端安全状态（病毒/漏洞/隔离状态）"""
        return None
    
    async def isolate_terminal(self, terminal_id: str, reason: str) -> bool:
        """隔离终端（仅火绒支持，需admin角色二次确认）"""
        raise NotImplementedError(f"{self.system_name} 不支持终端隔离")
    
    async def unisolate_terminal(self, terminal_id: str) -> bool:
        """解除终端隔离"""
        raise NotImplementedError(f"{self.system_name} 不支持解除隔离")
    
    # ── VPN/在线状态 ──
    
    async def get_vpn_sessions(self, username: Optional[str] = None) -> List[VpnSession]:
        """查询VPN在线会话（仅aTrust支持）"""
        return []
    
    async def get_online_status(self, username: str) -> bool:
        """查询用户是否在线"""
        return False
```

### 4.3 统一门面服务

```python
class ExternalSystemService:
    """外部系统统一门面 — 上层业务只调用此类"""
    
    def __init__(self, adapters: Dict[str, ExternalSystemAdapter], cache: CacheService):
        self._adapters = adapters  # {"lianruan": LianRuanAdapter, ...}
        self._cache = cache
    
    async def find_user_terminal(self, username: str) -> Optional[TerminalInfo]:
        """查找用户终端 — 优先联软，降级aTrust，最后eHR
        
        做什么：按映射优先级依次查询，任一系统返回即停止
        为什么：联软strusername精确匹配最可靠，aTrust次之
        """
        # 1. 联软（主源，strusername精确匹配）
        result = await self._query_with_cache("lianruan", "get_terminal_by_user", username)
        if result:
            return result
        
        # 2. aTrust（VPN源，bindUserList匹配）
        result = await self._query_with_cache("atrust", "get_terminal_by_user", username)
        if result:
            return result
        
        # 3. eHR（静态辅助，无终端数据）
        return None
    
    async def get_terminal_security(self, terminal_id: str) -> Optional[SecurityStatus]:
        """获取终端安全状态 — 仅火绒"""
        return await self._query_with_cache("huorong", "get_security_status", terminal_id)
    
    async def isolate_terminal(self, terminal_id: str, reason: str, operator: str) -> bool:
        """隔离终端 — 仅火绒，需operator记录审计日志"""
        logger.warning(f"终端隔离操作: terminal={terminal_id}, operator={operator}, reason={reason}")
        return await self._adapters["huorong"].isolate_terminal(terminal_id, reason)
```

---

## 五、缓存策略

| 数据类型 | 缓存TTL | 刷新策略 | 说明 |
|---------|---------|---------|------|
| 终端映射（员工→终端） | 30分钟 | 定时刷新+访问时检查 | 映射关系不常变 |
| 终端详情（硬件/软件） | 60分钟 | 懒加载 | 硬件配置极少变 |
| 安全状态（漏洞/病毒） | 5分钟 | 短TTL+事件驱动 | 安全状态需近实时 |
| VPN在线状态 | 1分钟 | 短TTL | 在线状态变化快 |
| eHR员工信息 | 24小时 | 每日凌晨全量同步 | 静态数据 |

缓存key格式：`ext:{system}:{method}:{param_hash}`

---

## 六、降级策略

| 故障场景 | 处理方式 | 用户影响 |
|---------|---------|---------|
| 单个系统不可用 | 跳过该系统，尝试下一优先级 | 部分数据缺失，不阻断 |
| 所有外部系统不可用 | 返回缓存数据（如有）+ 明确标注"数据可能过时" | 信息可能过时 |
| 缓存+外部系统均不可用 | 返回空结果+告警通知坐席 | 无法获取外部数据 |
| 火绒隔离操作失败 | 重试1次 → 失败则记录待执行队列 → 告警坐席 | 安全操作不静默失败 |

---

## 七、配置管理

```python
class ExternalSystemConfig(BaseModel):
    """外部系统连接配置 — 从环境变量或配置中心读取"""
    
    # 联软
    lianruan_base_url: str = "http://192.168.x.x:30098"
    lianruan_api_account: Optional[str] = None
    lianruan_api_password: Optional[str] = None
    
    # 火绒
    huorong_base_url: str = "http://huorong.oa.servyou-it.com:8080"
    huorong_access_key_id: Optional[str] = None
    huorong_access_key_secret: Optional[str] = None
    
    # aTrust
    atrust_base_url: str = "https://atrust.servyou-it.com:4433"
    atrust_api_id: Optional[str] = None
    atrust_api_secret: Optional[str] = None
    atrust_directory_domain: Optional[str] = None
    
    # eHR
    ehr_base_url: Optional[str] = None
    ehr_client_id: Optional[str] = None
    ehr_client_secret: Optional[str] = None
    
    # 全局
    cache_enabled: bool = True
    mock_mode: bool = False  # True时所有请求走MockAdapter
```

---

## 八、目录结构

```
backend/app/services/external/
├── __init__.py           # 模块导出
├── base.py               # 抽象基类 ExternalSystemAdapter + 数据模型
├── config.py             # 配置管理 ExternalSystemConfig
├── cache.py              # 缓存装饰器和策略
├── mock.py               # MockAdapter（开发期使用）
├── lianruan_adapter.py   # 联软适配器
├── huorong_adapter.py    # 火绒适配器
├── atrust_adapter.py     # aTrust适配器
├── ehr_adapter.py        # eHR适配器
└── service.py            # ExternalSystemService 统一门面
```

---

## 九、实施路径

| 阶段 | 内容 | 依赖 |
|------|------|------|
| Step 1 | base.py + config.py + mock.py + service.py + cache.py | 无，立即可做 |
| Step 2 | huorong_adapter.py | 凭证现在可拿 |
| Step 3 | lianruan_adapter.py | 凭证明天可拿 |
| Step 4 | atrust_adapter.py | 凭证约一周 |
| Step 5 | ehr_adapter.py | 待对接HR团队 |

---

## 十、与项目阶段的对应关系

| 项目阶段 | Adapter用途 | 对接系统 |
|---------|------------|---------|
| 阶段一(1C) | 不使用 — MVP只跑会话管理 | 无 |
| 阶段二(2B) | 联软终端查询 + 火绒安全状态 | 联软+火绒 |
| 阶段二(2C) | 火绒漏洞/病毒/隔离 | 火绒 |
| 阶段三(3B) | aTrust VPN数据 + AI混合排查 | aTrust |
| 阶段三(3C) | eHR员工信息 + 标注体系 | eHR |
