Files
Neo-ZQYY/apps/backend/app/routers/execution.py

284 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""执行与队列 API
提供 8 个端点:
- POST /api/execution/run — 直接执行任务
- GET /api/execution/queue — 获取当前队列(按 site_id 过滤)
- POST /api/execution/queue — 添加到队列
- PUT /api/execution/queue/reorder — 重排队列
- DELETE /api/execution/queue/{id} — 删除队列任务
- POST /api/execution/{id}/cancel — 取消执行中的任务
- GET /api/execution/history — 执行历史(按 site_id 过滤)
- GET /api/execution/{id}/logs — 获取历史日志
所有端点需要 JWT 认证site_id 从 JWT 提取。
"""
from __future__ import annotations
import asyncio
import json
import logging
import uuid
from fastapi import APIRouter, Depends, HTTPException, Query, status
from app.auth.dependencies import CurrentUser, get_current_user
from app.database import get_connection
from app.schemas.execution import (
ExecutionHistoryItem,
ExecutionLogsResponse,
ExecutionRunResponse,
QueueTaskResponse,
ReorderRequest,
)
from app.schemas.tasks import TaskConfigSchema
from app.services.task_executor import task_executor
from app.services.task_queue import task_queue
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/execution", tags=["任务执行"])
# ── POST /api/execution/run — 直接执行任务 ────────────────────
@router.post("/run", response_model=ExecutionRunResponse)
async def run_task(
config: TaskConfigSchema,
user: CurrentUser = Depends(get_current_user),
) -> ExecutionRunResponse:
"""直接执行任务(不经过队列)。
从 JWT 注入 store_id创建 execution_id 后异步启动子进程。
"""
config = config.model_copy(update={"store_id": user.site_id})
execution_id = str(uuid.uuid4())
# 异步启动执行,不阻塞响应
asyncio.create_task(
task_executor.execute(
config=config,
execution_id=execution_id,
site_id=user.site_id,
)
)
return ExecutionRunResponse(
execution_id=execution_id,
message="任务已提交执行",
)
# ── GET /api/execution/queue — 获取当前队列 ───────────────────
@router.get("/queue", response_model=list[QueueTaskResponse])
async def get_queue(
user: CurrentUser = Depends(get_current_user),
) -> list[QueueTaskResponse]:
"""获取当前门店的待执行队列。"""
tasks = task_queue.list_pending(user.site_id)
return [
QueueTaskResponse(
id=t.id,
site_id=t.site_id,
config=t.config,
status=t.status,
position=t.position,
created_at=t.created_at,
started_at=t.started_at,
finished_at=t.finished_at,
exit_code=t.exit_code,
error_message=t.error_message,
)
for t in tasks
]
# ── POST /api/execution/queue — 添加到队列 ───────────────────
@router.post("/queue", response_model=QueueTaskResponse, status_code=status.HTTP_201_CREATED)
async def enqueue_task(
config: TaskConfigSchema,
user: CurrentUser = Depends(get_current_user),
) -> QueueTaskResponse:
"""将任务配置添加到执行队列。"""
config = config.model_copy(update={"store_id": user.site_id})
task_id = task_queue.enqueue(config, user.site_id)
# 查询刚创建的任务返回完整信息
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, site_id, config, status, position,
created_at, started_at, finished_at,
exit_code, error_message
FROM task_queue WHERE id = %s
""",
(task_id,),
)
row = cur.fetchone()
conn.commit()
finally:
conn.close()
if row is None:
raise HTTPException(status_code=500, detail="入队后查询失败")
config_data = row[2] if isinstance(row[2], dict) else json.loads(row[2])
return QueueTaskResponse(
id=str(row[0]),
site_id=row[1],
config=config_data,
status=row[3],
position=row[4],
created_at=row[5],
started_at=row[6],
finished_at=row[7],
exit_code=row[8],
error_message=row[9],
)
# ── PUT /api/execution/queue/reorder — 重排队列 ──────────────
@router.put("/queue/reorder")
async def reorder_queue(
body: ReorderRequest,
user: CurrentUser = Depends(get_current_user),
) -> dict:
"""调整队列中任务的执行顺序。"""
task_queue.reorder(body.task_id, body.new_position, user.site_id)
return {"message": "队列已重排"}
# ── DELETE /api/execution/queue/{id} — 删除队列任务 ──────────
@router.delete("/queue/{task_id}")
async def delete_queue_task(
task_id: str,
user: CurrentUser = Depends(get_current_user),
) -> dict:
"""从队列中删除待执行任务。仅允许删除 pending 状态的任务。"""
deleted = task_queue.delete(task_id, user.site_id)
if not deleted:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="任务不存在或非待执行状态,无法删除",
)
return {"message": "任务已从队列中删除"}
# ── POST /api/execution/{id}/cancel — 取消执行 ──────────────
@router.post("/{execution_id}/cancel")
async def cancel_execution(
execution_id: str,
user: CurrentUser = Depends(get_current_user),
) -> dict:
"""取消正在执行的任务。"""
cancelled = await task_executor.cancel(execution_id)
if not cancelled:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="执行任务不存在或已完成",
)
return {"message": "已发送取消信号"}
# ── GET /api/execution/history — 执行历史 ────────────────────
@router.get("/history", response_model=list[ExecutionHistoryItem])
async def get_execution_history(
limit: int = Query(default=50, ge=1, le=200),
user: CurrentUser = Depends(get_current_user),
) -> list[ExecutionHistoryItem]:
"""获取执行历史记录,按 started_at 降序排列。"""
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, site_id, task_codes, status, started_at,
finished_at, exit_code, duration_ms, command, summary,
schedule_id
FROM task_execution_log
WHERE site_id = %s
ORDER BY started_at DESC
LIMIT %s
""",
(user.site_id, limit),
)
rows = cur.fetchall()
conn.commit()
finally:
conn.close()
return [
ExecutionHistoryItem(
id=str(row[0]),
site_id=row[1],
task_codes=row[2] or [],
status=row[3],
started_at=row[4],
finished_at=row[5],
exit_code=row[6],
duration_ms=row[7],
command=row[8],
summary=row[9],
schedule_id=str(row[10]) if row[10] else None,
)
for row in rows
]
# ── GET /api/execution/{id}/logs — 获取历史日志 ──────────────
@router.get("/{execution_id}/logs", response_model=ExecutionLogsResponse)
async def get_execution_logs(
execution_id: str,
user: CurrentUser = Depends(get_current_user),
) -> ExecutionLogsResponse:
"""获取指定执行的完整日志。
优先从内存缓冲区读取(执行中),否则从数据库读取(已完成)。
"""
# 先尝试内存缓冲区(执行中的任务)
if task_executor.is_running(execution_id):
lines = task_executor.get_logs(execution_id)
return ExecutionLogsResponse(
execution_id=execution_id,
output_log="\n".join(lines) if lines else None,
)
# 从数据库读取已完成任务的日志
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT output_log, error_log
FROM task_execution_log
WHERE id = %s AND site_id = %s
""",
(execution_id, user.site_id),
)
row = cur.fetchone()
conn.commit()
finally:
conn.close()
if row is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="执行记录不存在",
)
return ExecutionLogsResponse(
execution_id=execution_id,
output_log=row[0],
error_log=row[1],
)