2026-06-20 22:16:07 +08:00
|
|
|
|
"""Trash 子域 Router。
|
|
|
|
|
|
|
|
|
|
|
|
外部系统限界上下文的回收站管理 API,覆盖回收站列表查询 / 恢复软删除资源 /
|
|
|
|
|
|
物理删除 / 清理过期回收站。所有端点通过 ``create_use_cases_from_db`` 装配
|
|
|
|
|
|
use_cases,经 ``trash_service`` 端口调用用例。
|
|
|
|
|
|
|
2026-07-11 21:39:05 +08:00
|
|
|
|
鉴权分层(与前端权限保持一致):
|
|
|
|
|
|
- 查询(``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 领域异常,由全局异常处理器统一处理。
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
|
|
|
|
|
路径顺序(关键,来自 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 (
|
2026-07-11 21:39:05 +08:00
|
|
|
|
BatchHardDeleteInput,
|
|
|
|
|
|
BatchItemInput,
|
|
|
|
|
|
BatchRestoreInput,
|
2026-06-20 22:16:07 +08:00
|
|
|
|
HardDeleteInput,
|
|
|
|
|
|
ListTrashInput,
|
|
|
|
|
|
PurgeTrashInput,
|
|
|
|
|
|
RestoreInput,
|
|
|
|
|
|
)
|
|
|
|
|
|
from yuxi.storage.postgres.models_business import User
|
|
|
|
|
|
|
2026-07-11 21:39:05 +08:00
|
|
|
|
from server.utils.auth_middleware import get_admin_user, get_db, get_superadmin_user
|
2026-06-20 22:16:07 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-11 21:39:05 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-20 22:16:07 +08:00
|
|
|
|
# ---------------- 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 21:39:05 +08:00
|
|
|
|
start_time: datetime | None = Query(None, description="仅明细模式生效(需传 resource_type),按 deleted_at 过滤下界"),
|
|
|
|
|
|
end_time: datetime | None = Query(None, description="仅明细模式生效(需传 resource_type),按 deleted_at 过滤上界"),
|
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),
|
2026-07-11 21:39:05 +08:00
|
|
|
|
current_user: User = Depends(get_superadmin_user),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""清理过期回收站(物理删除超过保留期的软删除资源)。
|
|
|
|
|
|
|
2026-07-11 21:39:05 +08:00
|
|
|
|
``purged_by`` 由当前管理员填充。物理删除不可恢复,仅 superadmin 可执行。
|
2026-06-20 22:16:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
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()}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-11 21:39:05 +08:00
|
|
|
|
@trash_router.post("/batch/restore", response_model=dict)
|
|
|
|
|
|
async def batch_restore_resources(
|
|
|
|
|
|
payload: BatchOperationRequest,
|
|
|
|
|
|
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 = 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,
|
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
|
current_user: User = Depends(get_superadmin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""批量物理删除资源(不可恢复,仅 superadmin)。
|
|
|
|
|
|
|
|
|
|
|
|
每条独立事务,已知领域异常记录为失败项,未预期异常冒泡。
|
|
|
|
|
|
``deleted_by`` 由当前管理员填充。
|
|
|
|
|
|
"""
|
|
|
|
|
|
use_cases = create_use_cases_from_db(db)
|
|
|
|
|
|
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()}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-20 22:16:07 +08:00
|
|
|
|
@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),
|
2026-07-11 21:39:05 +08:00
|
|
|
|
current_user: User = Depends(get_superadmin_user),
|
2026-06-20 22:16:07 +08:00
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""物理删除回收站中的资源(不可恢复)。
|
|
|
|
|
|
|
2026-07-11 21:39:05 +08:00
|
|
|
|
``deleted_by`` 由当前管理员填充。物理删除不可恢复,仅 superadmin 可执行。
|
2026-06-20 22:16:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
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()}
|