Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 73 additions & 5 deletions backend/api/v1/overview.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,76 @@
from fastapi import APIRouter
from typing import List, Optional

router = APIRouter(prefix="/overview", tags=["分组概览"])
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import ValidationError
from sqlalchemy.ext.asyncio import AsyncSession

from backend.core.database import get_db
from backend.schemas.common import PageResponse
from backend.schemas.overview import OverviewFilterOptions, OverviewItem, OverviewQuery
from backend.services.overview_service import get_overview_options, list_overview

@router.get("")
async def list_overview():
return {"message": "TODO"}
router = APIRouter(prefix="/overview", tags=["分组执行历史"])


def _nonempty_subtask(subtask: Optional[List[str]]) -> bool:
if not subtask:
return False
for s in subtask:
if s is not None and str(s).strip():
return True
return False


@router.get("/options", response_model=OverviewFilterOptions)
async def get_overview_options_endpoint(db: AsyncSession = Depends(get_db)):
"""分组执行历史筛选项(单表去重)。"""
return await get_overview_options(db)


@router.get("", response_model=PageResponse[OverviewItem])
async def get_overview_list(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
batch: Optional[List[str]] = Query(None),
subtask: Optional[List[str]] = Query(None),
platform: Optional[List[str]] = Query(None),
code_branch: Optional[List[str]] = Query(None),
result: Optional[List[str]] = Query(None),
sort_field: Optional[str] = Query(None),
sort_order: Optional[str] = Query(None),
all_batches: bool = Query(False, description="为 true 时不注入默认最近30批;须配合 subtask"),
db: AsyncSession = Depends(get_db),
):
if all_batches and not _nonempty_subtask(subtask):
raise HTTPException(
status_code=422,
detail="全部分组跨轮次查询时必须指定分组(subtask)",
)
try:
query = OverviewQuery(
page=page,
page_size=page_size,
batch=batch,
subtask=subtask,
platform=platform,
code_branch=code_branch,
result=result,
sort_field=sort_field,
sort_order=sort_order,
all_batches=all_batches,
)
except ValidationError as e:
parts = [str(err.get("msg", "")) for err in e.errors()]
raise HTTPException(
status_code=422,
detail="; ".join(p for p in parts if p) or "参数校验失败",
) from e

rows, total = await list_overview(db, query)
items = [OverviewItem.model_validate(r) for r in rows]
return PageResponse(
items=items,
total=total,
page=page,
page_size=page_size,
)
60 changes: 57 additions & 3 deletions backend/schemas/overview.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,59 @@
from pydantic import BaseModel
from datetime import datetime
from typing import List, Optional

from pydantic import BaseModel, field_validator

class OverviewListResponse(BaseModel):
pass
from backend.schemas.common import PageRequest

OVERVIEW_RESULT_ALLOWED = frozenset({"passed", "failed"})


class OverviewItem(BaseModel):
id: int
batch: Optional[str] = None
subtask: Optional[str] = None
result: Optional[str] = None
case_num: Optional[str] = None
batch_start: Optional[datetime] = None
batch_end: Optional[datetime] = None
reports_url: Optional[str] = None
log_url: Optional[str] = None
screenshot_url: Optional[str] = None
pipeline_url: Optional[str] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
passed_num: Optional[int] = None
failed_num: Optional[int] = None
platform: Optional[str] = None
code_branch: Optional[str] = None

model_config = {"from_attributes": True}


class OverviewQuery(PageRequest):
batch: Optional[List[str]] = None
subtask: Optional[List[str]] = None
platform: Optional[List[str]] = None
code_branch: Optional[List[str]] = None
result: Optional[List[str]] = None
sort_field: Optional[str] = None
sort_order: Optional[str] = None
all_batches: bool = False

@field_validator("result", mode="before")
@classmethod
def validate_result_values(cls, v: Optional[List[str]]) -> Optional[List[str]]:
if not v:
return v
for item in v:
if item is not None and str(item) not in OVERVIEW_RESULT_ALLOWED:
raise ValueError("result 仅允许 passed、failed")
return v


