2026-06-20 22:16:07 +08:00
|
|
|
|
"""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
|
2026-07-04 00:16:45 +08:00
|
|
|
|
from typing import Any, Literal
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
2026-07-04 00:16:45 +08:00
|
|
|
|
from fastapi import APIRouter, Depends, Path, Query
|
2026-07-11 21:39:05 +08:00
|
|
|
|
from pydantic import BaseModel, Field
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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,
|
2026-07-11 21:39:05 +08:00
|
|
|
|
TriggerSystemHealthCheckInput,
|
2026-06-20 22:16:07 +08:00
|
|
|
|
)
|
|
|
|
|
|
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 过滤(逗号分隔)"),
|
2026-07-04 00:16:45 +08:00
|
|
|
|
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),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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,
|
2026-07-11 06:57:24 +08:00
|
|
|
|
limit=limit,
|
|
|
|
|
|
offset=offset,
|
2026-06-20 22:16:07 +08:00
|
|
|
|
)
|
|
|
|
|
|
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]:
|
2026-07-11 06:57:24 +08:00
|
|
|
|
"""按状态聚合统计。返回 4 状态分组计数 + total。
|
|
|
|
|
|
|
|
|
|
|
|
统计口径:每个系统取其最近一条健康检查记录的状态进行分组。
|
|
|
|
|
|
``not_checked`` 表示最近记录状态为 not_checked 的系统数,
|
|
|
|
|
|
**非**从未探测的系统数(后者使用 ``/unchecked`` 端点)。
|
|
|
|
|
|
``total`` 为有健康检查记录的系统总数,非全部外部系统数。
|
|
|
|
|
|
"""
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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(
|
2026-07-04 00:16:45 +08:00
|
|
|
|
health_status: Literal["auth_failure", "connectivity_issue"] = Query(
|
2026-06-20 22:16:07 +08:00
|
|
|
|
"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(
|
2026-07-11 21:39:05 +08:00
|
|
|
|
system_ids: list[int] = Query(..., min_length=1, description="待检查的系统 ID 列表(逗号分隔)"),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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]:
|
2026-07-11 21:39:05 +08:00
|
|
|
|
"""清理过期记录(软删除)。按 checked_at 过滤,记录操作人以备审计。"""
|
2026-06-20 22:16:07 +08:00
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
2026-07-11 21:39:05 +08:00
|
|
|
|
input_dto = DeleteOldRecordsInput(before=before, deleted_by=current_user.uid)
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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(
|
2026-07-04 00:16:45 +08:00
|
|
|
|
system_id: int = Path(ge=1),
|
|
|
|
|
|
env_key: str | None = Query(None, max_length=32, description="按环境过滤"),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
limit: int = Query(20, ge=1, le=100),
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_required_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
2026-07-11 06:57:24 +08:00
|
|
|
|
"""系统跨环境健康状态。每个 env_key 仅返回最近一条记录。
|
|
|
|
|
|
|
|
|
|
|
|
指定 ``env_key`` 时仅返回该环境的最新记录;不指定时返回全部环境的最新记录。
|
|
|
|
|
|
"""
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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(
|
2026-07-04 00:16:45 +08:00
|
|
|
|
system_id: int = Path(ge=1),
|
|
|
|
|
|
env_key: str | None = Query(None, max_length=32, description="环境标识(不传则跨 env 取最近一条)"),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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}
|
2026-07-11 21:39:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 系统级健康检查触发(管理员) ----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""触发系统级健康检查。
|
|
|
|
|
|
|
|
|
|
|
|
自动选取该系统下第一个启用的系统级工具执行探测。
|
|
|
|
|
|
若系统无可用工具,返回 400 错误(提示用户先配置工具)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
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()}
|