1. 清理测试文件中未使用的导入与冗余代码 2. 修复审计日志与批量操作的空值约束,统一填充"global"作为默认渠道 3. 调整批量消息撤回的响应语义,对齐其他端点的部分成功契约 4. 修复访问规则批量克隆的唯一约束问题,新增后缀自动处理逻辑 5. 替换anyio为asyncio并行调用,修正时间UTC导入路径 6. 优化前端外部系统概览页的刷新状态提示与缓存逻辑 7. 修复测试用例中的断言与请求方式问题,适配httpx删除请求特性 8. 重构后端路由的依赖注入,移除冗余的数据库会话依赖 9. 调整测试用例的权限校验逻辑,修正强制登出的权限判断 10. 修复语义分块测试的numpy依赖问题,清理冗余导入
199 lines
8.1 KiB
Python
199 lines
8.1 KiB
Python
"""HealthCheck 子域 Router。
|
||
|
||
外部系统限界上下文的健康检查查询 API,覆盖全局最新状态一览 /
|
||
状态聚合统计 / 系统跨环境状态 / 系统最新状态 / 故障系统列表 /
|
||
未探测系统列表 / 过期记录清理。
|
||
所有端点通过 ``create_use_cases_from_db`` 装配 use_cases,
|
||
经 ``health_check_service`` 端口调用用例。
|
||
|
||
路径顺序约束:静态路径(latest/stats/failing/unchecked/old-records)
|
||
必须在动态路径 /by-system/{system_id} 之前声明。
|
||
|
||
认证策略:
|
||
- 查询类端点使用 get_required_user(支持 JWT + API Key 双模认证)
|
||
- 清理端点使用 get_admin_user
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from typing import Any, Literal
|
||
|
||
from fastapi import APIRouter, Depends, Path, Query
|
||
from pydantic import BaseModel, Field
|
||
from yuxi.external_systems.infrastructure.container import UseCases
|
||
from yuxi.external_systems.use_cases.dto.health_check import (
|
||
CountByStatusInput,
|
||
DeleteOldRecordsInput,
|
||
GetLatestBySystemInput,
|
||
ListBySystemInput,
|
||
ListFailingInput,
|
||
ListLatestHealthInput,
|
||
ListUncheckedInput,
|
||
TriggerSystemHealthCheckInput,
|
||
)
|
||
from yuxi.storage.postgres.models_business import User
|
||
|
||
from server.routers.external_systems import get_use_cases
|
||
from server.utils.auth_middleware import get_admin_user, get_required_user
|
||
|
||
health_check_router = APIRouter(prefix="/health-checks", tags=["external-systems-health-check"])
|
||
|
||
|
||
# ---------------- Endpoints ----------------
|
||
|
||
|
||
# ---- 静态路径端点(必须在动态路径 /by-system/{system_id} 之前声明) ----
|
||
|
||
|
||
@health_check_router.get("/latest", response_model=dict)
|
||
async def list_latest_health(
|
||
system_ids: list[int] | None = Query(None, description="按系统 ID 过滤(逗号分隔)"),
|
||
health_status: Literal["healthy", "auth_failure", "connectivity_issue", "not_checked"] | None = Query(
|
||
None, description="健康状态:healthy/auth_failure/connectivity_issue/not_checked"
|
||
),
|
||
limit: int = Query(20, ge=1, le=100),
|
||
offset: int = Query(0, ge=0),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""全局最新健康状态一览。每个系统仅取最近一条记录。"""
|
||
input_dto = ListLatestHealthInput(
|
||
system_ids=system_ids,
|
||
health_status=health_status,
|
||
limit=limit,
|
||
offset=offset,
|
||
)
|
||
output = await use_cases.health_check_service.list_latest(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@health_check_router.get("/stats", response_model=dict)
|
||
async def count_by_status(
|
||
system_ids: list[int] | None = Query(None, description="按系统 ID 过滤(逗号分隔)"),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""按状态聚合统计。返回 4 状态分组计数 + total。
|
||
|
||
统计口径:每个系统取其最近一条健康检查记录的状态进行分组。
|
||
``not_checked`` 表示最近记录状态为 not_checked 的系统数,
|
||
**非**从未探测的系统数(后者使用 ``/unchecked`` 端点)。
|
||
``total`` 为有健康检查记录的系统总数,非全部外部系统数。
|
||
"""
|
||
input_dto = CountByStatusInput(system_ids=system_ids)
|
||
output = await use_cases.health_check_service.count_by_status(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@health_check_router.get("/failing", response_model=dict)
|
||
async def list_failing(
|
||
health_status: Literal["auth_failure", "connectivity_issue"] = Query(
|
||
"auth_failure",
|
||
description="故障状态过滤:auth_failure / connectivity_issue",
|
||
),
|
||
system_ids: list[int] | None = Query(None, description="按系统 ID 过滤(逗号分隔)"),
|
||
limit: int = Query(100, ge=1, le=500),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""故障系统列表。通过 list_latest(health_status=...) 参数化查询。"""
|
||
input_dto = ListFailingInput(
|
||
health_status=health_status,
|
||
system_ids=system_ids,
|
||
limit=limit,
|
||
)
|
||
output = await use_cases.health_check_service.list_failing(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@health_check_router.get("/unchecked", response_model=dict)
|
||
async def list_unchecked(
|
||
system_ids: list[int] = Query(..., min_length=1, description="待检查的系统 ID 列表(逗号分隔)"),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""未探测系统列表。返回传入 system_ids 中无记录的子集。"""
|
||
input_dto = ListUncheckedInput(system_ids=system_ids)
|
||
output = await use_cases.health_check_service.list_unchecked(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@health_check_router.delete("/old-records", response_model=dict)
|
||
async def delete_old_records(
|
||
before: datetime = Query(..., description="清理此时间之前的记录(ISO 8601 格式)"),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""清理过期记录(软删除)。按 checked_at 过滤,记录操作人以备审计。"""
|
||
input_dto = DeleteOldRecordsInput(before=before, deleted_by=current_user.uid)
|
||
output = await use_cases.health_check_service.delete_old_records(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
# ---- 动态路径端点(最后声明) ----
|
||
|
||
|
||
@health_check_router.get("/by-system/{system_id}", response_model=dict)
|
||
async def list_by_system(
|
||
system_id: int = Path(ge=1),
|
||
env_key: str | None = Query(None, max_length=32, description="按环境过滤"),
|
||
limit: int = Query(20, ge=1, le=100),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""系统跨环境健康状态。每个 env_key 仅返回最近一条记录。
|
||
|
||
指定 ``env_key`` 时仅返回该环境的最新记录;不指定时返回全部环境的最新记录。
|
||
"""
|
||
input_dto = ListBySystemInput(
|
||
system_id=system_id,
|
||
env_key=env_key,
|
||
limit=limit,
|
||
)
|
||
output = await use_cases.health_check_service.list_by_system(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@health_check_router.get("/by-system/{system_id}/latest", response_model=dict)
|
||
async def get_latest_by_system(
|
||
system_id: int = Path(ge=1),
|
||
env_key: str | None = Query(None, max_length=32, description="环境标识(不传则跨 env 取最近一条)"),
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""系统最新健康状态。指定 env_key 时按 env 查询,否则跨 env 取最近一条。"""
|
||
input_dto = GetLatestBySystemInput(system_id=system_id, env_key=env_key)
|
||
data = await use_cases.health_check_service.get_latest_by_system(input_dto)
|
||
return {"success": True, "data": data}
|
||
|
||
|
||
# ---- 系统级健康检查触发(管理员) ----
|
||
|
||
|
||
class _TriggerSystemHealthCheckBody(BaseModel):
|
||
"""触发系统级健康检查请求体。"""
|
||
|
||
env_key: str | None = Field(default=None, max_length=32, description="环境标识(不传则用默认环境)")
|
||
|
||
|
||
@health_check_router.post("/systems/{system_id}", response_model=dict)
|
||
async def trigger_system_health_check(
|
||
system_id: int = Path(ge=1),
|
||
body: _TriggerSystemHealthCheckBody | None = None,
|
||
use_cases: UseCases = Depends(get_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""触发系统级健康检查。
|
||
|
||
自动选取该系统下第一个启用的系统级工具执行探测。
|
||
若系统无可用工具,返回 400 错误(提示用户先配置工具)。
|
||
"""
|
||
input_dto = TriggerSystemHealthCheckInput(
|
||
system_id=system_id,
|
||
env_key=body.env_key if body else None,
|
||
triggered_by=current_user.uid,
|
||
)
|
||
output = await use_cases.tool_service.trigger_system_health_check(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|