"""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 sqlalchemy.ext.asyncio import AsyncSession from yuxi.external_systems.infrastructure.container import create_use_cases_from_db from yuxi.external_systems.use_cases.dto.health_check import ( CountByStatusInput, DeleteOldRecordsInput, GetLatestBySystemInput, ListBySystemInput, ListFailingInput, ListLatestHealthInput, ListUncheckedInput, ) from yuxi.storage.postgres.models_business import User from server.utils.auth_middleware import get_admin_user, get_db, 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), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """全局最新健康状态一览。每个系统仅取最近一条记录。""" use_cases = create_use_cases_from_db(db) input_dto = ListLatestHealthInput( system_ids=system_ids, health_status=health_status, page=offset // limit + 1, page_size=limit, ) 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 过滤(逗号分隔)"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """按状态聚合统计。返回 4 状态分组计数 + total。""" use_cases = create_use_cases_from_db(db) 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), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """故障系统列表。通过 list_latest(health_status=...) 参数化查询。""" use_cases = create_use_cases_from_db(db) 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(..., description="待检查的系统 ID 列表(逗号分隔)"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """未探测系统列表。返回传入 system_ids 中无记录的子集。""" use_cases = create_use_cases_from_db(db) 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 格式)"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """清理过期记录(软删除)。按 checked_at 过滤。""" use_cases = create_use_cases_from_db(db) input_dto = DeleteOldRecordsInput(before=before) 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), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """系统跨环境健康状态。返回同一系统跨多个 env_key 的最新状态。""" use_cases = create_use_cases_from_db(db) 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 取最近一条)"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """系统最新健康状态。指定 env_key 时按 env 查询,否则跨 env 取最近一条。""" use_cases = create_use_cases_from_db(db) 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}