1. 为渠道账户ID查询添加最小长度校验,统一分析模块常量引用 2. 新增扫码登录向导端点,完善文档说明 3. 优化配对统计接口,移除无效参数 4. 为出站箱接口添加批量上限与202状态码 5. 新增测试用例、访问规则、配额等模块的查询与校验参数 6. 新增适配器健康批量查询、健康检查触发接口 7. 统一告警、审计日志的错误处理方式 8. 新增插件配置账户ID支持,优化批量操作响应 9. 新增环境健康批量查询、Webhook限流与参数校验 10. 完善会话管理、审计日志的参数与文档说明 11. 修复导入模块的校验错误处理逻辑
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 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 (
|
||
BatchHardDeleteInput,
|
||
BatchItemInput,
|
||
BatchRestoreInput,
|
||
HardDeleteInput,
|
||
ListTrashInput,
|
||
PurgeTrashInput,
|
||
RestoreInput,
|
||
)
|
||
from yuxi.storage.postgres.models_business import User
|
||
|
||
from server.utils.auth_middleware import get_admin_user, get_db, 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: datetime | None = Query(None, description="仅明细模式生效(需传 resource_type),按 deleted_at 过滤下界"),
|
||
end_time: datetime | None = Query(None, description="仅明细模式生效(需传 resource_type),按 deleted_at 过滤上界"),
|
||
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_superadmin_user),
|
||
) -> dict[str, Any]:
|
||
"""清理过期回收站(物理删除超过保留期的软删除资源)。
|
||
|
||
``purged_by`` 由当前管理员填充。物理删除不可恢复,仅 superadmin 可执行。
|
||
"""
|
||
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("/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()}
|
||
|
||
|
||
@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_superadmin_user),
|
||
) -> dict[str, Any]:
|
||
"""物理删除回收站中的资源(不可恢复)。
|
||
|
||
``deleted_by`` 由当前管理员填充。物理删除不可恢复,仅 superadmin 可执行。
|
||
"""
|
||
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()}
|