Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,714 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Scheduler API - 定时任务管理接口
|
||||
提供定时任务的 CRUD 操作和管理功能
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import select, func, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.config import settings
|
||||
from app.base_schema import PaginatedResponse, ResponseModel
|
||||
from scheduler.model import SchedulerJob, SchedulerLog
|
||||
from scheduler.schema import (
|
||||
SchedulerJobCreate,
|
||||
SchedulerJobUpdate,
|
||||
SchedulerJobResponse,
|
||||
SchedulerJobSimple,
|
||||
SchedulerJobBatchDeleteIn,
|
||||
SchedulerJobBatchDeleteOut,
|
||||
SchedulerJobBatchUpdateStatusIn,
|
||||
SchedulerJobBatchUpdateStatusOut,
|
||||
SchedulerJobExecuteIn,
|
||||
SchedulerJobExecuteOut,
|
||||
SchedulerJobStatisticsOut,
|
||||
SchedulerJobSearchRequest,
|
||||
SchedulerLogResponse,
|
||||
SchedulerLogBatchDeleteIn,
|
||||
SchedulerLogBatchDeleteOut,
|
||||
SchedulerLogCleanIn,
|
||||
SchedulerLogCleanOut,
|
||||
SchedulerStatusOut,
|
||||
)
|
||||
from scheduler.service import scheduler_service
|
||||
|
||||
router = APIRouter(prefix="/scheduler", tags=["定时任务管理"])
|
||||
|
||||
|
||||
def _build_job_response(job: SchedulerJob) -> SchedulerJobResponse:
|
||||
"""构建任务响应"""
|
||||
return SchedulerJobResponse(
|
||||
id=job.id,
|
||||
application_id=job.application_id,
|
||||
name=job.name,
|
||||
code=job.code,
|
||||
description=job.description,
|
||||
group=job.group,
|
||||
trigger_type=job.trigger_type,
|
||||
trigger_type_display=job.get_trigger_type_display(),
|
||||
cron_expression=job.cron_expression,
|
||||
interval_seconds=job.interval_seconds,
|
||||
run_date=job.run_date,
|
||||
task_func=job.task_func,
|
||||
task_args=job.task_args,
|
||||
task_kwargs=job.task_kwargs,
|
||||
status=job.status,
|
||||
status_display=job.get_status_display(),
|
||||
priority=job.priority,
|
||||
max_instances=job.max_instances,
|
||||
max_retries=job.max_retries,
|
||||
timeout=job.timeout,
|
||||
coalesce=job.coalesce,
|
||||
allow_concurrent=job.allow_concurrent,
|
||||
total_run_count=job.total_run_count,
|
||||
success_count=job.success_count,
|
||||
failure_count=job.failure_count,
|
||||
success_rate=job.get_success_rate(),
|
||||
last_run_time=job.last_run_time,
|
||||
next_run_time=job.next_run_time,
|
||||
last_run_status=job.last_run_status,
|
||||
last_run_result=job.last_run_result,
|
||||
remark=job.remark,
|
||||
sort=job.sort,
|
||||
sys_create_datetime=job.sys_create_datetime,
|
||||
sys_update_datetime=job.sys_update_datetime,
|
||||
)
|
||||
|
||||
|
||||
def _build_log_response(log: SchedulerLog) -> SchedulerLogResponse:
|
||||
"""构建日志响应"""
|
||||
return SchedulerLogResponse(
|
||||
id=log.id,
|
||||
job_id=log.job_id,
|
||||
job_name=log.job_name,
|
||||
job_code=log.job_code,
|
||||
status=log.status,
|
||||
status_display=log.get_status_display(),
|
||||
start_time=log.start_time,
|
||||
end_time=log.end_time,
|
||||
duration=log.duration,
|
||||
result=log.result,
|
||||
exception=log.exception,
|
||||
traceback=log.traceback,
|
||||
hostname=log.hostname,
|
||||
process_id=log.process_id,
|
||||
retry_count=log.retry_count,
|
||||
sys_create_datetime=log.sys_create_datetime,
|
||||
)
|
||||
|
||||
|
||||
# ==================== SchedulerJob APIs ====================
|
||||
|
||||
@router.post("/job", response_model=SchedulerJobResponse, summary="创建定时任务")
|
||||
async def create_scheduler_job(data: SchedulerJobCreate, db: AsyncSession = Depends(get_db)):
|
||||
"""创建新的定时任务"""
|
||||
# 检查任务编码是否已存在
|
||||
result = await db.execute(
|
||||
select(SchedulerJob).where(
|
||||
SchedulerJob.code == data.code,
|
||||
SchedulerJob.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
if result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail=f"任务编码已存在: {data.code}")
|
||||
|
||||
# 创建任务
|
||||
job = SchedulerJob(**data.model_dump())
|
||||
db.add(job)
|
||||
await db.commit()
|
||||
await db.refresh(job)
|
||||
|
||||
# 如果任务是启用状态,添加到调度器
|
||||
if job.is_enabled() and scheduler_service.is_running():
|
||||
await scheduler_service.add_job(job)
|
||||
|
||||
return _build_job_response(job)
|
||||
|
||||
|
||||
@router.get("/job/all", response_model=List[SchedulerJobSimple], summary="获取所有定时任务(简化版)")
|
||||
async def get_all_scheduler_jobs(
|
||||
application_id: str = Query(None, alias="applicationId", description="所属应用ID"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取所有定时任务(不分页,简化版)"""
|
||||
conditions = [SchedulerJob.is_deleted == False] # noqa: E712
|
||||
if application_id:
|
||||
conditions.append(SchedulerJob.application_id == application_id)
|
||||
else:
|
||||
conditions.append(SchedulerJob.application_id.is_(None))
|
||||
|
||||
result = await db.execute(
|
||||
select(SchedulerJob).where(*conditions).order_by(SchedulerJob.priority.desc(), SchedulerJob.name)
|
||||
)
|
||||
jobs = result.scalars().all()
|
||||
return jobs
|
||||
|
||||
|
||||
@router.get("/job", response_model=PaginatedResponse[SchedulerJobResponse], summary="获取定时任务列表")
|
||||
async def get_scheduler_job_list(
|
||||
page: int = Query(default=1, ge=1, description="页码"),
|
||||
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"),
|
||||
application_id: str = Query(None, alias="applicationId", description="所属应用ID"),
|
||||
name: Optional[str] = Query(default=None, description="任务名称"),
|
||||
code: Optional[str] = Query(default=None, description="任务编码"),
|
||||
group: Optional[str] = Query(default=None, description="任务分组"),
|
||||
trigger_type: Optional[str] = Query(default=None, description="触发器类型"),
|
||||
status: Optional[int] = Query(default=None, description="任务状态"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取定时任务列表(分页)"""
|
||||
filters = [SchedulerJob.is_deleted == False] # noqa: E712
|
||||
# 应用过滤
|
||||
if application_id:
|
||||
filters.append(SchedulerJob.application_id == application_id)
|
||||
else:
|
||||
filters.append(SchedulerJob.application_id.is_(None))
|
||||
if name:
|
||||
filters.append(SchedulerJob.name.ilike(f"%{name}%"))
|
||||
if code:
|
||||
filters.append(SchedulerJob.code.ilike(f"%{code}%"))
|
||||
if group:
|
||||
filters.append(SchedulerJob.group == group)
|
||||
if trigger_type:
|
||||
filters.append(SchedulerJob.trigger_type == trigger_type)
|
||||
if status is not None:
|
||||
filters.append(SchedulerJob.status == status)
|
||||
|
||||
# 查询总数
|
||||
count_result = await db.execute(
|
||||
select(func.count(SchedulerJob.id)).where(*filters)
|
||||
)
|
||||
total = count_result.scalar()
|
||||
|
||||
# 查询数据
|
||||
offset = (page - 1) * page_size
|
||||
result = await db.execute(
|
||||
select(SchedulerJob).where(*filters)
|
||||
.order_by(SchedulerJob.priority.desc(), SchedulerJob.sys_update_datetime.desc())
|
||||
.offset(offset).limit(page_size)
|
||||
)
|
||||
jobs = result.scalars().all()
|
||||
|
||||
return PaginatedResponse(
|
||||
items=[_build_job_response(job) for job in jobs],
|
||||
total=total
|
||||
)
|
||||
|
||||
|
||||
@router.post("/job/batch/delete", response_model=SchedulerJobBatchDeleteOut, summary="批量删除定时任务")
|
||||
async def batch_delete_scheduler_jobs(
|
||||
data: SchedulerJobBatchDeleteIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""批量删除定时任务"""
|
||||
success_count = 0
|
||||
failed_ids = []
|
||||
|
||||
for job_id in data.ids:
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(SchedulerJob).where(SchedulerJob.id == job_id)
|
||||
)
|
||||
job = result.scalar_one_or_none()
|
||||
|
||||
if job:
|
||||
# 从调度器移除
|
||||
if scheduler_service.is_running():
|
||||
await scheduler_service.remove_job(job.code)
|
||||
|
||||
# 软删除
|
||||
job.is_deleted = True
|
||||
success_count += 1
|
||||
else:
|
||||
failed_ids.append(job_id)
|
||||
except Exception:
|
||||
failed_ids.append(job_id)
|
||||
|
||||
await db.commit()
|
||||
return SchedulerJobBatchDeleteOut(count=success_count, failed_ids=failed_ids)
|
||||
|
||||
|
||||
@router.post("/job/batch/update_status", response_model=SchedulerJobBatchUpdateStatusOut, summary="批量更新任务状态")
|
||||
async def batch_update_scheduler_job_status(
|
||||
data: SchedulerJobBatchUpdateStatusIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""批量启用、禁用或暂停任务"""
|
||||
result = await db.execute(
|
||||
select(SchedulerJob).where(SchedulerJob.id.in_(data.ids))
|
||||
)
|
||||
jobs = result.scalars().all()
|
||||
|
||||
count = 0
|
||||
for job in jobs:
|
||||
job.status = data.status
|
||||
count += 1
|
||||
|
||||
# 同步更新调度器
|
||||
if scheduler_service.is_running():
|
||||
if job.is_enabled():
|
||||
await scheduler_service.add_job(job)
|
||||
elif job.is_paused():
|
||||
await scheduler_service.pause_job(job.code)
|
||||
else:
|
||||
await scheduler_service.remove_job(job.code)
|
||||
|
||||
await db.commit()
|
||||
return SchedulerJobBatchUpdateStatusOut(count=count)
|
||||
|
||||
|
||||
@router.post("/job/execute", response_model=SchedulerJobExecuteOut, summary="立即执行任务")
|
||||
async def execute_scheduler_job(
|
||||
data: SchedulerJobExecuteIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""立即执行指定任务(不影响正常调度)"""
|
||||
result = await db.execute(
|
||||
select(SchedulerJob).where(SchedulerJob.id == data.job_id)
|
||||
)
|
||||
job = result.scalar_one_or_none()
|
||||
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
if not scheduler_service.is_running():
|
||||
raise HTTPException(status_code=400, detail="调度器未运行")
|
||||
|
||||
# 立即执行任务
|
||||
success = await scheduler_service.run_job_now(job.code)
|
||||
|
||||
if success:
|
||||
return SchedulerJobExecuteOut(
|
||||
success=True,
|
||||
message=f"任务 {job.name} 将立即执行"
|
||||
)
|
||||
else:
|
||||
return SchedulerJobExecuteOut(
|
||||
success=False,
|
||||
message=f"任务 {job.name} 执行失败,可能任务未在调度器中"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/job/search", response_model=PaginatedResponse[SchedulerJobResponse], summary="搜索定时任务")
|
||||
async def search_scheduler_jobs(
|
||||
data: SchedulerJobSearchRequest,
|
||||
page: int = Query(default=1, ge=1, description="页码"),
|
||||
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""搜索定时任务"""
|
||||
keyword = data.keyword
|
||||
filters = [
|
||||
SchedulerJob.is_deleted == False, # noqa: E712
|
||||
or_(
|
||||
SchedulerJob.name.ilike(f"%{keyword}%"),
|
||||
SchedulerJob.code.ilike(f"%{keyword}%"),
|
||||
SchedulerJob.description.ilike(f"%{keyword}%"),
|
||||
)
|
||||
]
|
||||
|
||||
# 查询总数
|
||||
count_result = await db.execute(
|
||||
select(func.count(SchedulerJob.id)).where(*filters)
|
||||
)
|
||||
total = count_result.scalar()
|
||||
|
||||
# 查询数据
|
||||
offset = (page - 1) * page_size
|
||||
result = await db.execute(
|
||||
select(SchedulerJob).where(*filters)
|
||||
.order_by(SchedulerJob.priority.desc())
|
||||
.offset(offset).limit(page_size)
|
||||
)
|
||||
jobs = result.scalars().all()
|
||||
|
||||
return PaginatedResponse(
|
||||
items=[_build_job_response(job) for job in jobs],
|
||||
total=total
|
||||
)
|
||||
|
||||
|
||||
@router.get("/job/statistics/data", response_model=SchedulerJobStatisticsOut, summary="获取任务统计信息")
|
||||
async def get_scheduler_job_statistics(db: AsyncSession = Depends(get_db)):
|
||||
"""获取任务统计信息"""
|
||||
# 任务统计
|
||||
total_result = await db.execute(
|
||||
select(func.count(SchedulerJob.id)).where(SchedulerJob.is_deleted == False) # noqa: E712
|
||||
)
|
||||
total_jobs = total_result.scalar() or 0
|
||||
|
||||
enabled_result = await db.execute(
|
||||
select(func.count(SchedulerJob.id)).where(
|
||||
SchedulerJob.is_deleted == False, # noqa: E712
|
||||
SchedulerJob.status == 1
|
||||
)
|
||||
)
|
||||
enabled_jobs = enabled_result.scalar() or 0
|
||||
|
||||
disabled_result = await db.execute(
|
||||
select(func.count(SchedulerJob.id)).where(
|
||||
SchedulerJob.is_deleted == False, # noqa: E712
|
||||
SchedulerJob.status == 0
|
||||
)
|
||||
)
|
||||
disabled_jobs = disabled_result.scalar() or 0
|
||||
|
||||
paused_result = await db.execute(
|
||||
select(func.count(SchedulerJob.id)).where(
|
||||
SchedulerJob.is_deleted == False, # noqa: E712
|
||||
SchedulerJob.status == 2
|
||||
)
|
||||
)
|
||||
paused_jobs = paused_result.scalar() or 0
|
||||
|
||||
# 执行统计
|
||||
total_exec_result = await db.execute(
|
||||
select(func.count(SchedulerLog.id))
|
||||
)
|
||||
total_executions = total_exec_result.scalar() or 0
|
||||
|
||||
success_exec_result = await db.execute(
|
||||
select(func.count(SchedulerLog.id)).where(SchedulerLog.status == 'success')
|
||||
)
|
||||
success_executions = success_exec_result.scalar() or 0
|
||||
|
||||
failed_exec_result = await db.execute(
|
||||
select(func.count(SchedulerLog.id)).where(SchedulerLog.status == 'failed')
|
||||
)
|
||||
failed_executions = failed_exec_result.scalar() or 0
|
||||
|
||||
# 计算成功率
|
||||
success_rate = round(success_executions / total_executions * 100, 2) if total_executions > 0 else 0
|
||||
|
||||
return SchedulerJobStatisticsOut(
|
||||
total_jobs=total_jobs,
|
||||
enabled_jobs=enabled_jobs,
|
||||
disabled_jobs=disabled_jobs,
|
||||
paused_jobs=paused_jobs,
|
||||
total_executions=total_executions,
|
||||
success_executions=success_executions,
|
||||
failed_executions=failed_executions,
|
||||
success_rate=success_rate,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/job/{job_id}", response_model=SchedulerJobResponse, summary="获取定时任务详情")
|
||||
async def get_scheduler_job(job_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""获取单个定时任务的详细信息"""
|
||||
result = await db.execute(
|
||||
select(SchedulerJob).where(SchedulerJob.id == job_id)
|
||||
)
|
||||
job = result.scalar_one_or_none()
|
||||
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
return _build_job_response(job)
|
||||
|
||||
|
||||
@router.put("/job/{job_id}", response_model=SchedulerJobResponse, summary="更新定时任务")
|
||||
async def update_scheduler_job(
|
||||
job_id: str,
|
||||
data: SchedulerJobUpdate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""更新定时任务"""
|
||||
result = await db.execute(
|
||||
select(SchedulerJob).where(SchedulerJob.id == job_id)
|
||||
)
|
||||
job = result.scalar_one_or_none()
|
||||
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
# 检查任务编码是否已存在(排除自身)
|
||||
if data.code:
|
||||
code_result = await db.execute(
|
||||
select(SchedulerJob).where(
|
||||
SchedulerJob.code == data.code,
|
||||
SchedulerJob.id != job_id,
|
||||
SchedulerJob.is_deleted == False # noqa: E712
|
||||
)
|
||||
)
|
||||
if code_result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail=f"任务编码已存在: {data.code}")
|
||||
|
||||
# 更新字段
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(job, key, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(job)
|
||||
|
||||
# 同步更新调度器
|
||||
if scheduler_service.is_running():
|
||||
await scheduler_service.modify_job(job)
|
||||
|
||||
return _build_job_response(job)
|
||||
|
||||
|
||||
@router.delete("/job/{job_id}", response_model=ResponseModel, summary="删除定时任务")
|
||||
async def delete_scheduler_job(
|
||||
job_id: str,
|
||||
hard: bool = Query(default=False, description="是否物理删除"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""删除定时任务"""
|
||||
result = await db.execute(
|
||||
select(SchedulerJob).where(SchedulerJob.id == job_id)
|
||||
)
|
||||
job = result.scalar_one_or_none()
|
||||
|
||||
if not job:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
# 从调度器移除
|
||||
if scheduler_service.is_running():
|
||||
await scheduler_service.remove_job(job.code)
|
||||
|
||||
if hard:
|
||||
await db.delete(job)
|
||||
else:
|
||||
job.is_deleted = True
|
||||
|
||||
await db.commit()
|
||||
return ResponseModel(message="删除成功")
|
||||
|
||||
|
||||
# ==================== SchedulerLog APIs ====================
|
||||
|
||||
@router.get("/log", response_model=PaginatedResponse[SchedulerLogResponse], summary="获取任务执行日志列表")
|
||||
async def get_scheduler_log_list(
|
||||
page: int = Query(default=1, ge=1, description="页码"),
|
||||
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"),
|
||||
job_id: Optional[str] = Query(default=None, description="任务ID"),
|
||||
job_code: Optional[str] = Query(default=None, description="任务编码"),
|
||||
job_name: Optional[str] = Query(default=None, description="任务名称"),
|
||||
status: Optional[str] = Query(default=None, description="执行状态"),
|
||||
start_time_gte: Optional[datetime] = Query(default=None, alias="startTimeGte", description="开始时间>="),
|
||||
start_time_lte: Optional[datetime] = Query(default=None, alias="startTimeLte", description="开始时间<="),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取任务执行日志列表(分页)"""
|
||||
filters = []
|
||||
if job_id:
|
||||
filters.append(SchedulerLog.job_id == job_id)
|
||||
if job_code:
|
||||
filters.append(SchedulerLog.job_code.ilike(f"%{job_code}%"))
|
||||
if job_name:
|
||||
filters.append(SchedulerLog.job_name.ilike(f"%{job_name}%"))
|
||||
if status:
|
||||
filters.append(SchedulerLog.status == status)
|
||||
if start_time_gte:
|
||||
filters.append(SchedulerLog.start_time >= start_time_gte)
|
||||
if start_time_lte:
|
||||
filters.append(SchedulerLog.start_time <= start_time_lte)
|
||||
|
||||
# 查询总数
|
||||
count_result = await db.execute(
|
||||
select(func.count(SchedulerLog.id)).where(*filters) if filters else select(func.count(SchedulerLog.id))
|
||||
)
|
||||
total = count_result.scalar()
|
||||
|
||||
# 查询数据
|
||||
offset = (page - 1) * page_size
|
||||
query = select(SchedulerLog).order_by(SchedulerLog.start_time.desc()).offset(offset).limit(page_size)
|
||||
if filters:
|
||||
query = select(SchedulerLog).where(*filters).order_by(SchedulerLog.start_time.desc()).offset(offset).limit(page_size)
|
||||
|
||||
result = await db.execute(query)
|
||||
logs = result.scalars().all()
|
||||
|
||||
return PaginatedResponse(
|
||||
items=[_build_log_response(log) for log in logs],
|
||||
total=total
|
||||
)
|
||||
|
||||
|
||||
@router.get("/log/by/job/{job_id}", response_model=PaginatedResponse[SchedulerLogResponse], summary="获取指定任务的执行日志")
|
||||
async def get_scheduler_logs_by_job(
|
||||
job_id: str,
|
||||
page: int = Query(default=1, ge=1, description="页码"),
|
||||
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取指定任务的所有执行日志"""
|
||||
# 查询总数
|
||||
count_result = await db.execute(
|
||||
select(func.count(SchedulerLog.id)).where(SchedulerLog.job_id == job_id)
|
||||
)
|
||||
total = count_result.scalar()
|
||||
|
||||
# 查询数据
|
||||
offset = (page - 1) * page_size
|
||||
result = await db.execute(
|
||||
select(SchedulerLog).where(SchedulerLog.job_id == job_id)
|
||||
.order_by(SchedulerLog.start_time.desc())
|
||||
.offset(offset).limit(page_size)
|
||||
)
|
||||
logs = result.scalars().all()
|
||||
|
||||
return PaginatedResponse(
|
||||
items=[_build_log_response(log) for log in logs],
|
||||
total=total
|
||||
)
|
||||
|
||||
|
||||
@router.get("/log/{log_id}", response_model=SchedulerLogResponse, summary="获取任务执行日志详情")
|
||||
async def get_scheduler_log(log_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""获取单个任务执行日志的详细信息"""
|
||||
result = await db.execute(
|
||||
select(SchedulerLog).where(SchedulerLog.id == log_id)
|
||||
)
|
||||
log = result.scalar_one_or_none()
|
||||
|
||||
if not log:
|
||||
raise HTTPException(status_code=404, detail="日志不存在")
|
||||
|
||||
return _build_log_response(log)
|
||||
|
||||
|
||||
@router.delete("/log/{log_id}", response_model=ResponseModel, summary="删除任务执行日志")
|
||||
async def delete_scheduler_log(log_id: str, db: AsyncSession = Depends(get_db)):
|
||||
"""删除任务执行日志"""
|
||||
result = await db.execute(
|
||||
select(SchedulerLog).where(SchedulerLog.id == log_id)
|
||||
)
|
||||
log = result.scalar_one_or_none()
|
||||
|
||||
if not log:
|
||||
raise HTTPException(status_code=404, detail="日志不存在")
|
||||
|
||||
await db.delete(log)
|
||||
await db.commit()
|
||||
return ResponseModel(message="删除成功")
|
||||
|
||||
|
||||
@router.post("/log/batch/delete", response_model=SchedulerLogBatchDeleteOut, summary="批量删除任务执行日志")
|
||||
async def batch_delete_scheduler_logs(
|
||||
data: SchedulerLogBatchDeleteIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""批量删除任务执行日志"""
|
||||
result = await db.execute(
|
||||
select(SchedulerLog).where(SchedulerLog.id.in_(data.ids))
|
||||
)
|
||||
logs = result.scalars().all()
|
||||
|
||||
count = len(logs)
|
||||
for log in logs:
|
||||
await db.delete(log)
|
||||
|
||||
await db.commit()
|
||||
return SchedulerLogBatchDeleteOut(count=count)
|
||||
|
||||
|
||||
@router.post("/log/clean", response_model=SchedulerLogCleanOut, summary="清理旧日志")
|
||||
async def clean_scheduler_logs(
|
||||
data: SchedulerLogCleanIn,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""清理旧日志"""
|
||||
cutoff_date = datetime.now() - timedelta(days=data.days)
|
||||
|
||||
filters = [SchedulerLog.start_time < cutoff_date]
|
||||
if data.status:
|
||||
filters.append(SchedulerLog.status == data.status)
|
||||
|
||||
result = await db.execute(
|
||||
select(SchedulerLog).where(*filters)
|
||||
)
|
||||
logs = result.scalars().all()
|
||||
|
||||
count = len(logs)
|
||||
for log in logs:
|
||||
await db.delete(log)
|
||||
|
||||
await db.commit()
|
||||
return SchedulerLogCleanOut(count=count)
|
||||
|
||||
|
||||
# ==================== Scheduler Control APIs ====================
|
||||
|
||||
@router.post("/start", response_model=ResponseModel, summary="启动调度器")
|
||||
async def start_scheduler():
|
||||
"""启动调度器(注意:调度器应在应用启动时自动启动)"""
|
||||
if scheduler_service.is_running():
|
||||
raise HTTPException(status_code=400, detail="调度器已在运行中")
|
||||
|
||||
# APScheduler 4.x 中调度器应在 lifespan 中启动
|
||||
raise HTTPException(status_code=400, detail="请重启应用以启动调度器")
|
||||
|
||||
|
||||
@router.post("/shutdown", response_model=ResponseModel, summary="关闭调度器")
|
||||
async def shutdown_scheduler():
|
||||
"""关闭调度器(注意:调度器会在应用关闭时自动关闭)"""
|
||||
if not scheduler_service.is_running():
|
||||
raise HTTPException(status_code=400, detail="调度器未运行")
|
||||
|
||||
# APScheduler 4.x 中调度器应在 lifespan 中关闭
|
||||
raise HTTPException(status_code=400, detail="请关闭应用以停止调度器")
|
||||
|
||||
|
||||
@router.post("/pause", response_model=ResponseModel, summary="暂停调度器")
|
||||
async def pause_scheduler():
|
||||
"""暂停调度器(暂不支持)"""
|
||||
raise HTTPException(status_code=400, detail="APScheduler 4.x 暂不支持暂停整个调度器")
|
||||
|
||||
|
||||
@router.post("/resume", response_model=ResponseModel, summary="恢复调度器")
|
||||
async def resume_scheduler():
|
||||
"""恢复调度器(暂不支持)"""
|
||||
raise HTTPException(status_code=400, detail="APScheduler 4.x 暂不支持恢复整个调度器")
|
||||
|
||||
|
||||
@router.get("/status", response_model=SchedulerStatusOut, summary="获取调度器状态")
|
||||
async def get_scheduler_status():
|
||||
"""获取调度器状态"""
|
||||
is_running = scheduler_service.is_running()
|
||||
jobs = await scheduler_service.get_all_jobs() if is_running else []
|
||||
|
||||
return SchedulerStatusOut(
|
||||
is_running=is_running,
|
||||
job_count=len(jobs),
|
||||
jobs=jobs,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/log/{log_id}/stream", summary="实时日志流")
|
||||
async def stream_task_log(log_id: str):
|
||||
"""
|
||||
通过 SSE 实时推送任务执行日志
|
||||
|
||||
Args:
|
||||
log_id: 日志记录 ID
|
||||
|
||||
Returns:
|
||||
SSE 事件流,包含任务执行过程中的日志消息
|
||||
"""
|
||||
from fastapi.responses import StreamingResponse
|
||||
from scheduler.task_log_service import TaskLogService
|
||||
import json
|
||||
|
||||
async def event_generator():
|
||||
try:
|
||||
async for log_entry in TaskLogService.subscribe(log_id):
|
||||
yield f"data: {json.dumps(log_entry, ensure_ascii=False)}\n\n"
|
||||
except Exception as e:
|
||||
yield f"data: {json.dumps({'level': 'error', 'message': str(e)}, ensure_ascii=False)}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
event_generator(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user