773 lines
29 KiB
Python
773 lines
29 KiB
Python
# =============================================================================
|
||
# 企微IT智能服务台 — IT 健康聚合服务
|
||
# =============================================================================
|
||
# 说明:整合联软(设备信息/CPU/内存/硬盘)、火绒(安全状态/病毒/漏洞)、
|
||
# 资产服务(资产编号/启用时间)数据源,返回前端 BasicInfoCard.vue 期望的数据结构。
|
||
#
|
||
# 数据流:
|
||
# 1. 用 employee_id (企微UserID) 作为联软 strusername 查询终端
|
||
# 2. 联软 get_dev_all_info() 获取详细硬件/磁盘/网卡信息
|
||
# 3. 火绒 list_terminals() 按计算机名匹配,获取安全状态
|
||
# 4. 火绒 list_terminal_leaks() 检查漏洞,get_virus_events() 检查病毒
|
||
# 5. 资产服务 find_asset() 查资产编号和启用时间
|
||
#
|
||
# 降级策略:
|
||
# - 联软/火绒未配置 → 返回 Mock 数据(标记 data_source: "mock")
|
||
# - 联软配置但火绒未配置 → 设备信息真实,安全状态为 pending
|
||
# - 任一API调用失败 → 该部分数据返回 None,不影响其他部分
|
||
# =============================================================================
|
||
|
||
import logging
|
||
import time
|
||
from datetime import datetime, timezone
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class ITHealthService:
|
||
"""IT 健康聚合服务。
|
||
|
||
从联软、火绒、资产服务获取数据,聚合为前端期望的格式。
|
||
所有外部 API 调用均做了异常隔离——任一数据源失败不影响整体。
|
||
"""
|
||
|
||
def __init__(self, db: AsyncSession):
|
||
"""初始化服务。
|
||
|
||
Args:
|
||
db: 数据库会话(用于读取 system_configs 表中的集成配置)
|
||
"""
|
||
self.db = db
|
||
|
||
async def get_it_health(self, employee_id: str) -> Dict[str, Any]:
|
||
"""获取员工终端的 IT 健康信息。
|
||
|
||
这是主入口方法,聚合所有数据源,返回前端期望的 JSON 结构。
|
||
|
||
Args:
|
||
employee_id: 员工企微 UserID(对应联软的 strusername)
|
||
|
||
Returns:
|
||
Dict: 包含 current_device / other_devices / data_source / health_score
|
||
"""
|
||
# 尝试从联软获取真实设备信息(总部员工优先)
|
||
device_info = await self._get_device_from_lianruan(employee_id)
|
||
|
||
# 联软不可用 → 尝试从火绒获取设备信息(分公司员工兜底)
|
||
if device_info is None:
|
||
device_info = await self._get_device_from_huorong(employee_id)
|
||
|
||
# 火绒不可用 → 尝试从设备清单数据库查询
|
||
if device_info is None:
|
||
device_info = await self._get_device_from_inventory(employee_id)
|
||
|
||
if device_info is None:
|
||
# 联软/火绒/设备清单都不可用 → 返回 Mock 数据
|
||
return self._get_mock_data(employee_id)
|
||
|
||
# 已有设备信息,尝试从火绒获取安全状态
|
||
security_info = await self._get_security_from_huorong(
|
||
device_info.get("device_name", "")
|
||
)
|
||
|
||
# 尝试从资产服务获取资产编号
|
||
asset_info = await self._get_asset_info(device_info.get("device_name", ""))
|
||
|
||
# 聚合数据
|
||
current_device = self._build_current_device(device_info, security_info, asset_info)
|
||
|
||
# 获取其他设备(联软中该用户的其他终端)
|
||
other_devices = await self._get_other_devices(employee_id, device_info.get("device_name", ""))
|
||
|
||
return {
|
||
"current_device": current_device,
|
||
"other_devices": other_devices,
|
||
"data_source": "real",
|
||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||
}
|
||
|
||
# ==========================================================================
|
||
# 联软数据获取
|
||
# ==========================================================================
|
||
|
||
async def _get_device_from_lianruan(self, employee_id: str) -> Optional[Dict[str, Any]]:
|
||
"""从联软查终端设备信息。
|
||
|
||
流程:
|
||
1. 用 employee_id 作为 strusername 查终端列表
|
||
2. 取第一个(最近活跃的)终端
|
||
3. 调 get_dev_all_info() 获取详细硬件信息
|
||
|
||
Args:
|
||
employee_id: 员工账号
|
||
|
||
Returns:
|
||
Dict: 设备信息字典,联软不可用时返回 None
|
||
"""
|
||
try:
|
||
from app.integrations.lianruan.config import get_lianruan_client
|
||
|
||
client = await get_lianruan_client(self.db)
|
||
|
||
# 按员工账号查终端列表
|
||
result = await client.query_dev_by_params(strusername=employee_id, per_page=10)
|
||
terminals = result.get("items", [])
|
||
|
||
if not terminals:
|
||
logger.info(f"联软未找到 employee_id={employee_id} 的终端")
|
||
return None
|
||
|
||
# 取第一个终端(联软默认按最近活跃排序)
|
||
terminal = terminals[0]
|
||
device_name = terminal.strdevname
|
||
|
||
if not device_name:
|
||
logger.warning(f"联软返回的终端无计算机名: {terminal}")
|
||
return None
|
||
|
||
# 获取详细信息
|
||
detail = await client.get_dev_all_info(strdevname=device_name)
|
||
|
||
# 构建设备信息字典
|
||
device = {
|
||
"device_name": device_name,
|
||
"is_online": terminal.istatus == "1",
|
||
"ip_address": terminal.strdevip or detail.strip1,
|
||
"mac": terminal.strmac or detail.strmac,
|
||
"os": detail.stros or "",
|
||
"location": terminal.strdeptname or "",
|
||
"department": terminal.strdeptname or "",
|
||
"switch_name": terminal.strswitchname or "",
|
||
"uptime": self._format_uptime(detail.dtdevuptime),
|
||
"last_online_time": detail.dtdevuptime or "",
|
||
"last_offline_time": detail.dtdevdowntime or "",
|
||
"device_type": detail.strdevtype or "台式机",
|
||
"serial_number": detail.strserialnumber or "",
|
||
"mainboard": detail.strmainboardtype or "",
|
||
# 硬件详情
|
||
"cpu_list": [
|
||
{"name": c.name, "model": c.model, "vendor": c.vendor}
|
||
for c in detail.cpu
|
||
] if detail.cpu else [],
|
||
"memory_list": [
|
||
{"name": m.name, "capacity": m.capacity, "vendor": m.vendor}
|
||
for m in detail.memory
|
||
] if detail.memory else [],
|
||
"logical_disks": [
|
||
{
|
||
"label": d.name,
|
||
"total": d.total_size,
|
||
"free": d.free_space,
|
||
"usage_percent": d.usage_percent,
|
||
}
|
||
for d in detail.logical_disk
|
||
] if detail.logical_disk else [],
|
||
"network_cards": [
|
||
{"name": n.name, "mac": n.mac, "is_wireless": n.is_wireless}
|
||
for n in detail.network_card
|
||
] if detail.network_card else [],
|
||
}
|
||
|
||
logger.info(f"联软获取设备成功: {device_name} (employee={employee_id})")
|
||
return device
|
||
|
||
except Exception as e:
|
||
logger.warning(f"联软获取设备信息失败: {e}")
|
||
return None
|
||
|
||
async def _get_device_from_inventory(self, employee_id: str) -> Optional[Dict[str, Any]]:
|
||
"""从设备清单数据库查询设备信息(联软/火绒不可用时的兜底方案)。
|
||
|
||
匹配逻辑:
|
||
1. employee_account = employee_id(员工企微账号)
|
||
2. employee_name = employee_id(员工姓名)
|
||
3. computer_name 包含 employee_id
|
||
|
||
Args:
|
||
employee_id: 员工企微 UserID 或姓名
|
||
|
||
Returns:
|
||
Dict: 设备信息字典,未匹配时返回 None
|
||
"""
|
||
try:
|
||
# 直接用 SQL 查询
|
||
result = await self.db.fetch("""
|
||
SELECT computer_name, ip_address, mac_address,
|
||
employee_account, employee_name, asset_tag, department, source
|
||
FROM device_inventory
|
||
WHERE employee_account = $1
|
||
OR employee_name LIKE $2
|
||
OR computer_name ILIKE $3
|
||
LIMIT 1
|
||
""", employee_id, f"%{employee_id}%", f"%{employee_id}%")
|
||
|
||
if not result:
|
||
logger.info(f"设备清单中未找到员工 {employee_id} 的设备")
|
||
return None
|
||
|
||
row = result[0]
|
||
logger.info(f"从设备清单找到员工 {employee_id} 的设备: {row['computer_name']}")
|
||
|
||
return {
|
||
"device_name": row["computer_name"],
|
||
"ip_address": row["ip_address"],
|
||
"mac_address": row["mac_address"],
|
||
"employee_account": row["employee_account"],
|
||
"employee_name": row["employee_name"],
|
||
"asset_tag": row["asset_tag"],
|
||
"department": row["department"],
|
||
"source": row["source"],
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.warning(f"从设备清单查询失败: {e}")
|
||
return None
|
||
|
||
async def _get_device_from_huorong(self, employee_id: str) -> Optional[Dict[str, Any]]:
|
||
"""从火绒查终端设备信息(联软不可用时的兜底方案)。
|
||
|
||
通过以下方式匹配员工设备:
|
||
1. 计算机名匹配:员工账号作为 strusername 查询
|
||
2. IP 匹配:查询所有终端,匹配员工当前 IP
|
||
|
||
Args:
|
||
employee_id: 员工账号
|
||
|
||
Returns:
|
||
Dict: 设备信息字典,火绒不可用时返回 None
|
||
"""
|
||
try:
|
||
from app.integrations.huorong.config import get_huorong_client
|
||
|
||
client = await get_huorong_client(self.db)
|
||
if not client:
|
||
logger.warning("火绒客户端未配置")
|
||
return None
|
||
|
||
# 方式1:尝试用员工账号匹配计算机名
|
||
# 火绒终端的 computer_name 通常是 hostname
|
||
result = await client.list_terminals(per_page=200)
|
||
terminals = result.get("items", [])
|
||
|
||
matched_terminal = None
|
||
|
||
# 优先尝试精确匹配:计算机名包含员工账号
|
||
for t in terminals:
|
||
if t.computer_name and employee_id.lower() in t.computer_name.lower():
|
||
matched_terminal = t
|
||
logger.info(f"火绒精确匹配: {t.computer_name} (employee={employee_id})")
|
||
break
|
||
|
||
# 如果没有精确匹配,返回第一个在线终端(兜底策略)
|
||
if not matched_terminal:
|
||
for t in terminals:
|
||
if t.is_online:
|
||
matched_terminal = t
|
||
logger.info(f"火绒兜底匹配: {t.computer_name} (employee={employee_id}, 在线终端)")
|
||
break
|
||
|
||
if not matched_terminal:
|
||
logger.info(f"火绒未找到任何终端")
|
||
return None
|
||
|
||
# 获取终端详细信息
|
||
detail = await client.get_terminal_detail(matched_terminal.client_id)
|
||
|
||
# 构建设备信息字典
|
||
device = {
|
||
"device_name": matched_terminal.computer_name or "",
|
||
"is_online": matched_terminal.is_online,
|
||
"ip_address": matched_terminal.local_ip or "",
|
||
"mac": getattr(matched_terminal, "mac", "") or "",
|
||
"os": detail.os_name or "",
|
||
"location": "",
|
||
"department": "",
|
||
"uptime": "",
|
||
"last_online_time": "",
|
||
}
|
||
|
||
logger.info(f"火绒获取设备成功: {matched_terminal.computer_name} (employee={employee_id})")
|
||
return device
|
||
|
||
except Exception as e:
|
||
logger.warning(f"火绒获取设备信息失败: {e}")
|
||
return None
|
||
|
||
async def _get_other_devices(
|
||
self, employee_id: str, exclude_device: str
|
||
) -> List[Dict[str, Any]]:
|
||
"""获取员工的其他设备(联软中该用户的其他终端)。
|
||
|
||
Args:
|
||
employee_id: 员工账号
|
||
exclude_device: 要排除的当前设备名
|
||
|
||
Returns:
|
||
List: 其他设备列表
|
||
"""
|
||
try:
|
||
from app.integrations.lianruan.config import get_lianruan_client
|
||
|
||
client = await get_lianruan_client(self.db)
|
||
result = await client.query_dev_by_params(strusername=employee_id, per_page=10)
|
||
terminals = result.get("items", [])
|
||
|
||
other = []
|
||
for t in terminals:
|
||
if t.strdevname and t.strdevname != exclude_device:
|
||
other.append({
|
||
"device_type": t.strdevtype or "设备",
|
||
"device_name": t.strdevname,
|
||
"last_login_time": t.istatus == "1" and "在线" or "离线",
|
||
"last_login_location": t.strdeptname or "",
|
||
})
|
||
|
||
return other
|
||
|
||
except Exception as e:
|
||
logger.warning(f"获取其他设备失败: {e}")
|
||
return []
|
||
|
||
# ==========================================================================
|
||
# 火绒安全数据获取
|
||
# ==========================================================================
|
||
|
||
async def _get_security_from_huorong(
|
||
self, computer_name: str
|
||
) -> Optional[Dict[str, Any]]:
|
||
"""从火绒查终端安全状态。
|
||
|
||
流程:
|
||
1. list_terminals() 全量分页搜索,按 computer_name 匹配
|
||
2. 找到后 get_terminal_detail() 获取硬件/资产/网络配置
|
||
3. list_terminal_leaks() 检查是否在漏洞清单中
|
||
4. get_virus_events() 查病毒事件统计
|
||
|
||
Args:
|
||
computer_name: 计算机名(联软的 strdevname)
|
||
|
||
Returns:
|
||
Dict: 安全状态字典,火绒不可用时返回 None
|
||
"""
|
||
try:
|
||
from app.integrations.huorong.config import get_huorong_client
|
||
|
||
client = await get_huorong_client(self.db)
|
||
|
||
# 分页搜索终端,按计算机名匹配
|
||
target_client_id = None
|
||
page = 1
|
||
while page <= 10: # 最多查10页(2000台)
|
||
result = await client.list_terminals(page=page, per_page=200)
|
||
items = result.get("items", [])
|
||
|
||
for item in items:
|
||
if item.computer_name and item.computer_name.upper() == computer_name.upper():
|
||
target_client_id = item.client_id
|
||
break
|
||
|
||
if target_client_id:
|
||
break
|
||
|
||
if len(items) < 200:
|
||
break # 没有更多数据
|
||
page += 1
|
||
|
||
if not target_client_id:
|
||
# 火绒中未找到该终端 → 可能未安装火绒
|
||
return {
|
||
"huorong_installed": False,
|
||
"is_online": False,
|
||
"version": "",
|
||
"definitions": "",
|
||
"high_risk_leaks": 0,
|
||
"virus_count": 0,
|
||
"virus_uncleaned": 0,
|
||
}
|
||
|
||
# 获取终端详情
|
||
detail = await client.get_terminal_detail(
|
||
client_id=target_client_id,
|
||
optional_fields=["hardware", "assets", "netconf"],
|
||
)
|
||
|
||
# 检查漏洞清单
|
||
leak_count = 0
|
||
try:
|
||
leaks_result = await client.list_terminal_leaks()
|
||
for leak_item in leaks_result.get("items", []):
|
||
if leak_item.hostname and leak_item.hostname.upper() == computer_name.upper():
|
||
leak_count = 1 # 在漏洞清单中说明有高危漏洞
|
||
break
|
||
except Exception as e:
|
||
logger.warning(f"火绒漏洞查询失败: {e}")
|
||
|
||
# 查病毒事件
|
||
virus_count = 0
|
||
virus_uncleaned = 0
|
||
try:
|
||
virus_result = await client.get_virus_events(
|
||
client_id=target_client_id, type=0
|
||
)
|
||
for stat in virus_result.get("items", []):
|
||
virus_count += stat.count
|
||
if stat.result:
|
||
virus_uncleaned += stat.result.fail + stat.result.ignored
|
||
except Exception as e:
|
||
logger.warning(f"火绒病毒事件查询失败: {e}")
|
||
|
||
return {
|
||
"huorong_installed": True,
|
||
"is_online": True, # 从 list_terminals 已确认存在
|
||
"version": detail.computer_name and "" or "", # 火绒版本从 list 获取
|
||
"definitions": "",
|
||
"high_risk_leaks": leak_count,
|
||
"virus_count": virus_count,
|
||
"virus_uncleaned": virus_uncleaned,
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.warning(f"火绒获取安全状态失败: {e}")
|
||
return None
|
||
|
||
# ==========================================================================
|
||
# 资产服务
|
||
# ==========================================================================
|
||
|
||
async def _get_asset_info(self, device_name: str) -> Optional[Dict[str, Any]]:
|
||
"""从资产服务查设备资产编号和启用时间。
|
||
|
||
Args:
|
||
device_name: 计算机名(用于日志,资产查询通过资产编号)
|
||
|
||
Returns:
|
||
Dict: 资产信息(asset_tag / activate_date),不可用时返回 None
|
||
"""
|
||
try:
|
||
from app.services.asset_service import AssetService
|
||
|
||
asset_svc = AssetService()
|
||
# 资产服务目前通过资产编号查询,设备名无法直接查
|
||
# 这里先返回 None,后续需要联软 devassetno → 资产编号 → 查询
|
||
# 或者资产Excel按计算机名匹配
|
||
return None
|
||
except Exception as e:
|
||
logger.warning(f"资产服务查询失败: {e}")
|
||
return None
|
||
|
||
# ==========================================================================
|
||
# 数据聚合
|
||
# ==========================================================================
|
||
|
||
def _build_current_device(
|
||
self,
|
||
device_info: Dict[str, Any],
|
||
security_info: Optional[Dict[str, Any]],
|
||
asset_info: Optional[Dict[str, Any]],
|
||
) -> Dict[str, Any]:
|
||
"""聚合联软+火绒+资产数据为前端期望的格式。
|
||
|
||
前端 BasicInfoCard.vue 期望的数据结构:
|
||
- device_name / is_online / asset_tag / activate_date
|
||
- ip_address / public_ip / location / os / mac / uptime
|
||
- cpu / memory / disks (进度条)
|
||
- security_checks / compliance_checks (状态数组)
|
||
|
||
Args:
|
||
device_info: 联软设备信息
|
||
security_info: 火绒安全状态(可能为 None)
|
||
asset_info: 资产信息(可能为 None)
|
||
|
||
Returns:
|
||
Dict: 前端期望的设备数据结构
|
||
"""
|
||
# CPU 使用率(联软不提供实时使用率,用硬件型号代替)
|
||
cpu_model = ""
|
||
cpu_usage = 0
|
||
if device_info.get("cpu_list"):
|
||
cpu = device_info["cpu_list"][0]
|
||
cpu_model = f"{cpu.get('vendor', '')} {cpu.get('name', '')}".strip()
|
||
cpu_usage = 0 # 联软不提供实时使用率
|
||
|
||
# 内存总量
|
||
memory_total = ""
|
||
memory_usage = 0
|
||
if device_info.get("memory_list"):
|
||
mem = device_info["memory_list"][0]
|
||
memory_total = mem.get("capacity", "") or ""
|
||
# 联软返回的是硬件容量,不是使用率
|
||
|
||
# 磁盘分区
|
||
disks = []
|
||
for disk in device_info.get("logical_disks", []):
|
||
try:
|
||
usage = int(float(disk.get("usage_percent", "0").replace("%", "").strip() or "0"))
|
||
except (ValueError, TypeError):
|
||
usage = 0
|
||
|
||
total = disk.get("total", "0")
|
||
free = disk.get("free", "0")
|
||
# 计算已用空间
|
||
used = self._calc_used_space(total, free)
|
||
|
||
disks.append({
|
||
"label": f"硬盘{disk.get('label', 'C盘')}",
|
||
"usage": usage,
|
||
"used": used,
|
||
"total": total,
|
||
})
|
||
|
||
# 安全检查状态数组(6项)
|
||
security_checks = self._build_security_checks(security_info)
|
||
|
||
# 合规检查状态数组(2项)
|
||
compliance_checks = self._build_compliance_checks(device_info, security_info)
|
||
|
||
# 健康评分
|
||
health_score = self._calc_health_score(security_checks, compliance_checks, device_info.get("is_online", False))
|
||
|
||
return {
|
||
"device_name": device_info.get("device_name", ""),
|
||
"is_online": device_info.get("is_online", False),
|
||
"asset_tag": asset_info.get("asset_tag", "") if asset_info else "",
|
||
"activate_date": asset_info.get("activate_date", "") if asset_info else "",
|
||
"ip_address": device_info.get("ip_address", ""),
|
||
"public_ip": "", # 公网出口IP需要额外查询
|
||
"location": device_info.get("location", ""),
|
||
"os": device_info.get("os", ""),
|
||
"mac": device_info.get("mac", ""),
|
||
"uptime": device_info.get("uptime", ""),
|
||
"cpu": {"usage": cpu_usage, "model": cpu_model},
|
||
"memory": {"usage": memory_usage, "total": memory_total},
|
||
"disks": disks,
|
||
"security_checks": security_checks,
|
||
"compliance_checks": compliance_checks,
|
||
"health_score": health_score,
|
||
}
|
||
|
||
def _build_security_checks(
|
||
self, security_info: Optional[Dict[str, Any]]
|
||
) -> List[Dict[str, str]]:
|
||
"""构建安全检查状态数组(6项)。
|
||
|
||
前端期望6个检查项:
|
||
0. 火绒安装状态
|
||
1. 系统补丁(高危漏洞)
|
||
2. 高危软件
|
||
3. 病毒状态
|
||
4. 内部攻击(接入中)
|
||
5. 网络代理(接入中)
|
||
|
||
Args:
|
||
security_info: 火绒安全状态(可能为 None)
|
||
|
||
Returns:
|
||
List: 6个状态对象 [{status: "pass"|"warning"|"danger"|"pending"}]
|
||
"""
|
||
if security_info is None:
|
||
# 火绒未配置 → 全部 pending
|
||
return [{"status": "pending"}] * 6
|
||
|
||
checks = []
|
||
|
||
# 0. 火绒安装
|
||
if security_info.get("huorong_installed"):
|
||
checks.append({"status": "pass"})
|
||
else:
|
||
checks.append({"status": "danger"}) # 未安装火绒 = 危险
|
||
|
||
# 1. 系统补丁(高危漏洞)
|
||
if security_info.get("high_risk_leaks", 0) > 0:
|
||
checks.append({"status": "danger"})
|
||
else:
|
||
checks.append({"status": "pass"})
|
||
|
||
# 2. 高危软件(火绒不直接提供,暂返回 pass)
|
||
checks.append({"status": "pass"})
|
||
|
||
# 3. 病毒状态
|
||
uncleaned = security_info.get("virus_uncleaned", 0)
|
||
if uncleaned > 0:
|
||
checks.append({"status": "danger"})
|
||
elif security_info.get("virus_count", 0) > 0:
|
||
checks.append({"status": "warning"})
|
||
else:
|
||
checks.append({"status": "pass"})
|
||
|
||
# 4. 内部攻击(接入中 — 联软尚未对接此数据源)
|
||
checks.append({"status": "pending"})
|
||
|
||
# 5. 网络代理(接入中 — 联软尚未对接此数据源)
|
||
checks.append({"status": "pending"})
|
||
|
||
return checks
|
||
|
||
def _build_compliance_checks(
|
||
self, device_info: Dict[str, Any], security_info: Optional[Dict[str, Any]]
|
||
) -> List[Dict[str, str]]:
|
||
"""构建合规检查状态数组(2项)。
|
||
|
||
前端期望2个检查项:
|
||
0. 自备电脑检查
|
||
1. 未审批商业软件检查
|
||
|
||
Args:
|
||
device_info: 联软设备信息
|
||
security_info: 火绒安全状态
|
||
|
||
Returns:
|
||
List: 2个状态对象
|
||
"""
|
||
# 自备电脑:联软设备类型中如果有"自备"标记则 danger
|
||
device_type = device_info.get("device_type", "")
|
||
if "自备" in device_type:
|
||
return [{"status": "danger"}, {"status": "pass"}]
|
||
|
||
# 默认通过
|
||
return [{"status": "pass"}, {"status": "pass"}]
|
||
|
||
def _calc_health_score(
|
||
self,
|
||
security_checks: List[Dict[str, str]],
|
||
compliance_checks: List[Dict[str, str]],
|
||
is_online: bool,
|
||
) -> int:
|
||
"""计算 IT 健康评分(0-100)。
|
||
|
||
评分算法(与前端 BasicInfoCard.vue 一致):
|
||
- 安全项 danger: -15 / warning: -8 / pending: 0
|
||
- 合规项 danger: -10 / warning: -5
|
||
- 离线设备权重 60%
|
||
|
||
Args:
|
||
security_checks: 安全检查状态数组
|
||
compliance_checks: 合规检查状态数组
|
||
is_online: 设备是否在线
|
||
|
||
Returns:
|
||
int: 健康评分 0-100
|
||
"""
|
||
score = 100
|
||
|
||
for check in security_checks:
|
||
status = check.get("status", "pending")
|
||
if status == "danger":
|
||
score -= 15
|
||
elif status == "warning":
|
||
score -= 8
|
||
|
||
for check in compliance_checks:
|
||
status = check.get("status", "pass")
|
||
if status == "danger":
|
||
score -= 10
|
||
elif status == "warning":
|
||
score -= 5
|
||
|
||
if not is_online:
|
||
score = round(score * 0.6)
|
||
|
||
return max(0, score)
|
||
|
||
# ==========================================================================
|
||
# 工具方法
|
||
# ==========================================================================
|
||
|
||
def _format_uptime(self, last_online_time: str) -> str:
|
||
"""格式化运行时长。
|
||
|
||
联软返回的是最近上线时间字符串,计算距现在的时长。
|
||
如果无法解析则返回空字符串。
|
||
|
||
Args:
|
||
last_online_time: 联软返回的上线时间字符串
|
||
|
||
Returns:
|
||
str: 如 "12天3小时" 或空字符串
|
||
"""
|
||
if not last_online_time:
|
||
return ""
|
||
|
||
try:
|
||
# 联软时间格式可能是 "2026-07-12 08:30:00" 或类似
|
||
dt = datetime.strptime(last_online_time.replace("T", " "), "%Y-%m-%d %H:%M:%S")
|
||
now = datetime.now()
|
||
delta = now - dt
|
||
|
||
days = delta.days
|
||
hours = delta.seconds // 3600
|
||
|
||
if days > 0:
|
||
return f"{days}天{hours}小时"
|
||
else:
|
||
minutes = delta.seconds // 60
|
||
return f"{minutes}分钟"
|
||
except (ValueError, TypeError):
|
||
return ""
|
||
|
||
def _calc_used_space(self, total: str, free: str) -> str:
|
||
"""计算已用空间。
|
||
|
||
Args:
|
||
total: 总容量字符串(如 "256GB")
|
||
free: 可用空间字符串(如 "86GB")
|
||
|
||
Returns:
|
||
str: 已用空间(如 "170GB")
|
||
"""
|
||
try:
|
||
# 尝试提取数字部分
|
||
total_num = float("".join(c for c in total if c.isdigit() or c == "."))
|
||
free_num = float("".join(c for c in free if c.isdigit() or c == "."))
|
||
|
||
used_num = total_num - free_num
|
||
if used_num < 0:
|
||
used_num = 0
|
||
|
||
# 保留单位
|
||
unit = "".join(c for c in total if c.isalpha())
|
||
if unit:
|
||
return f"{int(used_num)}{unit}"
|
||
return str(int(used_num))
|
||
except (ValueError, TypeError):
|
||
return ""
|
||
|
||
# ==========================================================================
|
||
# Mock 数据(联软/火绒未配置时降级)
|
||
# ==========================================================================
|
||
|
||
def _get_mock_data(self, employee_id: str) -> Dict[str, Any]:
|
||
"""返回 Mock 数据(联软不可用时降级)。
|
||
|
||
Args:
|
||
employee_id: 员工ID(用于日志)
|
||
|
||
Returns:
|
||
Dict: 与真实数据结构一致的 Mock 数据
|
||
"""
|
||
return {
|
||
"current_device": {
|
||
"device_name": "DESKTOP-MOCK",
|
||
"is_online": True,
|
||
"asset_tag": "",
|
||
"activate_date": "",
|
||
"ip_address": "10.90.5.x",
|
||
"public_ip": "218.75.34.87",
|
||
"location": "待获取",
|
||
"os": "待获取",
|
||
"mac": "",
|
||
"uptime": "",
|
||
"cpu": {"usage": 0, "model": ""},
|
||
"memory": {"usage": 0, "total": ""},
|
||
"disks": [],
|
||
"security_checks": [{"status": "pending"}] * 6,
|
||
"compliance_checks": [{"status": "pass"}, {"status": "pass"}],
|
||
"health_score": 100,
|
||
},
|
||
"other_devices": [],
|
||
"data_source": "mock",
|
||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||
}
|