import math import os from pathlib import Path from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import FileResponse from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from app.core.logging import get_logger from app.db.models.data_collection import TaskExecution from app.db.session import get_db from app.module.collection.schema.collection import TaskExecutionBase, converter_execution_to_response from app.module.shared.schema import StandardResponse, PaginatedData router = APIRouter( prefix="/executions", tags=["data-collection/executions"], ) logger = get_logger(__name__) @router.get("", response_model=StandardResponse[PaginatedData[TaskExecutionBase]]) async def list_executions( page: int = 1, size: int = 20, task_id: Optional[str] = Query(None, description="Task ID"), task_name: Optional[str] = Query(None, description="Fuzzy search by task name"), status: Optional[str] = Query(None, description="Filter by execution status"), start_time: Optional[datetime] = Query(None, description="Start time range from (started_at >= start_time)"), end_time: Optional[datetime] = Query(None, description="Start time range to (started_at <= end_time)"), db: AsyncSession = Depends(get_db) ): """分页查询归集任务执行记录""" try: query = select(TaskExecution) if task_id: query = query.where(TaskExecution.task_id == task_id) if task_name: query = query.where(TaskExecution.task_name.ilike(f"%{task_name}%")) if status: query = query.where(TaskExecution.status == status) if start_time: query = query.where(TaskExecution.started_at >= start_time) if end_time: query = query.where(TaskExecution.started_at <= end_time) count_query = select(func.count()).select_from(query.subquery()) total = (await db.execute(count_query)).scalar_one() offset = (page - 1) * size executions = (await db.execute( query.order_by(TaskExecution.created_at.desc()) .offset(offset) .limit(size) )).scalars().all() items = [converter_execution_to_response(exe) for exe in executions] total_pages = math.ceil(total / size) if total > 0 else 0 return StandardResponse( code="0", message="Success", data=PaginatedData( content=items, total_elements=total, total_pages=total_pages, page=page, size=size, ) ) except Exception as e: logger.error(f"Failed to list task executions: {str(e)}", e) raise HTTPException(status_code=500, detail="Internal server error") @router.get("/{execution_id}/log") async def get_execution_log( execution_id: str, db: AsyncSession = Depends(get_db) ): """Get log file content for execution record""" try: execution = await db.get(TaskExecution, execution_id) if not execution: raise HTTPException(status_code=404, detail="Execution record not found") log_path = getattr(execution, "log_path", None) if not log_path: raise HTTPException(status_code=404, detail="Log path not found") path = Path(str(log_path)) if not path.is_absolute(): path = Path(os.getcwd()) / path path = path.resolve() if not path.exists() or not path.is_file(): raise HTTPException(status_code=404, detail="Log file not found") filename = path.name headers = { "Content-Disposition": f'inline; filename={filename}' } return FileResponse( path=str(path), media_type="text/plain; charset=utf-8", filename=filename, headers=headers, ) except HTTPException: raise except Exception as e: logger.error(f"Failed to get execution log: {str(e)}", e) raise HTTPException(status_code=500, detail="Internal server error")