本次提交包含多类代码优化: 1. 修复多处单行代码换行格式,统一代码排版 2. 为外部系统模块新增快捷菜单配置 3. 优化前端API请求参数命名一致性 4. 补充媒体下载超限错误码与领域异常类 5. 完善仓储层更新逻辑,支持显式清空字段 6. 优化部分测试用例与工具函数代码结构 7. 为微信插件白名单缓存增加过期时间
240 lines
8.6 KiB
Python
240 lines
8.6 KiB
Python
"""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()}
|