refactor(routers): 统一完善所有外部系统接口的参数校验和类型约束

1. 为所有查询参数添加max_length长度限制,规范参数输入范围
2. 使用Literal类型替换普通字符串参数,限定合法取值范围
3. 为路径参数添加Path校验,确保ID参数合法有效
4. 优化请求体参数的声明,补充缺失的Body注解和校验规则
5. 统一分页参数的offset/limit使用方式,替换旧的page/page_size模式
This commit is contained in:
Kris 2026-07-04 00:16:45 +08:00
parent e5e9f45411
commit aabde54f2e
14 changed files with 214 additions and 198 deletions

View File

@ -13,7 +13,7 @@ from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends, Path, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.external_systems.infrastructure.container import create_use_cases_from_db
@ -115,7 +115,7 @@ class BatchToggleAccessRulesRequest(BaseModel):
model_config = ConfigDict(frozen=True)
rule_ids: list[int] = Field(min_length=1)
rule_ids: list[int] = Field(..., min_length=1, max_length=100)
enabled: bool
@ -133,7 +133,7 @@ class ReorderAccessRulesRequest(BaseModel):
model_config = ConfigDict(frozen=True)
orders: list[ReorderItem] = Field(min_length=1)
orders: list[ReorderItem] = Field(..., min_length=1, max_length=100)
class UpdateConditionsRequest(BaseModel):
@ -149,7 +149,7 @@ class BatchCloneAccessRulesRequest(BaseModel):
model_config = ConfigDict(frozen=True)
source_rule_ids: list[int] = Field(min_length=1)
source_rule_ids: list[int] = Field(..., min_length=1, max_length=100)
override_config: dict[str, Any] = Field(default_factory=dict)
@ -160,12 +160,12 @@ class BatchCloneAccessRulesRequest(BaseModel):
async def list_access_rules(
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
system_id: int | None = Query(None),
tool_slug: str | None = Query(None),
principal_type: str | None = Query(None),
principal_id: str | None = Query(None),
env_key: str | None = Query(None),
effect: str | None = Query(None),
system_id: int | None = Query(None, ge=1),
tool_slug: str | None = Query(None, max_length=128),
principal_type: str | None = Query(None, max_length=32),
principal_id: str | None = Query(None, max_length=64),
env_key: str | None = Query(None, max_length=32),
effect: Literal["allow", "deny"] | None = Query(None, description="规则效果allow/deny"),
enabled: bool | None = Query(None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
@ -296,7 +296,7 @@ async def batch_clone_access_rules(
@access_rule_router.get("/{rule_id}", response_model=dict)
async def get_access_rule(
rule_id: int,
rule_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -309,8 +309,8 @@ async def get_access_rule(
@access_rule_router.put("/{rule_id}", response_model=dict)
async def update_access_rule(
rule_id: int,
payload: UpdateAccessRuleRequest,
rule_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -327,7 +327,7 @@ async def update_access_rule(
@access_rule_router.delete("/{rule_id}", response_model=dict)
async def delete_access_rule(
rule_id: int,
rule_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -343,7 +343,7 @@ async def delete_access_rule(
@access_rule_router.get("/{rule_id}/conditions", response_model=dict)
async def get_conditions(
rule_id: int,
rule_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -356,8 +356,8 @@ async def get_conditions(
@access_rule_router.put("/{rule_id}/conditions", response_model=dict)
async def update_conditions(
rule_id: int,
payload: UpdateConditionsRequest,
rule_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:

View File

@ -14,9 +14,9 @@ dedup_key 过滤参数。Request Schema 与 Input DTO 不共享类Router 内
from __future__ import annotations
from datetime import datetime
from typing import Any
from typing import Any, Literal
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Body, Depends, Path, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.external_systems.infrastructure.container import create_use_cases_from_db
@ -48,20 +48,20 @@ class FireAlertRequest(BaseModel):
model_config = ConfigDict(frozen=True)
system_id: int | None
alert_type: str
title: str
system_id: int | None = Field(default=None, ge=1)
alert_type: str = Field(..., min_length=1, max_length=64)
title: str = Field(..., min_length=1, max_length=256)
triggered_at: datetime
env_key: str | None = None
severity: str = "warning"
env_key: str | None = Field(default=None, max_length=32)
severity: Literal["info", "warning", "critical", "fatal"] = "warning"
description: str | None = None
detail: dict[str, Any] = Field(default_factory=dict)
resource_type: str | None = None
resource_id: str | None = None
related_execution_id: str | None = None
related_trace_id: str | None = None
resource_type: str | None = Field(default=None, max_length=32)
resource_id: str | None = Field(default=None, max_length=128)
related_execution_id: str | None = Field(default=None, max_length=64)
related_trace_id: str | None = Field(default=None, max_length=64)
metric_snapshot: dict[str, Any] | None = None
dedup_key: str | None = None
dedup_key: str | None = Field(default=None, max_length=256)
class AcknowledgeAlertRequest(BaseModel):
@ -85,7 +85,7 @@ class BatchAcknowledgeAlertsRequest(BaseModel):
model_config = ConfigDict(frozen=True)
alert_ids: list[int]
alert_ids: list[int] = Field(..., min_length=1, max_length=100)
note: str | None = None
@ -94,7 +94,7 @@ class BatchResolveAlertsRequest(BaseModel):
model_config = ConfigDict(frozen=True)
alert_ids: list[int]
alert_ids: list[int] = Field(..., min_length=1, max_length=100)
resolution_note: str | None = None
@ -105,18 +105,22 @@ class BatchResolveAlertsRequest(BaseModel):
async def list_alerts(
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
system_id: int | None = Query(None),
env_key: str | None = Query(None),
alert_type: str | None = Query(None),
severity: str | None = Query(None),
status: str | None = Query(None),
start_at: datetime | None = Query(None, description="ISO8601 开始时间"),
end_at: datetime | None = Query(None, description="ISO8601 结束时间"),
system_id: int | None = Query(None, ge=1),
env_key: str | None = Query(None, max_length=32),
alert_type: str | None = Query(None, max_length=64),
severity: Literal["info", "warning", "critical", "fatal"] | None = Query(
None, description="严重度info/warning/critical/fatal"
),
status: Literal["firing", "acknowledged", "resolved", "suppressed"] | None = Query(
None, description="告警状态firing/acknowledged/resolved/suppressed"
),
start_time: datetime | None = Query(None, description="ISO8601 开始时间"),
end_time: datetime | None = Query(None, description="ISO8601 结束时间"),
# 新增查询参数
resource_type: str | None = Query(None),
resource_id: str | None = Query(None),
related_trace_id: str | None = Query(None),
dedup_key: str | None = Query(None),
resource_type: str | None = Query(None, max_length=32),
resource_id: str | None = Query(None, max_length=128),
related_trace_id: str | None = Query(None, max_length=64),
dedup_key: str | None = Query(None, max_length=256),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -130,8 +134,8 @@ async def list_alerts(
alert_type=alert_type,
severity=severity,
status=status,
start_at=start_at,
end_at=end_at,
start_at=start_time,
end_at=end_time,
resource_type=resource_type,
resource_id=resource_id,
related_trace_id=related_trace_id,
@ -143,7 +147,7 @@ async def list_alerts(
@alert_router.get("/stats", response_model=dict)
async def get_alert_stats(
system_id: int | None = Query(None),
system_id: int | None = Query(None, ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -209,7 +213,7 @@ async def batch_resolve_alerts(
@alert_router.get("/{alert_id}", response_model=dict)
async def get_alert(
alert_id: int,
alert_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -235,8 +239,8 @@ async def fire_alert(
@alert_router.post("/{alert_id}/acknowledge", response_model=dict)
async def acknowledge_alert(
alert_id: int,
payload: AcknowledgeAlertRequest,
alert_id: int = Path(ge=1),
payload: AcknowledgeAlertRequest = Body(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -253,8 +257,8 @@ async def acknowledge_alert(
@alert_router.post("/{alert_id}/resolve", response_model=dict)
async def resolve_alert(
alert_id: int,
payload: ResolveAlertRequest,
alert_id: int = Path(ge=1),
payload: ResolveAlertRequest = Body(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -271,7 +275,7 @@ async def resolve_alert(
@alert_router.get("/{alert_id}/context", response_model=dict)
async def get_alert_context(
alert_id: int,
alert_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -284,7 +288,7 @@ async def get_alert_context(
@alert_router.get("/{alert_id}/notifications", response_model=dict)
async def get_alert_notifications(
alert_id: int,
alert_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:

View File

@ -161,13 +161,13 @@ class ProbeAssetRequest(BaseModel):
async def list_assets(
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
adapter_type: str | None = Query(None),
asset_type: str | None = Query(None),
status: str | None = Query(None),
keyword: str | None = Query(None),
adapter_type: str | None = Query(None, max_length=32),
asset_type: str | None = Query(None, max_length=32),
status: str | None = Query(None, max_length=32),
keyword: str | None = Query(None, max_length=128),
start_time: datetime | None = Query(None, description="创建时间起始UTC ISO 8601"),
end_time: datetime | None = Query(None, description="创建时间结束UTC ISO 8601"),
created_by: str | None = Query(None, description="按创建人过滤"),
created_by: str | None = Query(None, max_length=64, description="按创建人过滤"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -334,7 +334,7 @@ async def probe_asset(
@asset_router.get("/{asset_id}", response_model=dict)
async def get_asset(
asset_id: int,
asset_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -347,8 +347,8 @@ async def get_asset(
@asset_router.put("/{asset_id}", response_model=dict)
async def update_asset(
asset_id: int,
payload: UpdateAssetRequest,
asset_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -369,7 +369,7 @@ async def update_asset(
@asset_router.delete("/{asset_id}", response_model=dict)
async def delete_asset(
asset_id: int,
asset_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -382,7 +382,7 @@ async def delete_asset(
@asset_router.post("/{asset_id}/validate", response_model=dict)
async def validate_asset(
asset_id: int,
asset_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -395,7 +395,7 @@ async def validate_asset(
@asset_router.get("/{asset_id}/preview", response_model=dict)
async def preview_asset(
asset_id: int,
asset_id: int = Path(ge=1),
max_lines: int = Query(200, ge=1, le=10000),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
@ -414,7 +414,7 @@ async def preview_asset(
@asset_router.get("/{asset_id}/generate-preview", response_model=dict)
async def generate_preview(
asset_id: int,
asset_id: int = Path(ge=1),
max_lines: int = Query(200, ge=1, le=10000),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
@ -433,7 +433,7 @@ async def generate_preview(
@asset_router.get("/{asset_id}/references", response_model=dict)
async def get_asset_references(
asset_id: int,
asset_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:

View File

@ -13,7 +13,7 @@ import json
from datetime import UTC, datetime
from typing import Any
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends, Path, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.external_systems.infrastructure.container import create_use_cases_from_db
@ -44,12 +44,12 @@ audit_log_router = APIRouter(
@audit_log_router.get("/export")
async def export_audit_logs(
system_id: int | None = Query(None),
env_key: str | None = Query(None),
event_type: str | None = Query(None),
action_type: str | None = Query(None),
resource: str | None = Query(None),
user: str | None = Query(None),
system_id: int | None = Query(None, ge=1),
env_key: str | None = Query(None, max_length=32),
event_type: str | None = Query(None, max_length=64),
action_type: str | None = Query(None, max_length=64),
resource: str | None = Query(None, max_length=256),
user: str | None = Query(None, max_length=64),
start_time: datetime | None = Query(None),
end_time: datetime | None = Query(None),
db: AsyncSession = Depends(get_db),
@ -95,12 +95,12 @@ async def export_audit_logs(
async def list_audit_logs(
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
system_id: int | None = Query(None),
env_key: str | None = Query(None),
event_type: str | None = Query(None),
action_type: str | None = Query(None),
resource: str | None = Query(None),
user: str | None = Query(None),
system_id: int | None = Query(None, ge=1),
env_key: str | None = Query(None, max_length=32),
event_type: str | None = Query(None, max_length=64),
action_type: str | None = Query(None, max_length=64),
resource: str | None = Query(None, max_length=256),
user: str | None = Query(None, max_length=64),
start_time: datetime | None = Query(None),
end_time: datetime | None = Query(None),
db: AsyncSession = Depends(get_db),
@ -126,7 +126,7 @@ async def list_audit_logs(
@audit_log_router.get("/stats", response_model=dict)
async def get_audit_log_stats(
system_id: int | None = Query(None),
system_id: int | None = Query(None, ge=1),
start_time: datetime | None = Query(None),
end_time: datetime | None = Query(None),
db: AsyncSession = Depends(get_db),
@ -147,11 +147,11 @@ async def get_audit_log_stats(
async def list_config_changes(
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
system_id: int | None = Query(None),
env_key: str | None = Query(None),
action_type: str | None = Query(None),
resource: str | None = Query(None),
user: str | None = Query(None),
system_id: int | None = Query(None, ge=1),
env_key: str | None = Query(None, max_length=32),
action_type: str | None = Query(None, max_length=64),
resource: str | None = Query(None, max_length=256),
user: str | None = Query(None, max_length=64),
start_time: datetime | None = Query(None),
end_time: datetime | None = Query(None),
db: AsyncSession = Depends(get_db),
@ -220,7 +220,7 @@ async def list_operators(
@audit_log_router.get("/{log_id}", response_model=dict)
async def get_audit_log_detail(
log_id: int,
log_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:

View File

@ -16,7 +16,7 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
from typing import Any, Literal
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
@ -87,10 +87,9 @@ async def get_executions_stats(
@dashboard_router.get("/alerts", response_model=dict)
async def get_alerts_stats(
severity_status: str = Query(
severity_status: Literal["firing", "acknowledged", "resolved", "suppressed"] = Query(
"firing",
description="count_by_severity 的状态过滤",
pattern="^(firing|acknowledged|resolved|suppressed)$",
description="count_by_severity 的状态过滤告警状态firing/acknowledged/resolved/suppressed",
),
top_limit: int = Query(10, ge=1, le=50, description="Top 告警去重键数量"),
db: AsyncSession = Depends(get_db),

View File

@ -53,7 +53,7 @@ class CleanupExecutionsRequest(BaseModel):
system_id: int | None = None
before_at: datetime | None = None
status: str | None = None
status: Literal["pending", "running", "success", "failed", "timeout", "throttled", "cancelled"] | None = None
batch_size: int = Field(default=1000, ge=1, le=10000)
@ -84,17 +84,19 @@ async def cleanup_old_executions(
async def paginate_executions(
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
system_id: int | None = Query(None),
env_key: str | None = Query(None),
tool_slug: str | None = Query(None),
status: str | None = Query(None, description="按状态过滤success/failed/running/timeout"),
caller: str | None = Query(None),
caller_id: str | None = Query(None, description="按调用方实体过滤"),
operation: str | None = Query(None, description="按操作类型过滤"),
tag_key: str | None = Query(None, description="按业务标签键过滤"),
tag_value: str | None = Query(None, description="按业务标签值过滤"),
trace_id: str | None = Query(None),
correlation_id: str | None = Query(None),
system_id: int | None = Query(None, ge=1),
env_key: str | None = Query(None, max_length=32),
tool_slug: str | None = Query(None, max_length=128),
status: Literal["pending", "running", "success", "failed", "timeout", "throttled", "cancelled"] | None = Query(
None, description="执行状态pending/running/success/failed/timeout/throttled/cancelled"
),
caller: str | None = Query(None, max_length=32),
caller_id: str | None = Query(None, max_length=64, description="按调用方实体过滤"),
operation: str | None = Query(None, max_length=32, description="按操作类型过滤"),
tag_key: str | None = Query(None, max_length=64, description="按业务标签键过滤"),
tag_value: str | None = Query(None, max_length=256, description="按业务标签值过滤"),
trace_id: str | None = Query(None, max_length=64),
correlation_id: str | None = Query(None, max_length=64),
start_time: datetime | None = Query(None, description="ISO8601 开始时间"),
end_time: datetime | None = Query(None, description="ISO8601 结束时间"),
db: AsyncSession = Depends(get_db),
@ -130,9 +132,9 @@ async def paginate_executions(
@execution_router.get("/stats", response_model=dict)
async def get_execution_stats(
system_id: int | None = Query(None, description="系统过滤"),
env_key: str | None = Query(None, description="环境过滤"),
adapter_type: str | None = Query(None, description="适配器类型过滤"),
system_id: int | None = Query(None, ge=1, description="系统过滤"),
env_key: str | None = Query(None, max_length=32, description="环境过滤"),
adapter_type: str | None = Query(None, max_length=32, description="适配器类型过滤"),
start_time: datetime | None = Query(None, description="起始时间"),
end_time: datetime | None = Query(None, description="结束时间"),
tool_limit: int = Query(20, ge=1, le=100, description="Top 工具数量"),
@ -156,9 +158,9 @@ async def get_execution_stats(
@execution_router.get("/stats/grouped", response_model=dict)
async def get_grouped_stats(
group_by: Literal["tool"] = Query(..., description="分组维度,仅支持 tool"),
system_id: int | None = Query(None, description="系统过滤"),
env_key: str | None = Query(None, description="环境过滤"),
adapter_type: str | None = Query(None, description="适配器类型过滤"),
system_id: int | None = Query(None, ge=1, description="系统过滤"),
env_key: str | None = Query(None, max_length=32, description="环境过滤"),
adapter_type: str | None = Query(None, max_length=32, description="适配器类型过滤"),
start_time: datetime | None = Query(None, description="起始时间"),
end_time: datetime | None = Query(None, description="结束时间"),
limit: int = Query(20, ge=1, le=100, description="返回数量"),
@ -181,7 +183,7 @@ async def get_grouped_stats(
@execution_router.get("/stats/error-analysis", response_model=dict)
async def get_error_analysis(
system_id: int = Query(..., description="系统 ID必填"),
system_id: int = Query(..., ge=1, description="系统 ID必填"),
start_time: datetime | None = Query(None, description="起始时间"),
end_time: datetime | None = Query(None, description="结束时间"),
limit: int = Query(5, ge=1, le=50, description="返回数量"),
@ -202,9 +204,9 @@ async def get_error_analysis(
@execution_router.get("/stats/trend", response_model=dict)
async def get_execution_trend(
tool_slug: str = Query(..., description="工具标识(必填)"),
system_id: int | None = Query(None, description="系统过滤"),
env_key: str | None = Query(None, description="环境过滤"),
tool_slug: str = Query(..., min_length=1, max_length=128, description="工具标识(必填)"),
system_id: int | None = Query(None, ge=1, description="系统过滤"),
env_key: str | None = Query(None, max_length=32, description="环境过滤"),
start_time: datetime | None = Query(None, description="起始时间"),
end_time: datetime | None = Query(None, description="结束时间"),
interval: Literal["hour", "day"] = Query("day", description="时间桶粒度hour / day"),
@ -227,7 +229,7 @@ async def get_execution_trend(
@execution_router.get("/slow", response_model=dict)
async def list_slow_executions(
system_id: int = Query(..., description="系统 ID必填"),
system_id: int = Query(..., ge=1, description="系统 ID必填"),
min_duration_ms: int = Query(..., ge=0, description="耗时阈值(毫秒,必填)"),
limit: int = Query(50, ge=1, le=200, description="返回数量"),
db: AsyncSession = Depends(get_db),

View File

@ -17,9 +17,9 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
from typing import Any, Literal
from fastapi import APIRouter, Depends, Query
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 (
@ -47,9 +47,11 @@ health_check_router = APIRouter(prefix="/health-checks", tags=["external-systems
@health_check_router.get("/latest", response_model=dict)
async def list_latest_health(
system_ids: list[int] | None = Query(None, description="按系统 ID 过滤(逗号分隔)"),
health_status: str | None = Query(None, description="按健康状态过滤"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
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]:
@ -58,8 +60,8 @@ async def list_latest_health(
input_dto = ListLatestHealthInput(
system_ids=system_ids,
health_status=health_status,
page=page,
page_size=page_size,
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()}
@ -80,10 +82,9 @@ async def count_by_status(
@health_check_router.get("/failing", response_model=dict)
async def list_failing(
health_status: str = Query(
health_status: Literal["auth_failure", "connectivity_issue"] = Query(
"auth_failure",
description="故障状态过滤auth_failure / connectivity_issue",
pattern="^(auth_failure|connectivity_issue)$",
),
system_ids: list[int] | None = Query(None, description="按系统 ID 过滤(逗号分隔)"),
limit: int = Query(100, ge=1, le=500),
@ -132,8 +133,8 @@ async def delete_old_records(
@health_check_router.get("/by-system/{system_id}", response_model=dict)
async def list_by_system(
system_id: int,
env_key: str | None = Query(None, description="按环境过滤"),
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),
@ -151,8 +152,8 @@ async def list_by_system(
@health_check_router.get("/by-system/{system_id}/latest", response_model=dict)
async def get_latest_by_system(
system_id: int,
env_key: str | None = Query(None, description="环境标识(不传则跨 env 取最近一条)"),
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]:

View File

@ -38,9 +38,11 @@ _SUMMARY_EXCLUDED_FIELDS = ("connection_extra_schema", "example_config", "depend
@integration_router.get("", response_model=dict)
async def list_integrations(
adapter_type: str | None = Query(None, description="按适配器类型精确过滤"),
tags: str | None = Query(None, description="按标签过滤多个标签以逗号分隔OR 关系)"),
keyword: str | None = Query(None, description="模糊匹配 display_name / description大小写不敏感"),
adapter_type: str | None = Query(None, max_length=32, description="按适配器类型精确过滤"),
tags: str | None = Query(None, max_length=256, description="按标签过滤多个标签以逗号分隔OR 关系)"),
keyword: str | None = Query(
None, max_length=128, description="模糊匹配 display_name / description大小写不敏感"
),
limit: int = Query(50, ge=1, le=200, description="分页大小,最大 200"),
offset: int = Query(0, ge=0, description="分页偏移"),
current_user: User = Depends(get_required_user),

View File

@ -101,10 +101,10 @@ async def list_generated_tools(
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
system_id: int | None = Query(None),
category: str | None = Query(None),
adapter_type: str | None = Query(None),
category: str | None = Query(None, max_length=64),
adapter_type: str | None = Query(None, max_length=32),
enabled: bool | None = Query(None),
keyword: str | None = Query(None),
keyword: str | None = Query(None, max_length=128),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:

View File

@ -17,7 +17,7 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
from typing import Any, Literal
from fastapi import APIRouter, Depends, Query, Response
from sqlalchemy.ext.asyncio import AsyncSession
@ -48,8 +48,8 @@ metric_router = APIRouter(prefix="/metrics", tags=["external-systems-metric"])
@metric_router.get("/aggregate", response_model=dict)
async def aggregate_metrics(
system_id: int | None = Query(None),
env_key: str | None = Query(None),
adapter_type: str | None = Query(None),
env_key: str | None = Query(None, max_length=32),
adapter_type: str | None = Query(None, max_length=32),
start_time: datetime | None = Query(None),
end_time: datetime | None = Query(None),
db: AsyncSession = Depends(get_db),
@ -71,11 +71,11 @@ async def aggregate_metrics(
@metric_router.get("/timeseries", response_model=dict)
async def get_metrics_timeseries(
system_id: int | None = Query(None),
env_key: str | None = Query(None),
adapter_type: str | None = Query(None),
env_key: str | None = Query(None, max_length=32),
adapter_type: str | None = Query(None, max_length=32),
start_time: datetime = Query(..., description="起始时间(必填)"),
end_time: datetime = Query(..., description="结束时间(必填)"),
interval: str = Query("hour", pattern="^(hour|day)$", description="聚合间隔hour / day"),
interval: Literal["hour", "day"] = Query("hour", description="聚合间隔hour / day"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -95,8 +95,8 @@ async def get_metrics_timeseries(
@metric_router.get("/by-system", response_model=dict)
async def get_metrics_by_system(
env_key: str | None = Query(None),
adapter_type: str | None = Query(None),
env_key: str | None = Query(None, max_length=32),
adapter_type: str | None = Query(None, max_length=32),
start_time: datetime = Query(..., description="起始时间(必填)"),
end_time: datetime = Query(..., description="结束时间(必填)"),
limit: int = Query(20, ge=1, le=100),
@ -138,7 +138,7 @@ async def get_metrics_by_env(
@metric_router.get("/latest", response_model=dict)
async def get_latest_metric_bucket(
system_id: int = Query(..., description="系统 ID必填"),
env_key: str = Query("default", description="环境标识"),
env_key: str = Query("default", max_length=32, description="环境标识"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -153,7 +153,7 @@ async def get_latest_metric_bucket(
async def export_metrics_prometheus(
start_time: datetime | None = Query(None),
end_time: datetime | None = Query(None),
env_key: str | None = Query(None),
env_key: str | None = Query(None, max_length=32),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> Response:
@ -183,7 +183,7 @@ async def delete_old_metric_buckets(
@metric_router.get("", response_model=dict)
async def list_metric_buckets(
system_id: int | None = Query(None),
env_key: str | None = Query(None),
env_key: str | None = Query(None, max_length=32),
start_time: datetime | None = Query(None),
end_time: datetime | None = Query(None),
limit: int = Query(20, ge=1, le=100),

View File

@ -17,7 +17,7 @@ from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends, Path, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.external_systems.infrastructure.container import create_use_cases_from_db
@ -79,9 +79,9 @@ class UpdateNotificationChannelRequest(BaseModel):
async def list_notification_channels(
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
channel_type: str | None = Query(None),
channel_type: str | None = Query(None, max_length=32),
enabled: bool | None = Query(None),
keyword: str | None = Query(None),
keyword: str | None = Query(None, max_length=128),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -112,7 +112,7 @@ async def get_notification_channel_stats(
@notification_channel_router.get("/{channel_id}", response_model=dict)
async def get_notification_channel(
channel_id: int,
channel_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -145,8 +145,8 @@ async def create_notification_channel(
@notification_channel_router.put("/{channel_id}", response_model=dict)
async def update_notification_channel(
channel_id: int,
payload: UpdateNotificationChannelRequest,
channel_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -163,7 +163,7 @@ async def update_notification_channel(
@notification_channel_router.delete("/{channel_id}", response_model=dict)
async def delete_notification_channel(
channel_id: int,
channel_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:

View File

@ -17,9 +17,9 @@ Request Schema 与 Input DTO 不共享类Router 内显式构造 DTO。
from __future__ import annotations
from datetime import datetime
from typing import Any
from typing import Any, Literal
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends, Path, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.external_systems.infrastructure.container import create_use_cases_from_db
@ -170,8 +170,8 @@ class UpsertQuotaRequest(BaseModel):
@quota_router.get("/thresholds", response_model=dict)
async def get_quota_threshold(
system_id: int = Query(...),
env_key: str | None = Query(None),
quota_key: str | None = Query(None),
env_key: str | None = Query(None, max_length=32),
quota_key: str | None = Query(None, max_length=128),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -192,8 +192,8 @@ async def get_quota_threshold(
@quota_router.get("/by-key", response_model=dict)
async def get_quota_by_key(
system_id: int = Query(...),
env_key: str = Query("default"),
quota_key: str = Query(...),
env_key: str = Query("default", max_length=32),
quota_key: str = Query(..., max_length=128),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -318,7 +318,7 @@ async def upsert_quota(
@quota_router.delete("/by-system/{system_id}", response_model=dict)
async def delete_quotas_by_system(
system_id: int,
system_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -337,14 +337,18 @@ async def list_quotas(
limit: int = Query(100, ge=1, le=500, description="返回记录数上限"),
offset: int = Query(0, ge=0, description="偏移量(与 limit 配合分页)"),
system_id: int | None = Query(None, description="按系统过滤"),
env_key: str | None = Query(None, description="按环境键过滤"),
quota_key: str | None = Query(None, description="按配额键过滤"),
quota_window: str | None = Query(None, description="按窗口类型过滤day/month/total 等)"),
source: str | None = Query(None, description="按来源过滤config/response_header 等)"),
env_key: str | None = Query(None, max_length=32, description="按环境键过滤"),
quota_key: str | None = Query(None, max_length=128, description="按配额键过滤"),
quota_window: Literal["daily", "hourly", "rolling_24h", "minute", "custom"] | None = Query(
None, description="窗口类型daily/hourly/rolling_24h/minute/custom"
),
source: Literal["response_header", "manual", "config"] | None = Query(
None, description="配额来源response_header/manual/config"
),
near_warning: bool = Query(False, description="仅返回接近告警阈值的配额"),
near_critical: bool = Query(False, description="仅返回接近临界阈值的配额"),
expiring_before: datetime | None = Query(None, description="仅返回 window_end <= 该时间的配额"),
keyword: str | None = Query(None, description="按 quota_name / quota_key 模糊匹配"),
keyword: str | None = Query(None, max_length=128, description="按 quota_name / quota_key 模糊匹配"),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -385,7 +389,7 @@ async def create_quota(
@quota_router.get("/{quota_id}", response_model=dict)
async def get_quota(
quota_id: int,
quota_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -398,8 +402,8 @@ async def get_quota(
@quota_router.put("/{quota_id}", response_model=dict)
async def update_quota(
quota_id: int,
payload: UpdateQuotaRequest,
quota_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -416,8 +420,8 @@ async def update_quota(
@quota_router.patch("/{quota_id}/threshold", response_model=dict)
async def set_quota_threshold(
quota_id: int,
payload: SetQuotaThresholdRequest,
quota_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -434,8 +438,8 @@ async def set_quota_threshold(
@quota_router.post("/{quota_id}/reset", response_model=dict)
async def reset_quota(
quota_id: int,
payload: ResetQuotaRequest,
quota_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -452,7 +456,7 @@ async def reset_quota(
@quota_router.delete("/{quota_id}", response_model=dict)
async def delete_quota(
quota_id: int,
quota_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:

View File

@ -14,7 +14,7 @@ import json
from datetime import UTC, datetime
from typing import Any
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Body, Depends, Path, Query
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.ext.asyncio import AsyncSession
@ -123,7 +123,7 @@ class ImportTestCasesRequest(BaseModel):
model_config = ConfigDict(frozen=True)
items: list[CreateTestCaseRequest] = Field(..., min_length=1)
items: list[CreateTestCaseRequest] = Field(..., min_length=1, max_length=100)
class UpdateAlertConfigRequest(BaseModel):
@ -142,10 +142,10 @@ class UpdateAlertConfigRequest(BaseModel):
async def list_test_cases(
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
tool_slug: str | None = Query(None),
env_key: str | None = Query(None),
tool_slug: str | None = Query(None, max_length=128),
env_key: str | None = Query(None, max_length=32),
enabled: bool | None = Query(None),
schedule_cron: str | None = Query(None),
schedule_cron: str | None = Query(None, max_length=64),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -269,7 +269,7 @@ async def export_test_cases(
@test_case_router.get("/{case_id}", response_model=dict)
async def get_test_case(
case_id: int,
case_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -282,8 +282,8 @@ async def get_test_case(
@test_case_router.put("/{case_id}", response_model=dict)
async def update_test_case(
case_id: int,
payload: UpdateTestCaseRequest,
case_id: int = Path(ge=1),
payload: UpdateTestCaseRequest = Body(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -300,7 +300,7 @@ async def update_test_case(
@test_case_router.delete("/{case_id}", response_model=dict)
async def delete_test_case(
case_id: int,
case_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -316,7 +316,7 @@ async def delete_test_case(
@test_case_router.post("/{case_id}/run", response_model=dict)
async def run_test_case(
case_id: int,
case_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -332,8 +332,8 @@ async def run_test_case(
@test_case_router.post("/{case_id}/clone", response_model=dict)
async def clone_test_case(
case_id: int,
payload: CloneTestCaseRequest,
case_id: int = Path(ge=1),
payload: CloneTestCaseRequest = Body(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -350,7 +350,7 @@ async def clone_test_case(
@test_case_router.get("/{case_id}/alert-config", response_model=dict)
async def get_alert_config(
case_id: int,
case_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -363,8 +363,8 @@ async def get_alert_config(
@test_case_router.put("/{case_id}/alert-config", response_model=dict)
async def update_alert_config(
case_id: int,
payload: UpdateAlertConfigRequest,
case_id: int = Path(ge=1),
payload: UpdateAlertConfigRequest = Body(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:

View File

@ -18,9 +18,9 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
from typing import Any, Literal
from fastapi import APIRouter, Depends, Header, Query, Request
from fastapi import APIRouter, Body, Depends, Header, Path, Query, Request
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.external_systems.infrastructure.container import create_use_cases_from_db
@ -194,7 +194,9 @@ async def list_subscriptions(
system_id: int | None = Query(None, ge=1),
env_key: str | None = Query(None, max_length=32),
source_event_type: str | None = Query(None, max_length=128),
status: str | None = Query(None, max_length=32),
status: Literal["pending", "active", "paused", "expired", "disabled"] | None = Query(
None, description="订阅状态pending/active/paused/expired/disabled"
),
enabled: bool | None = Query(None),
target_handler_type: str | None = Query(None, max_length=32),
target_handler_id: str | None = Query(None, max_length=64),
@ -276,7 +278,9 @@ async def list_events(
subscription_id: int | None = Query(None, ge=1),
subscription_slug: str | None = Query(None, max_length=128),
event_type: str | None = Query(None, max_length=128),
processing_status: str | None = Query(None, max_length=32),
processing_status: Literal["pending", "processing", "processed", "failed", "ignored", "duplicate"] | None = Query(
None, description="处理状态pending/processing/processed/failed/ignored/duplicate"
),
correlation_id: str | None = Query(None, max_length=128),
triggered_execution_id: str | None = Query(None, max_length=128),
signature_verified: bool | None = Query(None),
@ -369,7 +373,7 @@ async def list_renewal_failures(
@webhook_router.get("/subscriptions/{subscription_id}", response_model=dict)
async def get_subscription(
subscription_id: int,
subscription_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -382,8 +386,8 @@ async def get_subscription(
@webhook_router.put("/subscriptions/{subscription_id}", response_model=dict)
async def update_subscription(
subscription_id: int,
payload: UpdateWebhookSubscriptionRequest,
subscription_id: int = Path(ge=1),
payload: UpdateWebhookSubscriptionRequest = Body(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -400,7 +404,7 @@ async def update_subscription(
@webhook_router.delete("/subscriptions/{subscription_id}", response_model=dict)
async def delete_subscription(
subscription_id: int,
subscription_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -413,7 +417,7 @@ async def delete_subscription(
@webhook_router.post("/subscriptions/{subscription_id}/renew", response_model=dict)
async def renew_subscription(
subscription_id: int,
subscription_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -426,7 +430,7 @@ async def renew_subscription(
@webhook_router.get("/events/{event_id}/deliveries", response_model=dict)
async def get_event_deliveries(
event_id: int,
event_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -439,7 +443,7 @@ async def get_event_deliveries(
@webhook_router.post("/events/{event_id}/replay", response_model=dict)
async def replay_event(
event_id: int,
event_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -452,7 +456,7 @@ async def replay_event(
@webhook_router.get("/subscriptions/{subscription_id}/filter-rules", response_model=dict)
async def get_filter_rules(
subscription_id: int,
subscription_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -465,8 +469,8 @@ async def get_filter_rules(
@webhook_router.put("/subscriptions/{subscription_id}/filter-rules", response_model=dict)
async def update_filter_rules(
subscription_id: int,
payload: UpdateFilterRulesRequest,
subscription_id: int = Path(ge=1),
payload: UpdateFilterRulesRequest = Body(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -483,7 +487,7 @@ async def update_filter_rules(
@webhook_router.get("/subscriptions/{subscription_id}/transform-rules", response_model=dict)
async def get_transform_rules(
subscription_id: int,
subscription_id: int = Path(ge=1),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
@ -496,8 +500,8 @@ async def get_transform_rules(
@webhook_router.put("/subscriptions/{subscription_id}/transform-rules", response_model=dict)
async def update_transform_rules(
subscription_id: int,
payload: UpdateTransformRulesRequest,
subscription_id: int = Path(ge=1),
payload: UpdateTransformRulesRequest = Body(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -514,8 +518,8 @@ async def update_transform_rules(
@webhook_router.post("/subscriptions/{subscription_id}/test", response_model=dict)
async def test_subscription(
subscription_id: int,
payload: TestWebhookSubscriptionRequest,
subscription_id: int = Path(ge=1),
payload: TestWebhookSubscriptionRequest = Body(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
@ -533,8 +537,8 @@ async def test_subscription(
@webhook_router.post("/subscriptions/{subscription_id}/verify-signature", response_model=dict)
async def verify_signature(
subscription_id: int,
payload: VerifySignatureRequest,
subscription_id: int = Path(ge=1),
payload: VerifySignatureRequest = Body(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]: