本次提交包含多类优化:
1. 移除多个路由文件中多余的空导入行,统一代码格式
2. 重构Query参数定义,将长参数拆分为多行提升可读性
3. 新增多个业务端点:
- 渠道能力画像矩阵查询CAP-03
- 配对审批计数接口用于待办角标
- 批量查询对端目录资料接口
- 向导扫码登录相关端点
- 会话实时事件SSE推送端点
- 工作台待办统计接口
4. 完善异常处理逻辑,补充OperationTimeoutError导入并优化NotImplementedError的细节返回
5. 调整路由导入顺序,修复动态路由路径冲突隐患
6. 更新文档注释与接口清单,修正部分接口描述细节
309 lines
14 KiB
Python
309 lines
14 KiB
Python
"""健康检查域 Router(HLT-AGG-01 / HLT-AGG-02 / HLT-EXP-01 / HLT-SINGLE / HLT-PROBE)。
|
||
|
||
本 router 实现渠道健康检查域的全部 HTTP 端点,覆盖聚合健康(简略 / 详细)、
|
||
诊断导出、单渠道健康、主动探测共 5 个操作。
|
||
|
||
模板选型:
|
||
- HLT-AGG-01 / HLT-AGG-02 / HLT-EXP-01:模板 B(数据面端口路由),
|
||
经 ``use_cases.health_check`` 端口调用 ``HealthCheckService`` 类型化
|
||
方法,不经控制面管道。
|
||
- HLT-SINGLE / HLT-PROBE:模板 A(控制面端口路由),经
|
||
``use_cases.health_check_control`` 端口调用 ``ChannelControlService``
|
||
类型化方法,走控制面管道五阶段(auth → permission → rate_limit →
|
||
dispatch → audit)。
|
||
|
||
鉴权策略:HLT-AGG-01 无鉴权(K8s 探针 + 公开状态页);其余端点使用
|
||
``get_admin_user`` 依赖,要求管理员角色。角色校验由依赖函数完成,router
|
||
内不做角色判断(规范 §4)。
|
||
|
||
路径设计:静态路径(``/health`` / ``/health/detail`` /
|
||
``/health/diagnostics/export``)先于动态路径
|
||
(``/{channel_type}/{account_id}/health`` /
|
||
``/{channel_type}/{account_id}/probe``)声明,避免 ``/health`` 被捕获为
|
||
``channel_type="health"``(规范 §6.5)。子 router 不自行设置 prefix,
|
||
根前缀 ``/channels`` 由 ``channels_router`` 聚合 router 统一追加。
|
||
|
||
注册顺序:``health_router`` 必须在 ``channels_router`` 聚合中先于含
|
||
``/{channel_type}`` 动态路径的 router(如 ``account_router`` /
|
||
``login_router`` / ``directory_router`` 等)注册。
|
||
|
||
端点清单(对应《健康检查域设计方案》§2.1 + P1 缺口 §2.3.17):
|
||
- GET /health HLT-AGG-01 get_health
|
||
- GET /health/detail HLT-AGG-02 get_health_detail
|
||
- POST /health/diagnostics/export HLT-EXP-01 export_diagnostics
|
||
- GET /{channel_type}/{account_id}/health HLT-SINGLE get_single_health
|
||
- POST /{channel_type}/{account_id}/probe HLT-PROBE probe_channel
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any, Literal
|
||
|
||
from fastapi import APIRouter, Depends, Query, Request
|
||
from pydantic import BaseModel, ConfigDict, Field
|
||
from yuxi.channels.contract.dtos.channel import ChannelType
|
||
from yuxi.channels.contract.dtos.health import (
|
||
DiagnosticsExportRequest,
|
||
HealthQuery,
|
||
)
|
||
from yuxi.channels.contract.dtos.health_check import ProbeCmd, SingleHealthQuery
|
||
from yuxi.storage.postgres.models_business import User
|
||
|
||
from server.routers.channels import (
|
||
build_operator,
|
||
dataclass_to_dict,
|
||
get_channel_use_cases,
|
||
raiseOnControlFailure,
|
||
serialize_control_data,
|
||
)
|
||
from server.utils.auth_middleware import get_admin_user
|
||
|
||
# build_operator 仅在需要构造 Operator 注入端口方法时使用(HLT-EXP-01)。
|
||
# HLT-AGG-01 / HLT-AGG-02 的 checkHealth(query) 契约不接受 operator,不构造。
|
||
|
||
health_router = APIRouter(tags=["channels-health"])
|
||
|
||
|
||
# ---------------- 静态路径端点(须先于动态路径声明) ----------------
|
||
|
||
|
||
@health_router.get("/health", response_model=dict)
|
||
async def get_health(
|
||
use_cases=Depends(get_channel_use_cases),
|
||
) -> dict[str, Any]:
|
||
"""聚合健康检查(简略,HLT-AGG-01,FR-35)。
|
||
|
||
无鉴权端点,适用于 K8s liveness/readiness 探针与公开状态页。
|
||
返回简略视图:status / version / degraded_components / 简化渠道列表
|
||
(channel_type / plugin_state),不暴露 account_id / last_error /
|
||
worker_status / db_connection_pool_status / circuit_breaker_state 等
|
||
运维细节与业务标识,避免无鉴权端点信息泄露。
|
||
|
||
编排链路:HTTP 入参 → 构造 HealthQuery(with_probe=False)→
|
||
use_cases.health_check.checkHealth(query) 直接调用端口方法 →
|
||
从 HealthSnapshot 提取简略字段构造响应 dict。
|
||
|
||
采用模板 B(数据面):不经过控制面管道,不调用 raiseOnControlFailure,
|
||
契约异常由全局 unified_error_handler 统一映射为 HTTP 响应。响应遵循
|
||
``{"success": True, "data": ...}`` 统一结构,K8s 探针仅依赖 HTTP 200
|
||
状态码判定存活,不解析响应体结构。
|
||
"""
|
||
query = HealthQuery()
|
||
snapshot = await use_cases.health_check.checkHealth(query)
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"status": snapshot.status,
|
||
"version": snapshot.version,
|
||
"degraded_components": list(snapshot.degraded_components),
|
||
"channels": [
|
||
{
|
||
"channel_type": ch.channel_type,
|
||
"plugin_state": ch.plugin_state,
|
||
}
|
||
for ch in snapshot.channels
|
||
],
|
||
},
|
||
}
|
||
|
||
|
||
@health_router.get("/health/detail", response_model=dict)
|
||
async def get_health_detail(
|
||
channel_type: ChannelType | None = Query(default=None, description="按渠道过滤(None 表示全渠道)"),
|
||
with_probe: bool = Query(default=False, description="是否绕过缓存触发实时聚合(非调用 probeChannel)"),
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""聚合健康检查(详细,HLT-AGG-02,FR-35)。
|
||
|
||
管理员鉴权端点,适用于运维排障。返回完整 HealthSnapshot,含
|
||
worker_status / redis_stream_status / db_connection_pool_status /
|
||
各渠道 circuit_breaker_state / queue_depth / connection_pool_status /
|
||
trace_id / error_code 等运维字段。
|
||
|
||
编排链路:HTTP 入参(channel_type / with_probe)→
|
||
构造 HealthQuery(channel_filter / with_probe)→
|
||
use_cases.health_check.checkHealth(query) 直接调用端口方法 →
|
||
dataclass_to_dict(snapshot) 序列化完整快照返回。
|
||
|
||
采用模板 B(数据面):不经过控制面管道,不调用 raiseOnControlFailure。
|
||
with_probe 参数语义:with_probe=True 绕过短期缓存触发实时聚合,
|
||
非调用 probeChannel 端口方法(详见设计方案 §1.5 决策 5)。
|
||
|
||
checkHealth(query: HealthQuery) 契约不接受 operator 参数(只读无副作用
|
||
查询),current_user 仅用于鉴权(get_admin_user 依赖副作用),不在
|
||
函数体使用。
|
||
"""
|
||
query = HealthQuery(channel_filter=channel_type, with_probe=with_probe)
|
||
snapshot = await use_cases.health_check.checkHealth(query)
|
||
return {"success": True, "data": dataclass_to_dict(snapshot)}
|
||
|
||
|
||
# ---------------- Request Schemas ----------------
|
||
|
||
|
||
class DiagnosticsExportPayload(BaseModel):
|
||
"""诊断导出请求体(HLT-EXP-01,Router 层 Pydantic schema)。
|
||
|
||
字段对齐契约层 DiagnosticsExportRequest DTO,但 operator 由 router
|
||
构造(从认证用户),不在 body 中。命名为 ``Payload`` 后缀以区分 Router
|
||
层 schema 与契约层 DTO,避免同名冲突(规范反模式:别名导入降低可读性)。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
channel_filter: ChannelType | None = Field(default=None, description="按渠道过滤(None 表示全渠道)")
|
||
include_audit_logs: bool = Field(default=True, description="是否包含审计日志")
|
||
audit_log_count: int = Field(default=100, ge=1, le=500, description="审计日志条数上限")
|
||
include_error_logs: bool = Field(default=True, description="是否包含错误日志")
|
||
error_log_count: int = Field(default=100, ge=1, le=500, description="错误日志条数上限")
|
||
|
||
|
||
# ---------------- Endpoints ----------------
|
||
|
||
|
||
@health_router.post("/health/diagnostics/export", response_model=dict)
|
||
async def export_diagnostics(
|
||
payload: DiagnosticsExportPayload,
|
||
request: Request,
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""导出诊断信息包(HLT-EXP-01,FR-35)。
|
||
|
||
管理员鉴权端点,适用于故障排障。导出 manifest 快照 + 插件状态 +
|
||
审计日志 + 错误日志 + 队列深度 + Worker / Redis / DB 池状态,
|
||
敏感字段已脱敏(部分遮蔽 app_secret / encrypt_key / verification_token /
|
||
access_token / refresh_token;全量遮蔽 webhook_secret / webhook_url /
|
||
password / api_key),审计日志写入 fail-closed。
|
||
|
||
编排链路:HTTP 入参 → build_operator 构造操作人 → 构造
|
||
DiagnosticsExportRequest DTO → use_cases.health_check.exportDiagnostics(request)
|
||
直接调用端口方法 → dataclass_to_dict(bundle) 序列化诊断包返回。
|
||
|
||
对应数据面端口方法 exportDiagnostics(非控制面管道路径),
|
||
不调用 raiseOnControlFailure。敏感字段脱敏由 framework 层
|
||
DiagnosticsExporter 完成,router 层不重复脱敏。鉴权由 framework 层
|
||
``get_admin_user`` 守门,对齐端口契约 ``@pre: operator 已通过权限校验
|
||
(管理员角色)``,用例服务不重复校验权限(避免双重鉴权)。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
export_request = DiagnosticsExportRequest(
|
||
operator=operator,
|
||
channel_filter=payload.channel_filter,
|
||
include_audit_logs=payload.include_audit_logs,
|
||
audit_log_count=payload.audit_log_count,
|
||
include_error_logs=payload.include_error_logs,
|
||
error_log_count=payload.error_log_count,
|
||
)
|
||
bundle = await use_cases.health_check.exportDiagnostics(export_request)
|
||
return {"success": True, "data": dataclass_to_dict(bundle)}
|
||
|
||
|
||
# ---------------- 动态路径端点(须在静态路径之后声明) ----------------
|
||
|
||
|
||
class ProbeRequest(BaseModel):
|
||
"""主动探测请求体(HLT-PROBE,Router 层 Pydantic schema)。
|
||
|
||
字段对齐契约层 ``ProbeCmd`` DTO 的非操作人部分,``operator`` 由 router
|
||
构造(从认证用户),不在 body 中。``probe_type`` 仅允许 ``connectivity``
|
||
或 ``full``,由 Pydantic Literal 约束在 HTTP 边界即拒绝非法值,
|
||
契约层 ``ProbeCmd.__post_init__`` 与控制面管道 dispatch_stage 重复校验
|
||
(防御深度)。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
probe_type: Literal["connectivity", "full"] = Field(
|
||
default="connectivity",
|
||
description="探测类型(connectivity 仅测连通性,full 含凭据/权限/Webhook 全项检查)",
|
||
)
|
||
timeout_ms: int = Field(
|
||
default=5000,
|
||
ge=1000,
|
||
le=30000,
|
||
description="探测超时毫秒数(默认 5000,上限 30000)",
|
||
)
|
||
|
||
|
||
@health_router.get(
|
||
"/{channel_type}/{account_id}/health",
|
||
response_model=dict,
|
||
)
|
||
async def get_single_health(
|
||
channel_type: ChannelType,
|
||
account_id: str,
|
||
request: Request,
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""查询单渠道健康详情(HLT-SINGLE)。
|
||
|
||
对应控制面操作 ``health/single``(由 ``ChannelControlService.getSingleHealth``
|
||
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
|
||
聚合指定渠道账户的账户状态、熔断器状态、队列深度、插件状态、连接池状态、
|
||
worker 状态等健康指标,供运维排障定位单渠道健康问题。本端点为纯读聚合
|
||
(GET 安全方法语义),不触发渠道 API 调用;主动深度探测由
|
||
``POST /{channel_type}/{account_id}/probe``(HLT-PROBE)承担。
|
||
|
||
编排链路:HTTP 入参 → ``build_operator`` 构造操作人 → 构造
|
||
``SingleHealthQuery`` → ``use_cases.health_check_control.getSingleHealth``
|
||
调用控制面端口方法 → ``ControlResult`` 经 ``raiseOnControlFailure`` 转译
|
||
失败 → ``serialize_control_data`` 序列化单渠道健康结果返回。
|
||
|
||
采用模板 A(控制面):经 ``health_check_control`` 端口(与数据面
|
||
``health_check`` 字段分离)调用 ``ChannelControlService`` 类型化方法,
|
||
走控制面管道五阶段。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
query = SingleHealthQuery(
|
||
channel_type=channel_type,
|
||
account_id=account_id,
|
||
)
|
||
result = await use_cases.health_check_control.getSingleHealth(query, operator=operator)
|
||
raiseOnControlFailure(result)
|
||
return {"success": True, "data": serialize_control_data(result.data)}
|
||
|
||
|
||
@health_router.post(
|
||
"/{channel_type}/{account_id}/probe",
|
||
response_model=dict,
|
||
)
|
||
async def probe_channel(
|
||
channel_type: ChannelType,
|
||
account_id: str,
|
||
request: Request,
|
||
body: ProbeRequest,
|
||
use_cases=Depends(get_channel_use_cases),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""主动深度探测指定渠道(HLT-PROBE)。
|
||
|
||
对应控制面操作 ``health/probe``(由 ``ChannelControlService.probeChannelDeep``
|
||
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
|
||
管理员发起的主动深度探测,通过插件 ``ProbeableAdapter.probeDeep``
|
||
执行深度探测(``connectivity`` 仅测连通性,``full`` 含凭据 / 权限 /
|
||
Webhook 全项检查)。渠道未实现 ``probeDeep`` 时控制面返回 501。
|
||
|
||
编排链路:HTTP 入参 → ``build_operator`` 构造操作人 → 构造 ``ProbeCmd``
|
||
→ ``use_cases.health_check_control.probeChannelDeep`` 调用控制面端口方法
|
||
→ ``ControlResult`` 经 ``raiseOnControlFailure`` 转译失败 →
|
||
``serialize_control_data`` 序列化探测结果返回。
|
||
|
||
采用模板 A(控制面):经 ``health_check_control`` 端口调用
|
||
``ChannelControlService`` 类型化方法,走控制面管道五阶段。探测可触发
|
||
副作用(调用渠道 API),审计事务策略 INDEPENDENT(best-effort)。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
cmd = ProbeCmd(
|
||
channel_type=channel_type,
|
||
account_id=account_id,
|
||
operator=operator,
|
||
probe_type=body.probe_type,
|
||
timeout_ms=body.timeout_ms,
|
||
)
|
||
result = await use_cases.health_check_control.probeChannelDeep(cmd)
|
||
raiseOnControlFailure(result)
|
||
return {"success": True, "data": serialize_control_data(result.data)}
|