2026-06-20 22:16:07 +08:00
|
|
|
|
"""Quota 子域 Router。
|
|
|
|
|
|
|
|
|
|
|
|
外部系统限界上下文的配额管理 API,覆盖配额列表 / 详情 / 创建 / 更新 /
|
|
|
|
|
|
阈值查询 / 阈值设置 / 删除,以及扩展能力:按业务键查询 / 接近告警阈值列表 /
|
|
|
|
|
|
接近临界阈值列表 / 即将重置列表 / 统计聚合 / 重置窗口 / 批量调整 /
|
|
|
|
|
|
创建或更新 / 按系统批量删除。所有端点通过 ``create_use_cases_from_db`` 装配
|
|
|
|
|
|
use_cases,经 ``quota_service`` 端口调用用例。
|
|
|
|
|
|
|
|
|
|
|
|
Request Schema 与 Input DTO 不共享类,Router 内显式构造 DTO。
|
|
|
|
|
|
|
|
|
|
|
|
路径顺序约束:静态路径(``/thresholds`` / ``/by-key`` / ``/near-warning`` /
|
|
|
|
|
|
``/near-critical`` / ``/expiring`` / ``/stats`` / ``/batch-adjust`` /
|
|
|
|
|
|
``/upsert`` / ``/by-system/{system_id}``)必须在 ``/{quota_id}`` 前声明,
|
|
|
|
|
|
避免被路径参数捕获。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
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-06-20 22:16:07 +08:00
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
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.quota import (
|
|
|
|
|
|
BatchAdjustQuotaInput,
|
|
|
|
|
|
CreateQuotaInput,
|
|
|
|
|
|
DeleteQuotaInput,
|
|
|
|
|
|
DeleteQuotasBySystemInput,
|
|
|
|
|
|
GetQuotaByKeyInput,
|
|
|
|
|
|
GetQuotaInput,
|
|
|
|
|
|
GetQuotaStatsInput,
|
|
|
|
|
|
GetQuotaThresholdInput,
|
|
|
|
|
|
ListExpiringInput,
|
|
|
|
|
|
ListNearCriticalInput,
|
|
|
|
|
|
ListNearWarningInput,
|
|
|
|
|
|
ListQuotasInput,
|
|
|
|
|
|
QuotaAdjustmentItem,
|
|
|
|
|
|
ResetQuotaInput,
|
|
|
|
|
|
SetQuotaThresholdInput,
|
|
|
|
|
|
UpdateQuotaInput,
|
|
|
|
|
|
UpsertQuotaInput,
|
|
|
|
|
|
)
|
|
|
|
|
|
from yuxi.storage.postgres.models_business import User
|
|
|
|
|
|
|
|
|
|
|
|
from server.utils.auth_middleware import get_admin_user, get_db, get_required_user
|
|
|
|
|
|
|
|
|
|
|
|
quota_router = APIRouter(prefix="/quotas", tags=["external-systems-quota"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------- Request Schemas ----------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CreateQuotaRequest(BaseModel):
|
|
|
|
|
|
"""创建配额请求体。字段对齐 ``CreateQuotaInput``。
|
|
|
|
|
|
|
|
|
|
|
|
字符串字段长度约束对齐 ``ExternalSystemQuotaUsage`` ORM 列定义,在边界层
|
|
|
|
|
|
拦截非法输入;``window_start``/``window_end`` 使用 datetime 类型,由
|
|
|
|
|
|
Pydantic 解析 ISO 8601 字符串;阈值字段约束 0-100,与
|
|
|
|
|
|
``SetQuotaThresholdRequest`` 保持一致。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
system_id: int
|
|
|
|
|
|
quota_key: str = Field(..., min_length=1, max_length=128)
|
|
|
|
|
|
quota_name: str = Field(..., min_length=1, max_length=128)
|
|
|
|
|
|
quota_window: str = Field(..., min_length=1, max_length=32)
|
|
|
|
|
|
env_key: str = Field(default="default", min_length=1, max_length=32)
|
|
|
|
|
|
window_start: datetime | None = None
|
|
|
|
|
|
window_end: datetime | None = None
|
|
|
|
|
|
limit_value: int = Field(default=0, ge=0)
|
|
|
|
|
|
used_value: int = Field(default=0, ge=0)
|
|
|
|
|
|
unit: str = Field(default="calls", min_length=1, max_length=32)
|
|
|
|
|
|
source: str = Field(default="config", min_length=1, max_length=32)
|
|
|
|
|
|
source_header_name: str | None = Field(default=None, max_length=128)
|
|
|
|
|
|
warning_threshold: int = Field(default=80, ge=0, le=100)
|
|
|
|
|
|
critical_threshold: int = Field(default=95, ge=0, le=100)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SetQuotaThresholdRequest(BaseModel):
|
|
|
|
|
|
"""设置配额阈值请求体。字段对齐 ``SetQuotaThresholdInput``(不含 id)。
|
|
|
|
|
|
|
|
|
|
|
|
仅透传客户端显式设置的字段。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
warning_threshold: int | None = Field(default=None, ge=0, le=100)
|
|
|
|
|
|
critical_threshold: int | None = Field(default=None, ge=0, le=100)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpdateQuotaRequest(BaseModel):
|
|
|
|
|
|
"""更新配额请求体。字段对齐 ``UpdateQuotaInput``(不含 id)。
|
|
|
|
|
|
|
|
|
|
|
|
所有字段可选,支持部分更新;约束与 ``CreateQuotaRequest`` 保持一致,
|
|
|
|
|
|
在边界层拦截非法输入。仅透传客户端显式设置的字段。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
quota_name: str | None = Field(default=None, min_length=1, max_length=128)
|
|
|
|
|
|
limit_value: int | None = Field(default=None, ge=0)
|
|
|
|
|
|
used_value: int | None = Field(default=None, ge=0)
|
|
|
|
|
|
unit: str | None = Field(default=None, min_length=1, max_length=32)
|
|
|
|
|
|
source: str | None = Field(default=None, min_length=1, max_length=32)
|
|
|
|
|
|
source_header_name: str | None = Field(default=None, max_length=128)
|
|
|
|
|
|
warning_threshold: int | None = Field(default=None, ge=0, le=100)
|
|
|
|
|
|
critical_threshold: int | None = Field(default=None, ge=0, le=100)
|
|
|
|
|
|
window_start: datetime | None = None
|
|
|
|
|
|
window_end: datetime | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ResetQuotaRequest(BaseModel):
|
|
|
|
|
|
"""重置配额窗口请求体。字段对齐 ``ResetQuotaInput``(不含 id)。"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
window_start: datetime | None = None
|
|
|
|
|
|
window_end: datetime | None = None
|
|
|
|
|
|
used_value: int = Field(default=0, ge=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class QuotaAdjustmentItemRequest(BaseModel):
|
|
|
|
|
|
"""批量调整项请求体(对齐 ``QuotaAdjustmentItem`` DTO)。"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
quota_id: int = Field(..., ge=1)
|
|
|
|
|
|
limit_value: int = Field(..., ge=0)
|
|
|
|
|
|
reason: str | None = Field(default=None, max_length=500)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BatchAdjustQuotaRequest(BaseModel):
|
|
|
|
|
|
"""批量调整配额限额请求体。"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
adjustments: list[QuotaAdjustmentItemRequest] = Field(..., min_length=1, max_length=100)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpsertQuotaRequest(BaseModel):
|
|
|
|
|
|
"""创建或更新配额请求体。字段对齐 ``UpsertQuotaInput``。"""
|
|
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
|
|
system_id: int
|
|
|
|
|
|
quota_key: str = Field(..., min_length=1, max_length=128)
|
|
|
|
|
|
quota_name: str = Field(..., min_length=1, max_length=128)
|
|
|
|
|
|
quota_window: str = Field(..., min_length=1, max_length=32)
|
|
|
|
|
|
env_key: str = Field(default="default", min_length=1, max_length=32)
|
|
|
|
|
|
window_start: datetime | None = None
|
|
|
|
|
|
window_end: datetime | None = None
|
|
|
|
|
|
limit_value: int = Field(default=0, ge=0)
|
|
|
|
|
|
used_value: int = Field(default=0, ge=0)
|
|
|
|
|
|
unit: str = Field(default="calls", min_length=1, max_length=32)
|
|
|
|
|
|
source: str = Field(default="config", min_length=1, max_length=32)
|
|
|
|
|
|
source_header_name: str | None = Field(default=None, max_length=128)
|
|
|
|
|
|
warning_threshold: int = Field(default=80, ge=0, le=100)
|
|
|
|
|
|
critical_threshold: int = Field(default=95, ge=0, le=100)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------- Endpoints ----------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 静态路径端点(必须在 /{quota_id} 之前声明) ----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.get("/thresholds", response_model=dict)
|
|
|
|
|
|
async def get_quota_threshold(
|
|
|
|
|
|
system_id: int = Query(...),
|
2026-07-04 00:16:45 +08:00
|
|
|
|
env_key: str | None = Query(None, max_length=32),
|
|
|
|
|
|
quota_key: str | None = Query(None, max_length=128),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_required_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""获取达到告警阈值的配额列表(含临界)。
|
|
|
|
|
|
|
|
|
|
|
|
``system_id`` 为必填过滤维度,对齐 ``GetQuotaThresholdInput`` 契约。
|
|
|
|
|
|
"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = GetQuotaThresholdInput(
|
|
|
|
|
|
system_id=system_id,
|
|
|
|
|
|
env_key=env_key,
|
|
|
|
|
|
quota_key=quota_key,
|
|
|
|
|
|
)
|
|
|
|
|
|
output = await use_cases.quota_service.get_quota_threshold(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.get("/by-key", response_model=dict)
|
|
|
|
|
|
async def get_quota_by_key(
|
|
|
|
|
|
system_id: int = Query(...),
|
2026-07-04 00:16:45 +08:00
|
|
|
|
env_key: str = Query("default", max_length=32),
|
|
|
|
|
|
quota_key: str = Query(..., max_length=128),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_required_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""按业务键查询配额。无数据返回 null。
|
|
|
|
|
|
|
|
|
|
|
|
探测性查询语义:配额可能尚未配置,调用方需自行处理 null。与按 ID
|
|
|
|
|
|
查询(``GET /{quota_id}``,不存在抛 404)的语义不同。
|
|
|
|
|
|
"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = GetQuotaByKeyInput(
|
|
|
|
|
|
system_id=system_id,
|
|
|
|
|
|
env_key=env_key,
|
|
|
|
|
|
quota_key=quota_key,
|
|
|
|
|
|
)
|
|
|
|
|
|
output = await use_cases.quota_service.get_quota_by_key(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump() if output else None}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.get("/near-warning", response_model=dict)
|
|
|
|
|
|
async def list_near_warning(
|
|
|
|
|
|
limit: int = Query(100, ge=1, le=500, description="返回记录数上限"),
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_required_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""列出接近告警阈值的配额(全局,含临界)。
|
|
|
|
|
|
|
|
|
|
|
|
与 ``GET /thresholds`` 的区别:本端点为全局查询(无 ``system_id``
|
|
|
|
|
|
过滤),用于跨系统告警大盘。
|
|
|
|
|
|
"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = ListNearWarningInput(limit=limit)
|
|
|
|
|
|
output = await use_cases.quota_service.list_near_warning(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.get("/near-critical", response_model=dict)
|
|
|
|
|
|
async def list_near_critical(
|
|
|
|
|
|
limit: int = Query(100, ge=1, le=500, description="返回记录数上限"),
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_required_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""列出接近临界阈值的配额(全局,无 ``system_id`` 过滤)。"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = ListNearCriticalInput(limit=limit)
|
|
|
|
|
|
output = await use_cases.quota_service.list_near_critical(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.get("/expiring", response_model=dict)
|
|
|
|
|
|
async def list_expiring(
|
|
|
|
|
|
before: datetime = Query(..., description="ISO8601 截止时间,返回 window_end <= before 的配额"),
|
|
|
|
|
|
system_id: int | None = Query(None, description="按系统过滤(可选)"),
|
|
|
|
|
|
limit: int = Query(100, ge=1, le=500, description="返回记录数上限"),
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_required_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""列出即将重置的配额(``window_end <= before``)。"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = ListExpiringInput(
|
|
|
|
|
|
before=before,
|
|
|
|
|
|
system_id=system_id,
|
|
|
|
|
|
limit=limit,
|
|
|
|
|
|
)
|
|
|
|
|
|
output = await use_cases.quota_service.list_expiring(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.get("/stats", response_model=dict)
|
|
|
|
|
|
async def get_quota_stats(
|
|
|
|
|
|
system_id: int | None = Query(None, description="按系统过滤(可选,不传则全局聚合)"),
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_required_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""配额统计聚合。
|
|
|
|
|
|
|
|
|
|
|
|
所有字段均为精确值,统一复用仓储 ``count`` 方法(支持 ``system_id``
|
|
|
|
|
|
过滤):``total`` / ``by_window`` / ``near_warning_count`` /
|
|
|
|
|
|
``near_critical_count`` / ``expiring_count``(窗口已结束,需重置)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = GetQuotaStatsInput(system_id=system_id)
|
|
|
|
|
|
output = await use_cases.quota_service.get_quota_stats(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.post("/batch-adjust", response_model=dict)
|
|
|
|
|
|
async def batch_adjust_quotas(
|
|
|
|
|
|
payload: BatchAdjustQuotaRequest,
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""批量调整配额限额。``updated_by`` 由 ``current_user.uid`` 填充
|
|
|
|
|
|
(对齐 ``batch_enable_environments``,批量更新操作使用 ``updated_by``)。"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = BatchAdjustQuotaInput(
|
|
|
|
|
|
adjustments=[
|
|
|
|
|
|
QuotaAdjustmentItem(
|
|
|
|
|
|
quota_id=item.quota_id,
|
|
|
|
|
|
limit_value=item.limit_value,
|
|
|
|
|
|
reason=item.reason,
|
|
|
|
|
|
)
|
|
|
|
|
|
for item in payload.adjustments
|
|
|
|
|
|
],
|
|
|
|
|
|
updated_by=current_user.uid,
|
|
|
|
|
|
)
|
|
|
|
|
|
output = await use_cases.quota_service.batch_adjust_quotas(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.post("/upsert", response_model=dict)
|
|
|
|
|
|
async def upsert_quota(
|
|
|
|
|
|
payload: UpsertQuotaRequest,
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""创建或更新配额(基于 ``system_id+env_key+quota_key`` 冲突时更新)。"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = UpsertQuotaInput(**payload.model_dump(), created_by=current_user.uid)
|
|
|
|
|
|
output = await use_cases.quota_service.upsert_quota(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.delete("/by-system/{system_id}", response_model=dict)
|
|
|
|
|
|
async def delete_quotas_by_system(
|
2026-07-04 00:16:45 +08:00
|
|
|
|
system_id: int = Path(ge=1),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""按系统批量软删除配额。"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = DeleteQuotasBySystemInput(system_id=system_id, user=current_user.uid)
|
|
|
|
|
|
output = await use_cases.quota_service.delete_quotas_by_system(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 根路径端点 ----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.get("", response_model=dict)
|
|
|
|
|
|
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="按系统过滤"),
|
2026-07-04 00:16:45 +08:00
|
|
|
|
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"
|
|
|
|
|
|
),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
near_warning: bool = Query(False, description="仅返回接近告警阈值的配额"),
|
|
|
|
|
|
near_critical: bool = Query(False, description="仅返回接近临界阈值的配额"),
|
|
|
|
|
|
expiring_before: datetime | None = Query(None, description="仅返回 window_end <= 该时间的配额"),
|
2026-07-04 00:16:45 +08:00
|
|
|
|
keyword: str | None = Query(None, max_length=128, description="按 quota_name / quota_key 模糊匹配"),
|
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 = ListQuotasInput(
|
|
|
|
|
|
limit=limit,
|
|
|
|
|
|
offset=offset,
|
|
|
|
|
|
system_id=system_id,
|
|
|
|
|
|
env_key=env_key,
|
|
|
|
|
|
quota_key=quota_key,
|
|
|
|
|
|
quota_window=quota_window,
|
|
|
|
|
|
source=source,
|
|
|
|
|
|
near_warning=near_warning,
|
|
|
|
|
|
near_critical=near_critical,
|
|
|
|
|
|
expiring_before=expiring_before,
|
|
|
|
|
|
keyword=keyword,
|
|
|
|
|
|
)
|
|
|
|
|
|
output = await use_cases.quota_service.list_quotas(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.post("", response_model=dict)
|
|
|
|
|
|
async def create_quota(
|
|
|
|
|
|
payload: CreateQuotaRequest,
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""创建配额。``created_by`` 由 ``current_user.uid`` 填充。"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = CreateQuotaInput(**payload.model_dump(), created_by=current_user.uid)
|
|
|
|
|
|
output = await use_cases.quota_service.create_quota(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 动态路径端点 /{quota_id} ----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.get("/{quota_id}", response_model=dict)
|
|
|
|
|
|
async def get_quota(
|
2026-07-04 00:16:45 +08:00
|
|
|
|
quota_id: int = Path(ge=1),
|
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 = GetQuotaInput(id=quota_id)
|
|
|
|
|
|
output = await use_cases.quota_service.get_quota(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.put("/{quota_id}", response_model=dict)
|
|
|
|
|
|
async def update_quota(
|
|
|
|
|
|
payload: UpdateQuotaRequest,
|
2026-07-04 00:16:45 +08:00
|
|
|
|
quota_id: int = Path(ge=1),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""更新配额(部分更新)。仅透传显式设置的字段。``updated_by`` 由 ``current_user.uid`` 填充。"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = UpdateQuotaInput(
|
|
|
|
|
|
id=quota_id,
|
|
|
|
|
|
**payload.model_dump(exclude_unset=True),
|
|
|
|
|
|
updated_by=current_user.uid,
|
|
|
|
|
|
)
|
|
|
|
|
|
output = await use_cases.quota_service.update_quota(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.patch("/{quota_id}/threshold", response_model=dict)
|
|
|
|
|
|
async def set_quota_threshold(
|
|
|
|
|
|
payload: SetQuotaThresholdRequest,
|
2026-07-04 00:16:45 +08:00
|
|
|
|
quota_id: int = Path(ge=1),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""设置配额阈值。仅透传显式设置的字段。``updated_by`` 由 ``current_user.uid`` 填充。"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = SetQuotaThresholdInput(
|
|
|
|
|
|
id=quota_id,
|
|
|
|
|
|
**payload.model_dump(exclude_unset=True),
|
|
|
|
|
|
updated_by=current_user.uid,
|
|
|
|
|
|
)
|
|
|
|
|
|
output = await use_cases.quota_service.set_quota_threshold(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.post("/{quota_id}/reset", response_model=dict)
|
|
|
|
|
|
async def reset_quota(
|
|
|
|
|
|
payload: ResetQuotaRequest,
|
2026-07-04 00:16:45 +08:00
|
|
|
|
quota_id: int = Path(ge=1),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""重置配额窗口。仅透传显式设置的字段。``updated_by`` 由 ``current_user.uid`` 填充。"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = ResetQuotaInput(
|
|
|
|
|
|
id=quota_id,
|
|
|
|
|
|
**payload.model_dump(exclude_unset=True),
|
|
|
|
|
|
updated_by=current_user.uid,
|
|
|
|
|
|
)
|
|
|
|
|
|
output = await use_cases.quota_service.reset_quota(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@quota_router.delete("/{quota_id}", response_model=dict)
|
|
|
|
|
|
async def delete_quota(
|
2026-07-04 00:16:45 +08:00
|
|
|
|
quota_id: int = Path(ge=1),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""删除配额。``user`` 由 ``current_user.uid`` 填充。"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = DeleteQuotaInput(id=quota_id, user=current_user.uid)
|
|
|
|
|
|
output = await use_cases.quota_service.delete_quota(input_dto)
|
|
|
|
|
|
return {"success": True, "data": output.model_dump()}
|