VWED_server/routes/task_api.py
2025-05-12 15:43:21 +08:00

397 lines
14 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.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
天风任务管理API路由
实现任务的列表查询、创建、删除、运行等功能
"""
import json
from typing import Optional, Dict, Any
from fastapi import APIRouter, Depends, File, UploadFile, Form, Response, Path
from sqlalchemy.orm import Session
from pydantic import ValidationError
import logging
import os
import time
import random
from data.session import get_db
from services.task_service import TaskService, TaskNameExistsError
from routes.model.base import ApiResponse
from routes.model.task_model import (
CreateTaskRequest, DeleteTaskRequest, ExportBatchRequest, TaskResponse, TaskListParams
)
from routes.common_api import format_response, error_response
from utils.crypto_utils import CryptoUtils
from utils.logger import get_logger
# 创建路由
router = APIRouter(prefix="/api/vwed-task", tags=["VWED任务管理"])
# 设置日志
logger = get_logger("app.task_api")
# 标准API响应格式
def api_response(code: int = 200, message: str = "操作成功", data: Any = None) -> Dict[str, Any]:
"""
标准API响应格式
Args:
code: 状态码
message: 响应消息
data: 响应数据
Returns:
Dict[str, Any]: 格式化的响应数据
"""
return {
"code": code,
"message": message,
"data": data
}
@router.get("/list", response_model=ApiResponse)
async def get_task_list(
params: TaskListParams = Depends(),
db: Session = Depends(get_db)
):
"""
获取天风任务列表
Args:
params: 查询参数(分页、排序、筛选)
db: 数据库会话
Returns:
ApiResponse[TaskListResponse]: 包含任务列表和分页信息的响应
"""
try:
# 调用服务层方法获取任务列表
result = TaskService.get_task_list(
db=db,
page_num=params.pageNum,
page_size=params.pageSize,
keyword=params.keyword,
status=params.status,
sort_field=params.sortField,
sort_order=params.sortOrder
)
return api_response(data=result)
except Exception as e:
logger.error(f"获取天风任务列表失败: {str(e)}")
return error_response(f"获取天风任务列表失败: {str(e)}", 500)
@router.post("/create", response_model=ApiResponse[TaskResponse])
async def create_task(
task_req: CreateTaskRequest,
db: Session = Depends(get_db)
):
"""
创建任务
Args:
task_req: 创建任务请求数据
db: 数据库会话
Returns:
ApiResponse[TaskResponse]: 包含新建任务信息的响应
"""
try:
# 调用服务层方法创建任务
task = TaskService.create_task(
db=db,
label=task_req.label,
task_type=task_req.taskType,
remark=task_req.remark,
period=task_req.period,
delay=task_req.delay,
release_sites=task_req.releaseSites,
token=task_req.token,
tenant_id=task_req.tenantId,
map_id=task_req.mapId
)
return api_response(message="任务创建成功", data=task)
except ValidationError as e:
errors = e.errors()
error_msg = f"任务创建失败: {errors[0]['msg'] if errors else '验证错误'}"
return error_response(error_msg, 400)
except TaskNameExistsError as e:
logger.warning(f"任务创建失败: {str(e)}")
return error_response(f"任务名称已存在: {str(e)}", 400)
except Exception as e:
logger.error(f"创建任务失败: {str(e)}")
return error_response(f"创建任务失败: {str(e)}", 500)
@router.delete("/delete", response_model=ApiResponse)
async def delete_task(
delete_req: DeleteTaskRequest,
db: Session = Depends(get_db)
):
"""
删除任务
Args:
delete_req: 删除任务请求数据包含要删除的任务ID列表
db: 数据库会话
Returns:
ApiResponse: 操作结果
"""
try:
# 调用服务层方法删除任务
TaskService.delete_tasks(db=db, ids=delete_req.ids)
return api_response(message="删除成功")
except ValidationError as e:
errors = e.errors()
error_msg = f"删除任务失败: {errors[0]['msg'] if errors else '验证错误'}"
return error_response(error_msg, 400)
except Exception as e:
logger.error(f"删除任务失败: {str(e)}")
return error_response(f"删除任务失败: {str(e)}", 500)
@router.post("/export-batch")
async def export_batch(
export_req: ExportBatchRequest,
db: Session = Depends(get_db)
):
"""
批量导出任务
Args:
export_req: 导出请求数据包含要导出的任务ID列表
db: 数据库会话
Returns:
Response: 包含任务定义的加密专有格式文件
"""
try:
# 调用服务层方法导出任务
tasks = TaskService.export_tasks(db=db, ids=export_req.ids)
# 保留必要字段去除id、创建时间和版本等字段
simplified_tasks = []
fields_to_remove = ['id', 'created_at', 'updated_at', 'version', 'deleted']
for task in tasks:
if isinstance(task, dict):
task_export = {}
# 复制除了排除字段之外的所有字段
for key, value in task.items():
if key not in fields_to_remove:
task_export[key] = value
simplified_tasks.append(task_export)
# 加密数据并添加文件头、签名和校验和
encrypted_data = CryptoUtils.encrypt_data(simplified_tasks)
# 使用专有文件扩展名 .vtex (VWED Task Export)
filename = f"tasks_export_{len(export_req.ids)}.vtex" if len(export_req.ids) > 1 else f"task_{export_req.ids[0]}.vtex"
# 返回加密的二进制文件
headers = {
'Content-Disposition': f'attachment; filename={filename}',
'Content-Type': 'application/octet-stream',
'X-Content-Type-Options': 'nosniff', # 防止浏览器嗅探文件类型
"Access-Control-Expose-Headers": "Content-Disposition"
}
return Response(content=encrypted_data, media_type='application/octet-stream', headers=headers)
except ValidationError as e:
errors = e.errors()
error_msg = f"导出任务失败: {errors[0]['msg'] if errors else '验证错误'}"
return error_response(error_msg, 400)
except Exception as e:
logger.error(f"导出任务失败: {str(e)}")
return error_response(f"导出任务失败: {str(e)}", 500)
@router.post("/import", response_model=ApiResponse)
async def import_task(
file: UploadFile = File(...),
token: Optional[str] = Form(None, description="用户token值用于认证"),
tenantId: str = Form("default", description="租户ID用于多租户隔离"),
db: Session = Depends(get_db)
):
"""
导入任务
Args:
file: 任务配置文件,加密专有格式
token: 用户token值用于认证
tenantId: 租户ID用于多租户隔离
db: 数据库会话
Returns:
ApiResponse: 包含导入任务信息的响应
"""
try:
# 读取文件内容
content = await file.read()
try:
# 解密并验证数据
tasks_data = CryptoUtils.decrypt_data(content)
# 导入结果统计
import_results = {
"success_count": 0,
"failed_count": 0,
"failed_tasks": [],
"imported_tasks": []
}
# 批量导入所有任务
timestamp_base = int(time.time())
for index, task_data in enumerate(tasks_data):
try:
if not isinstance(task_data, dict):
import_results["failed_count"] += 1
import_results["failed_tasks"].append({"index": index, "reason": "任务数据格式无效"})
continue
# 为每个任务生成唯一的时间戳和随机数后缀
# 使用基础时间戳+索引以确保每个任务都有不同的时间戳
timestamp = timestamp_base + index
rand_num = random.randint(1000, 9999)
# 设置导入任务的名称,添加时间戳和随机数后缀
original_name = task_data.get('label', '导入的任务')
import_name = f"{original_name}-备份-{timestamp}{rand_num}"
# 导入任务
result = TaskService.import_task(
db=db,
task_data=task_data,
task_name=import_name,
token=token,
tenant_id=tenantId
)
import_results["success_count"] += 1
import_results["imported_tasks"].append({
"id": result.get("id", ""),
"name": import_name,
"original_name": original_name
})
except TaskNameExistsError as e:
import_results["failed_count"] += 1
import_results["failed_tasks"].append({
"index": index,
"name": task_data.get('label', '未知任务'),
"reason": f"任务名称已存在: {str(e)}"
})
except Exception as e:
import_results["failed_count"] += 1
import_results["failed_tasks"].append({
"index": index,
"name": task_data.get('label', '未知任务'),
"reason": str(e)
})
# 根据导入结果生成消息
if import_results["success_count"] > 0 and import_results["failed_count"] == 0:
message = f"成功导入 {import_results['success_count']} 个任务"
elif import_results["success_count"] > 0 and import_results["failed_count"] > 0:
message = f"部分导入成功: 成功 {import_results['success_count']} 个, 失败 {import_results['failed_count']}"
else:
message = f"导入失败: 所有 {import_results['failed_count']} 个任务导入失败"
return api_response(message=message, data=import_results)
except ValueError as e:
logger.error(f"文件格式无效或已损坏: {str(e)}")
return error_response(f"无法导入任务: {str(e)}", 400)
except Exception as e:
logger.error(f"解析任务数据失败: {str(e)}")
return error_response(f"解析任务数据失败: {str(e)}", 400)
except ValidationError as e:
errors = e.errors()
error_msg = f"导入任务失败: {errors[0]['msg'] if errors else '验证错误'}"
return error_response(error_msg, 400)
except Exception as e:
logger.error(f"导入任务失败: {str(e)}")
return error_response(f"导入任务失败: {str(e)}", 500)
@router.get("/{task_id}")
async def api_get_task(task_id: str, db: Session = Depends(get_db)):
"""获取单个任务详情"""
try:
task = TaskService.get_task_by_id(db, task_id)
if not task:
return error_response("任务不存在", 404)
return format_response(task)
except Exception as e:
logger.error(f"获取任务详情失败: {str(e)}")
return error_response(f"获取任务详情失败: {str(e)}", 500)
@router.post("/stop-all/{task_def_id}")
async def stop_task_definition(task_def_id: str = Path(..., description="任务定义ID")):
"""
停止指定任务定义下的所有运行任务实例,同时禁用定时任务
Args:
task_def_id: 任务定义ID不是任务记录ID
Returns:
包含停止结果的响应,包括成功停止的任务数量、是否为定时任务等信息
"""
try:
# 调用服务层方法
result = await TaskService.stop_task_def(task_def_id)
if not result.get("success", False):
return error_response(
message=result.get("message", "停止任务失败"),
code=400
)
# 构造返回消息
is_periodic = result.get("is_periodic", False)
total_running = result.get("total_running", 0)
stopped_count = result.get("stopped_count", 0)
failed_count = result.get("failed_count", 0)
# 如果有停止失败的任务,记录警告日志
if failed_count > 0:
failed_tasks = result.get("failed_tasks", [])
logger.warning(f"任务定义 {task_def_id} 停止时有 {failed_count} 个任务停止失败: {failed_tasks}")
message = ""
if is_periodic:
message += "已禁用定时任务并"
if total_running > 0:
message += f"停止了 {stopped_count}/{total_running} 个运行中的任务实例"
else:
message += "没有运行中的任务实例需要停止"
return format_response(
data={
"taskDefId": task_def_id,
"isPeriodic": is_periodic,
"totalRunning": total_running,
"stoppedCount": stopped_count,
"failedCount": result.get("failed_count", 0),
"failedTasks": result.get("failed_tasks", [])
},
message=message
)
except Exception as e:
logger.log_error_with_trace(f"停止任务定义异常", e)
return error_response(message=f"停止任务失败: {str(e)}", code=500)