2026-06-20 22:16:07 +08:00
|
|
|
|
"""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}"``)之前声明。
|
2026-07-11 06:57:24 +08:00
|
|
|
|
|
|
|
|
|
|
``ResourceType`` 枚举值与 ``TrashService._RESOURCE_REPO_MAP`` 的 key 对齐,
|
|
|
|
|
|
在边界层拦截非法资源类型(FastAPI 自动返回 422),同时在 OpenAPI 文档中
|
|
|
|
|
|
暴露支持的资源类型列表。
|
2026-06-20 22:16:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from datetime import datetime
|
2026-07-11 06:57:24 +08:00
|
|
|
|
from enum import StrEnum
|
2026-06-20 22:16:07 +08:00
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
2026-07-11 06:57:24 +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.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"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-11 06:57:24 +08:00
|
|
|
|
# ---------------- 边界层校验枚举(与 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"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-20 22:16:07 +08:00
|
|
|
|
# ---------------- 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(
|
2026-07-11 06:57:24 +08:00
|
|
|
|
resource_type: ResourceType | None = Query(None, description="资源类型过滤;不传时返回各类型计数摘要"),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
limit: int = Query(50, ge=1, le=200),
|
|
|
|
|
|
offset: int = Query(0, ge=0),
|
2026-07-11 06:57:24 +08:00
|
|
|
|
start_time: datetime | None = Query(None, description="仅明细模式生效(需传 resource_type)"),
|
|
|
|
|
|
end_time: datetime | None = Query(None, description="仅明细模式生效(需传 resource_type)"),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
2026-07-11 06:57:24 +08:00
|
|
|
|
"""查询回收站列表。
|
|
|
|
|
|
|
|
|
|
|
|
- 不传 ``resource_type``:返回各类型计数摘要(``by_type``),``items`` 为空
|
|
|
|
|
|
- 传 ``resource_type``:返回指定类型的软删除记录明细,``start_time``/``end_time`` 生效
|
|
|
|
|
|
"""
|
2026-06-20 22:16:07 +08:00
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = ListTrashInput(
|
2026-07-11 06:57:24 +08:00
|
|
|
|
resource_type=resource_type.value if resource_type is not None else None,
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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(
|
2026-07-11 06:57:24 +08:00
|
|
|
|
resource_type: ResourceType,
|
|
|
|
|
|
resource_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]:
|
|
|
|
|
|
"""恢复软删除资源。
|
|
|
|
|
|
|
|
|
|
|
|
``restored_by`` 由当前管理员填充。
|
|
|
|
|
|
"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = RestoreInput(
|
2026-07-11 06:57:24 +08:00
|
|
|
|
resource_type=resource_type.value,
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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(
|
2026-07-11 06:57:24 +08:00
|
|
|
|
resource_type: ResourceType,
|
|
|
|
|
|
resource_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]:
|
|
|
|
|
|
"""物理删除回收站中的资源(不可恢复)。
|
|
|
|
|
|
|
|
|
|
|
|
``deleted_by`` 由当前管理员填充。
|
|
|
|
|
|
"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
input_dto = HardDeleteInput(
|
2026-07-11 06:57:24 +08:00
|
|
|
|
resource_type=resource_type.value,
|
2026-06-20 22:16:07 +08:00
|
|
|
|
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()}
|