class OverviewFilterOptions(BaseModel):
batch: List[str] = []
subtask: List[str] = []
platform: List[str] = []
code_branch: List[str] = []
result: List[str] = ["passed", "failed"]
118 changes: 116 additions & 2 deletions backend/services/overview_service.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,116 @@
class OverviewService:
pass
from typing import List, Optional, Tuple

from sqlalchemy import cast, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.types import Integer

from backend.models.pipeline_overview import PipelineOverview
from backend.schemas.overview import OverviewFilterOptions, OverviewQuery

po = PipelineOverview

DEFAULT_OVERVIEW_BATCH_LIMIT = 30

ALLOWED_SORT_FIELDS = {
"batch",
"subtask",
"result",
"case_num",
"batch_start",
"batch_end",
"passed_num",
"failed_num",
"platform",
"code_branch",
"created_at",
}


async def list_overview(
db: AsyncSession, query: OverviewQuery
) -> Tuple[List[PipelineOverview], int]:
"""
分组执行历史列表:单表 pipeline_overview。
未选 batch 且非 all_batches 模式时注入最近 30 个不重复 batch(spec/14,与 History 批次数一致)。
"""
eff = query
if not query.all_batches and not query.batch:
default_batches_stmt = (
select(po.batch)
.where(po.batch.is_not(None))
.where(po.batch.like("20%"))
.distinct()
.order_by(po.batch.desc())
.limit(DEFAULT_OVERVIEW_BATCH_LIMIT)
)
default_result = await db.execute(default_batches_stmt)
default_batches = [r[0] for r in default_result.all() if r[0]]
if default_batches:
eff = query.model_copy(update={"batch": default_batches})
else:
return [], 0

stmt = select(po)
if eff.batch:
stmt = stmt.where(po.batch.in_(eff.batch))
if eff.subtask:
stmt = stmt.where(po.subtask.in_(eff.subtask))
if eff.platform:
stmt = stmt.where(po.platform.in_(eff.platform))
if eff.code_branch:
stmt = stmt.where(po.code_branch.in_(eff.code_branch))
if eff.result:
stmt = stmt.where(po.result.in_(eff.result))

count_stmt = select(func.count()).select_from(stmt.subquery())
total = (await db.execute(count_stmt)).scalar() or 0

sort_field = eff.sort_field
sort_order = (eff.sort_order or "").lower()
if (
sort_field
and sort_field in ALLOWED_SORT_FIELDS
and sort_order in ("asc", "desc")
):
if sort_field == "case_num":
sort_col = cast(po.case_num, Integer)
else:
sort_col = getattr(po, sort_field)
if sort_order == "asc":
stmt = stmt.order_by(sort_col.asc())
else:
stmt = stmt.order_by(sort_col.desc())
else:
stmt = stmt.order_by(po.batch.desc(), po.subtask.asc())

stmt = stmt.offset((eff.page - 1) * eff.page_size).limit(eff.page_size)
result = await db.execute(stmt)
rows = result.scalars().all()
return rows, total


async def get_overview_options(db: AsyncSession) -> OverviewFilterOptions:
async def _distinct(column, desc: bool = False, prefix: Optional[str] = None) -> List[str]:
s = (
select(column)
.where(column.is_not(None))
.where(column != "")
)
if prefix is not None:
s = s.where(column.like(prefix + "%"))
s = s.distinct().order_by(column.desc() if desc else column.asc())
r = await db.execute(s)
return [row[0] for row in r.all() if row[0]]

batch = await _distinct(po.batch, desc=True, prefix="20")
subtask = await _distinct(po.subtask)
platform = await _distinct(po.platform)
code_branch = await _distinct(po.code_branch)

return OverviewFilterOptions(
batch=batch,
subtask=subtask,
platform=platform,
code_branch=code_branch,
result=["passed", "failed"],
)
Loading
Loading