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

240 lines
8.6 KiB
Python
Raw Normal View History

"""Trash 子域 Router。
外部系统限界上下文的回收站管理 API覆盖回收站列表查询 / 恢复软删除资源 /
物理删除 / 清理过期回收站所有端点通过 ``create_use_cases_from_db`` 装配
use_cases ``trash_service`` 端口调用用例
鉴权分层与前端权限保持一致
- 查询``list_trash``与恢复``restore``使用 ``get_admin_user``admin + superadmin
- 物理删除``hard_delete``与清理``purge``使用 ``get_superadmin_user`` superadmin
因物理删除不可恢复前端同样仅对 superadmin 开放物理删除按钮
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 enum import StrEnum
from typing import Any
from fastapi import APIRouter, Depends, Path, Query
from pydantic import BaseModel, ConfigDict, Field
from yuxi.external_systems.infrastructure.container import UseCases
from yuxi.external_systems.use_cases.dto.trash import (
BatchHardDeleteInput,
BatchItemInput,
BatchRestoreInput,
HardDeleteInput,
ListTrashInput,
PurgeTrashInput,
RestoreInput,
)
from yuxi.storage.postgres.models_business import User
from server.routers.external_systems import get_use_cases, parse_and_naive, validate_time_range
from server.utils.auth_middleware import get_admin_user, get_superadmin_user
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)
class BatchItemRequest(BaseModel):
"""批量操作的单条资源项请求体。"""
model_config = ConfigDict(frozen=True)
resource_type: ResourceType
resource_id: int = Field(ge=1)
class BatchOperationRequest(BaseModel):
"""批量操作请求体。字段对齐 ``BatchRestoreInput`` / ``BatchHardDeleteInput``。"""
model_config = ConfigDict(frozen=True)
items: list[BatchItemRequest] = Field(min_length=1, max_length=100)
# ---------------- 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: str | None = Query(
None, description="仅明细模式生效(需传 resource_type按 deleted_at 过滤下界ISO 8601"
),
end_time: str | None = Query(
None, description="仅明细模式生效(需传 resource_type按 deleted_at 过滤上界ISO 8601"
),
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询回收站列表。
- 不传 ``resource_type``返回各类型计数摘要``by_type````items`` 为空
- ``resource_type``返回指定类型的软删除记录明细``start_time``/``end_time`` 生效
"""
start_dt = parse_and_naive("start_time", start_time)
end_dt = parse_and_naive("end_time", end_time)
validate_time_range(start_dt, end_dt)
input_dto = ListTrashInput(
resource_type=resource_type.value if resource_type is not None else None,
limit=limit,
offset=offset,
start_time=start_dt,
end_time=end_dt,
)
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,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_superadmin_user),
) -> dict[str, Any]:
"""清理过期回收站(物理删除超过保留期的软删除资源)。
``purged_by`` 由当前管理员填充物理删除不可恢复 superadmin 可执行
"""
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("/batch/restore", response_model=dict)
async def batch_restore_resources(
payload: BatchOperationRequest,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量恢复软删除资源。
每条独立事务已知领域异常记录为失败项未预期异常冒泡
``restored_by`` 由当前管理员填充
"""
input_dto = BatchRestoreInput(
items=[
BatchItemInput(resource_type=item.resource_type.value, resource_id=item.resource_id)
for item in payload.items
],
restored_by=current_user.uid,
)
output = await use_cases.trash_service.batch_restore(input_dto)
return {"success": True, "data": output.model_dump()}
@trash_router.post("/batch/hard-delete", response_model=dict)
async def batch_hard_delete_resources(
payload: BatchOperationRequest,
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_superadmin_user),
) -> dict[str, Any]:
"""批量物理删除资源(不可恢复,仅 superadmin
每条独立事务已知领域异常记录为失败项未预期异常冒泡
``deleted_by`` 由当前管理员填充
"""
input_dto = BatchHardDeleteInput(
items=[
BatchItemInput(resource_type=item.resource_type.value, resource_id=item.resource_id)
for item in payload.items
],
deleted_by=current_user.uid,
)
output = await use_cases.trash_service.batch_hard_delete(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),
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""恢复软删除资源。
``restored_by`` 由当前管理员填充
"""
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),
use_cases: UseCases = Depends(get_use_cases),
current_user: User = Depends(get_superadmin_user),
) -> dict[str, Any]:
"""物理删除回收站中的资源(不可恢复)。
``deleted_by`` 由当前管理员填充物理删除不可恢复 superadmin 可执行
"""
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()}