2025-04-30 16:57:46 +08:00
|
|
|
|
#!/usr/bin/env python
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
天风任务管理API路由
|
|
|
|
|
实现任务的列表查询、创建、删除、运行等功能
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
2025-05-12 15:43:21 +08:00
|
|
|
|
from typing import Optional, Dict, Any
|
|
|
|
|
from fastapi import APIRouter, Depends, File, UploadFile, Form, Response, Path
|
2025-04-30 16:57:46 +08:00
|
|
|
|
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 (
|
2025-05-12 15:43:21 +08:00
|
|
|
|
CreateTaskRequest, DeleteTaskRequest, ExportBatchRequest, TaskResponse, TaskListParams
|
2025-04-30 16:57:46 +08:00
|
|
|
|
)
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-12 15:43:21 +08:00
|
|
|
|
@router.get("/list", response_model=ApiResponse)
|
2025-04-30 16:57:46 +08:00
|
|
|
|
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)
|
2025-05-12 15:43:21 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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)
|