ForcePilot/backend/server/routers/external_systems/trash_router.py

170 lines
6.0 KiB
Python
Raw Normal View History

"""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()}