提交 f38a23f1 authored 作者: 陈泽健's avatar 陈泽健

feat(performance): 批量执行合并报告实现(数据持久化 + 聚合算法 + API)

- PerformanceBatchExecution / PerformanceProjectReport 模型实现(表已建立)
- _aggregate_task_results 聚合算法: 求和/重算错误率与TPS/加权平均响应时间/
  取极值/加权近似分位数(p50/p90/p95/p99)/聚合标准差重算, 排除失败任务
- 批量执行完成后自动生成合并报告并持久化, 返回 report_id
- GET /api/performance/reports 按 project_id/batch_id 查询, ?latest=1 取最新
- ProjectReportResponse/ProjectReportListResponse/TaskSummaryItem schema,
  BatchRunResponse 增加 report_id 字段
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 7f4e853f
......@@ -41,6 +41,8 @@ from app.schemas.performance import (
PerformanceProjectStatsResponse,
BatchRunRequest,
BatchRunResponse,
ProjectReportResponse,
ProjectReportListResponse,
)
from app.services.performance_service import PerformanceService
from app.utils.curl_parser import parse_curl
......@@ -527,6 +529,7 @@ async def batch_run_tasks(
message=f"批量执行完成: {result['success_count']} 成功, {result['fail_count']} 失败",
batch_id=result["batch_id"],
task_count=result["total"],
report_id=result.get("report_id"),
)
......@@ -554,4 +557,52 @@ async def run_project_all(
message=f"项目执行完成: {result['success_count']} 成功, {result['fail_count']} 失败",
batch_id=result["batch_id"],
task_count=result["total"],
report_id=result.get("report_id"),
)
# ==================== 合并报告 ====================
@router.get("/projects/{project_id}/report", response_model=ProjectReportResponse)
async def get_project_report(
project_id: str,
service: PerformanceService = Depends(get_perf_service),
):
"""
获取项目最新合并报告
返回该项目最近一次批量执行生成的合并报告。
"""
report = await service.get_project_report(project_id)
if not report:
raise HTTPException(status_code=404, detail="该项目暂无合并报告")
return report
@router.get("/batch-reports/{batch_id}", response_model=ProjectReportResponse)
async def get_batch_report(
batch_id: str,
service: PerformanceService = Depends(get_perf_service),
):
"""
按批次ID获取合并报告
"""
report = await service.get_batch_report(batch_id)
if not report:
raise HTTPException(status_code=404, detail="合并报告不存在")
return report
@router.get("/projects/{project_id}/reports", response_model=ProjectReportListResponse)
async def get_project_reports(
project_id: str,
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
service: PerformanceService = Depends(get_perf_service),
):
"""
获取项目历史合并报告列表
"""
result = await service.list_project_reports(project_id, page, page_size)
return ProjectReportListResponse(**result)
\ No newline at end of file
......@@ -460,5 +460,71 @@ class BatchRunResponse(BaseModel):
message: str
batch_id: str
task_count: int
report_id: Optional[str] = Field(None, description="合并报告ID(批量执行完成后生成)")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
# ==================== 合并报告 ====================
class TaskSummaryItem(BaseModel):
"""合并报告中的任务明细项"""
task_id: str = Field(..., description="任务ID")
task_name: str = Field("", description="任务名称")
status: str = Field("", description="任务状态")
total_requests: int = Field(0, description="请求数")
success_count: int = Field(0, description="成功数")
fail_count: int = Field(0, description="失败数")
avg_response_time: float = Field(0.0, description="平均响应时间(ms)")
actual_tps: float = Field(0.0, description="实际TPS")
error_rate: float = Field(0.0, description="错误率")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
class ProjectReportSummary(BaseModel):
"""合并报告聚合指标摘要"""
total_requests: int = 0
success_count: int = 0
fail_count: int = 0
error_rate: float = 0.0
actual_tps: float = 0.0
peak_tps: float = 0.0
avg_response_time: float = 0.0
min_response_time: float = 0.0
max_response_time: float = 0.0
p50_response_time: float = 0.0
p90_response_time: float = 0.0
p95_response_time: float = 0.0
p99_response_time: float = 0.0
std_dev: float = 0.0
apdex: float = 1.0
status_2xx: int = Field(0, alias="status2xx")
status_3xx: int = Field(0, alias="status3xx")
status_4xx: int = Field(0, alias="status4xx")
status_5xx: int = Field(0, alias="status5xx")
total_sent_bytes: int = 0
total_received_bytes: int = 0
duration_actual: float = 0.0
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
class ProjectReportResponse(BaseModel):
"""合并报告完整响应"""
id: str
project_id: str
batch_id: Optional[str] = None
created_at: Optional[str] = None
summary: ProjectReportSummary
task_summaries: List[TaskSummaryItem] = []
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
class ProjectReportListResponse(BaseModel):
"""合并报告列表响应"""
total: int
items: List[ProjectReportResponse]
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论