397 lines
14 KiB
Python
397 lines
14 KiB
Python
#!/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)
|