feat(api): 实验归档支持批量删除(含缺失项如实回报)
「实验对比」页需要批量删除,逐个 DELETE 会有 N 次往返且中途失败会留下半删除状态。
- `POST /experiments/bulk-delete`:一次最多 200 个 id(`BULK_DELETE_MAX_IDS`),
按请求顺序去重;返回 `deleted` / `missing` / `count`,**存在的删掉、不存在的如实列出**,
不假装全部成功(前端据此提示「N 个已删、M 个不存在」)。
- 路由声明在 `GET /{experiment_id}` 之前,避免被路径参数吞掉。
- 测试 +2:删除与缺失混合场景、id 列表校验(空/超长)。
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Experiment API(Phase 4):列表 / 详情 / 删除 / 一键复跑。"""
|
||||
"""Experiment API(Phase 4):列表 / 详情 / 删除(单个 + 批量)/ 一键复跑。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -7,6 +7,7 @@ from datetime import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException, Query, Response
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.api.deps import DbSession, ExperimentRepoDep, JobRepoDep
|
||||
from app.application.services.job_executor import new_id, run_job_background
|
||||
@@ -25,6 +26,20 @@ router = APIRouter(prefix="/experiments", tags=["experiments"])
|
||||
LIST_DEFAULT_LIMIT = 200
|
||||
LIST_MAX_LIMIT = 1000
|
||||
|
||||
# 单次批量删除的 id 上限:够覆盖列表一页(默认 200 条),又不至于让一次请求
|
||||
# 把整张表拖进内存。超了直接 422(附上限值),不静默截断成前 N 个。
|
||||
BULK_DELETE_MAX_IDS = 200
|
||||
|
||||
|
||||
class BulkDeleteRequest(BaseModel):
|
||||
"""批量删除请求体(**必填** id 列表,1~200 个)。"""
|
||||
|
||||
ids: list[str] = Field(
|
||||
min_length=1,
|
||||
max_length=BULK_DELETE_MAX_IDS,
|
||||
description=f"要删除的归档 id,1~{BULK_DELETE_MAX_IDS} 个(重复 id 自动去重)",
|
||||
)
|
||||
|
||||
|
||||
def _experiment_meta(exp: ExperimentRecord | ExperimentSummary) -> dict:
|
||||
"""列表项视图(body 形状与旧版一致,仅**新增** data_version / job_id / result_bytes)。
|
||||
@@ -85,6 +100,29 @@ def list_experiments(
|
||||
return [_experiment_meta(e) for e in rows]
|
||||
|
||||
|
||||
@router.post("/bulk-delete", summary=f"批量删除归档(1~{BULK_DELETE_MAX_IDS} 个,逐个回报)")
|
||||
def bulk_delete_experiments(
|
||||
body: BulkDeleteRequest,
|
||||
session: DbSession,
|
||||
experiment_repo: ExperimentRepoDep,
|
||||
) -> dict:
|
||||
"""批量删除归档,返回 `{"deleted": [...], "missing": [...], "count": n}`。
|
||||
|
||||
语义与单个删除完全一致(只删 experiment 行,job 历史保留),另外:
|
||||
- **重复 id 先按出现顺序去重**(同一个 id 报两次没有意义,也不该算两次成功);
|
||||
- **不静默跳过**:库里没有的 id 单独放进 `missing`,让界面能如实说
|
||||
「删了 3 个,2 个没找到(可能已被别处删掉)」——把缺失当成功会更难排查;
|
||||
- 一次请求内的 id 上限 `BULK_DELETE_MAX_IDS`,超了 Pydantic 直接 422 并带上限值。
|
||||
"""
|
||||
ids = list(dict.fromkeys(body.ids))
|
||||
deleted: list[str] = []
|
||||
missing: list[str] = []
|
||||
for experiment_id in ids:
|
||||
(deleted if experiment_repo.delete(experiment_id) else missing).append(experiment_id)
|
||||
session.commit()
|
||||
return {"deleted": deleted, "missing": missing, "count": len(deleted)}
|
||||
|
||||
|
||||
@router.get("/{experiment_id}", summary="Experiment 详情(含完整结果)")
|
||||
def get_experiment(experiment_id: str, experiment_repo: ExperimentRepoDep) -> dict:
|
||||
exp = experiment_repo.get(experiment_id)
|
||||
|
||||
Reference in New Issue
Block a user