本次提交对多个外部系统路由进行了多维度优化: 1. 统一分页参数:将所有路由的`page = offset//limit +1`、`page_size=limit`替换为标准的`limit`+`offset`分页格式 2. 完善接口文档:补充多个端点的功能说明、参数含义与返回字段解释 3. 增强参数校验:新增字段长度限制、正则校验、枚举类型约束与业务逻辑校验 4. 优化代码复用:提取重复逻辑为辅助函数,减少样板代码 5. 修复接口问题:修正工具健康检查端点路径参数类型,优化导出接口响应格式 6. 补充异常处理:为批量操作添加异常捕获与日志记录,避免流程中断
170 lines
6.0 KiB
Python
170 lines
6.0 KiB
Python
"""Trash 子域 Router。
|
||
|
||
外部系统限界上下文的回收站管理 API,覆盖回收站列表查询 / 恢复软删除资源 /
|
||
物理删除 / 清理过期回收站。所有端点通过 ``create_use_cases_from_db`` 装配
|
||
use_cases,经 ``trash_service`` 端口调用用例。
|
||
|
||
回收站为高危操作,所有端点使用 ``get_admin_user`` 鉴权。Request Schema 与
|
||
Input DTO 不共享类,Router 内显式构造 DTO,操作者字段(``restored_by`` /
|
||
``deleted_by`` / ``purged_by``)由 ``current_user.uid`` 填充。Router 内不
|
||
try/except 领域异常,由全局异常处理器统一处理。
|
||
|
||
路径顺序(关键,来自 FastAPI 路由匹配规则):静态路径端点(``GET ""`` /
|
||
``POST "/purge"``)必须在动态路径端点(``POST "/{resource_type}/{resource_id}/restore"``
|
||
/ ``DELETE "/{resource_type}/{resource_id}"``)之前声明。
|
||
|
||
``ResourceType`` 枚举值与 ``TrashService._RESOURCE_REPO_MAP`` 的 key 对齐,
|
||
在边界层拦截非法资源类型(FastAPI 自动返回 422),同时在 OpenAPI 文档中
|
||
暴露支持的资源类型列表。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from enum import StrEnum
|
||
from typing import Any
|
||
|
||
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
|
||
from yuxi.external_systems.use_cases.dto.trash import (
|
||
HardDeleteInput,
|
||
ListTrashInput,
|
||
PurgeTrashInput,
|
||
RestoreInput,
|
||
)
|
||
from yuxi.storage.postgres.models_business import User
|
||
|
||
from server.utils.auth_middleware import get_admin_user, get_db
|
||
|
||
trash_router = APIRouter(
|
||
prefix="/trash",
|
||
tags=["external-systems-trash"],
|
||
)
|
||
|
||
|
||
# ---------------- 边界层校验枚举(与 TrashService._RESOURCE_REPO_MAP 对齐) ----------------
|
||
|
||
|
||
class ResourceType(StrEnum):
|
||
"""回收站支持的资源类型。
|
||
|
||
值与 ``TrashService._RESOURCE_REPO_MAP`` 的 key 一一对应,
|
||
新增资源类型时需同步更新此处与 service 层映射。
|
||
"""
|
||
|
||
SYSTEM = "system"
|
||
ENVIRONMENT = "environment"
|
||
TOOL = "tool"
|
||
TOKEN = "token"
|
||
ASSET = "asset"
|
||
WEBHOOK_SUBSCRIPTION = "webhook_subscription"
|
||
QUOTA = "quota"
|
||
NOTIFICATION_CHANNEL = "notification_channel"
|
||
SECRET_ROTATION_POLICY = "secret_rotation_policy"
|
||
ACCESS_RULE = "access_rule"
|
||
TEST_CASE = "test_case"
|
||
|
||
|
||
# ---------------- Request Schemas ----------------
|
||
|
||
|
||
class PurgeTrashRequest(BaseModel):
|
||
"""清理过期回收站请求体。字段对齐 ``PurgeTrashInput``。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
older_than_days: int = Field(default=30, ge=1)
|
||
|
||
|
||
# ---------------- Endpoints ----------------
|
||
|
||
|
||
@trash_router.get("", response_model=dict)
|
||
async def list_trash(
|
||
resource_type: ResourceType | None = Query(None, description="资源类型过滤;不传时返回各类型计数摘要"),
|
||
limit: int = Query(50, ge=1, le=200),
|
||
offset: int = Query(0, ge=0),
|
||
start_time: datetime | None = Query(None, description="仅明细模式生效(需传 resource_type)"),
|
||
end_time: datetime | None = Query(None, description="仅明细模式生效(需传 resource_type)"),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""查询回收站列表。
|
||
|
||
- 不传 ``resource_type``:返回各类型计数摘要(``by_type``),``items`` 为空
|
||
- 传 ``resource_type``:返回指定类型的软删除记录明细,``start_time``/``end_time`` 生效
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListTrashInput(
|
||
resource_type=resource_type.value if resource_type is not None else None,
|
||
limit=limit,
|
||
offset=offset,
|
||
start_time=start_time,
|
||
end_time=end_time,
|
||
)
|
||
output = await use_cases.trash_service.list_trash(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@trash_router.post("/purge", response_model=dict)
|
||
async def purge_trash(
|
||
payload: PurgeTrashRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""清理过期回收站(物理删除超过保留期的软删除资源)。
|
||
|
||
``purged_by`` 由当前管理员填充。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = PurgeTrashInput(
|
||
older_than_days=payload.older_than_days,
|
||
purged_by=current_user.uid,
|
||
)
|
||
output = await use_cases.trash_service.purge(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@trash_router.post("/{resource_type}/{resource_id}/restore", response_model=dict)
|
||
async def restore_resource(
|
||
resource_type: ResourceType,
|
||
resource_id: int = Path(..., ge=1),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""恢复软删除资源。
|
||
|
||
``restored_by`` 由当前管理员填充。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = RestoreInput(
|
||
resource_type=resource_type.value,
|
||
resource_id=resource_id,
|
||
restored_by=current_user.uid,
|
||
)
|
||
output = await use_cases.trash_service.restore(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@trash_router.delete("/{resource_type}/{resource_id}", response_model=dict)
|
||
async def hard_delete_resource(
|
||
resource_type: ResourceType,
|
||
resource_id: int = Path(..., ge=1),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""物理删除回收站中的资源(不可恢复)。
|
||
|
||
``deleted_by`` 由当前管理员填充。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = HardDeleteInput(
|
||
resource_type=resource_type.value,
|
||
resource_id=resource_id,
|
||
deleted_by=current_user.uid,
|
||
)
|
||
output = await use_cases.trash_service.hard_delete(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|