VWED_server/services/task_edit_service.py
2025-05-12 15:43:21 +08:00

894 lines
36 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 -*-
"""
任务编辑服务模块
提供任务编辑相关的业务逻辑
"""
import json
import uuid
from typing import Dict, List, Any, Optional
from datetime import datetime
from sqlalchemy import select, and_, func, update
from routes.model.task_edit_model import (
TaskBasicInfo, TaskBackupRequest, SubTaskListParams,
TaskEditRunRequest
)
from data.models.taskdef import VWEDTaskDef
from data.models.taskrecord import VWEDTaskRecord
from data.session import get_async_session
from utils.component_manager import component_manager
from data.enum.task_def_enum import EnableStatus, PeriodicTaskStatus, TaskStatusEnum
from data.enum.task_record_enum import TaskStatus
from utils.logger import get_logger
# 获取日志记录器
logger = get_logger("services.task_edit_service")
class TaskEditService:
"""任务编辑服务类"""
@staticmethod
async def get_blocks() -> List[Dict[str, Any]]:
"""
获取系统中所有可用的块数据
用于任务编辑页面左侧的组件面板显示
Returns:
List[Dict[str, Any]]: 所有组件分类及其组件
"""
try:
# 通过组件管理器获取所有块数据
blocks = component_manager.get_all_blocks()
return blocks
except Exception as e:
logger.error(f"获取块数据失败: {str(e)}")
raise
@staticmethod
async def get_task_source(task_id: str) -> Optional[Dict[str, Any]]:
"""
获取指定任务的源码详情数据
Args:
task_id: 任务ID
Returns:
Optional[Dict[str, Any]]: 任务详情数据
"""
try:
async with get_async_session() as session:
# 查询任务定义
result = await session.execute(
select(VWEDTaskDef).where(VWEDTaskDef.id == task_id)
)
task_def = result.scalars().first()
if not task_def:
logger.warning(f"任务不存在: {task_id}")
return None
# 解析详情JSON
detail = json.loads(task_def.detail) if task_def.detail else {}
# 构建响应数据
response = {
"id": task_def.id,
"label": task_def.label,
"version": task_def.version,
"detail": detail,
"delay": task_def.delay,
"ifEnable": task_def.if_enable,
"period": task_def.period,
"periodicTask": task_def.periodic_task,
"remark": task_def.remark,
"status": task_def.status,
"templateName": task_def.template_name,
"templateDescription": task_def.template_name, # 使用模板名称作为描述
"createDate": task_def.created_at,
"createdBy": task_def.created_by
}
return response
except Exception as e:
logger.log_error_with_trace("获取任务源码详情失败", e)
raise
@staticmethod
async def save_task_edit(task_def_data: Dict[str, Any]) -> Dict[str, Any]:
"""
保存任务编辑数据
Args:
task_def_data: 任务定义数据已通过TaskSaveRequest模型验证
必须包含id和detail字段
label字段是可选的
detail字段已被验证包含inputParams、outputParams和rootBlock结构
Returns:
Dict[str, Any]: 保存结果
"""
try:
task_id = task_def_data.get("id")
if not task_id:
logger.error("缺少任务ID")
return {"code": 400, "message": "缺少任务ID", "success": False}
# 在API层已经调用check_task_changes此处不再重复检查
async with get_async_session() as session:
# 查询任务是否存在
result = await session.execute(
select(VWEDTaskDef).where(VWEDTaskDef.id == task_id)
)
task_def = result.scalars().first()
if not task_def:
logger.error(f"任务不存在: {task_id}")
return {"code": 404, "message": "任务不存在", "success": False}
# 更新版本号
new_version = task_def.version + 1
# 只有当提供了label且非空时才更新任务名称
label = task_def_data.get("label")
if label and label.strip():
task_def.label = label
# 转换detail为JSON字符串 - 使用ensure_ascii=False确保中文直接存储而不被编码
detail = task_def_data.get("detail")
if detail:
# detail字段已通过TaskDetailSave模型验证可以直接转换为JSON字符串
task_def.detail = json.dumps(detail, ensure_ascii=False)
task_def.version = new_version
# 保存到数据库
await session.commit()
# 获取当前时间作为更新时间
update_time = datetime.now()
return {
"code": 200,
"message": "保存成功",
"success": True,
"id": task_def.id,
"version": new_version,
"updateTime": update_time
}
except Exception as e:
logger.error(f"保存任务编辑数据失败: {str(e)}")
raise
@staticmethod
async def backup_task(task_id: str, backup_request: TaskBackupRequest) -> Optional[Dict[str, Any]]:
"""
备份任务
Args:
task_id: 源任务ID
backup_request: 备份请求
Returns:
Optional[Dict[str, Any]]: 备份结果
"""
try:
async with get_async_session() as session:
# 查询源任务
result = await session.execute(
select(VWEDTaskDef).where(VWEDTaskDef.id == task_id)
)
source_task = result.scalars().first()
if not source_task:
logger.error(f"源任务不存在: {task_id}")
return None
# 创建新任务ID
new_task_id = str(uuid.uuid4())
# 设置备份任务名称
backup_name = backup_request.backup_name
if not backup_name:
backup_name = f"{source_task.label}-备份"
# 创建备份任务
backup_task = VWEDTaskDef(
id=new_task_id,
label=backup_name,
version=1,
detail=source_task.detail,
delay=source_task.delay,
if_enable=EnableStatus.DISABLED, # 默认禁用
period=source_task.period,
periodic_task=source_task.periodic_task,
remark=backup_request.remark,
status=TaskStatusEnum.PENDING,
template_name=source_task.template_name,
created_by=source_task.created_by,
tenant_id=source_task.tenant_id,
release_sites=source_task.release_sites
)
session.add(backup_task)
await session.commit()
return {
"id": backup_task.id,
"label": backup_task.label,
"version": backup_task.version,
"templateName": backup_task.template_name,
"periodicTask": backup_task.periodic_task,
"ifEnable": backup_task.if_enable,
"status": backup_task.status,
"createDate": backup_task.created_at,
"sourceTaskId": source_task.id,
"remark": backup_task.remark
}
except Exception as e:
logger.error(f"备份任务失败: {str(e)}")
raise
@staticmethod
async def get_subtasks_list(params: SubTaskListParams) -> Dict[str, Any]:
"""
获取子任务列表
Args:
params: 查询参数
Returns:
Dict[str, Any]: 子任务列表数据
"""
try:
async with get_async_session() as session:
# 构建查询条件
query = select(VWEDTaskDef)
# 添加条件
conditions = []
# 排除指定ID
if params.exclude_id:
conditions.append(VWEDTaskDef.id != params.exclude_id)
# 关键词搜索
if params.keyword:
conditions.append(VWEDTaskDef.label.like(f"%{params.keyword}%"))
if conditions:
query = query.where(and_(*conditions))
# 计算总数
count_result = await session.execute(select(
func.count(VWEDTaskDef.id)).where(query.whereclause))
total = count_result.scalar() or 0
# 分页
query = query.order_by(VWEDTaskDef.created_at.desc())
query = query.offset((params.pageNum - 1) * params.pageSize).limit(params.pageSize)
# 执行查询
result = await session.execute(query)
tasks = result.scalars().all()
# 构建返回数据
task_list = []
for task in tasks:
try:
# 解析输入参数
detail = json.loads(task.detail) if task.detail else {}
input_params = detail.get("inputParams", [])
task_list.append({
"id": task.id,
"label": task.label,
"version": task.version,
"templateName": task.template_name,
"remark": task.remark,
"createDate": task.created_at,
"createdBy": task.created_by,
"status": task.status,
"inputParams": input_params
})
except Exception as e:
logger.error(f"解析子任务数据失败: {str(e)}")
return {
"total": total,
"list": task_list
}
except Exception as e:
logger.error(f"获取子任务列表失败: {str(e)}")
raise
@staticmethod
async def run_task(run_request: TaskEditRunRequest, client_ip: str = None, client_info: str = None, tf_api_token: str = None) -> Optional[Dict[str, Any]]:
"""
运行任务
Args:
run_request: 运行任务请求
client_ip: 客户端IP地址
client_info: 客户端设备信息
tf_api_token: 系统任务令牌
Returns:
Optional[Dict[str, Any]]: 运行结果
"""
try:
# 获取任务定义,从数据库加载任务
async with get_async_session() as session:
# 查询任务是否存在
from sqlalchemy import select
from data.models import VWEDTaskDef
result = await session.execute(
select(VWEDTaskDef).where(VWEDTaskDef.id == run_request.taskId)
)
task_def = result.scalars().first()
if not task_def:
logger.error(f"任务不存在: {run_request.taskId}")
return None
# 导入增强版任务调度器
from services.enhanced_scheduler import scheduler
# 转换参数对象为字典格式解决JSON序列化问题
params = []
if run_request.params:
# 使用字典推导式将参数列表转换为字典
params = [param.model_dump() for param in run_request.params]
# 获取当前时间作为任务运行时间
now = datetime.now()
# 设置任务来源信息
source_type = run_request.source_type
source_system = run_request.source_system
source_device = run_request.source_device
map_id = task_def.map_id
source_ip = client_ip # 获取IP地址
source_time = now # 始终使用当前时间
# 记录任务请求信息
logger.info(f"准备启动任务: {run_request.taskId}, 来源: {source_system}, 设备: {source_device}")
logger.debug(f"任务参数: {params}")
# 区分定时任务和普通任务处理
if task_def.periodic_task == PeriodicTaskStatus.PERIODIC:
# 定时任务处理流程
logger.info(f"启动定时任务: {run_request.taskId}")
# 1. 确保定时任务启用
if task_def.if_enable != EnableStatus.ENABLED:
# 如果定时任务未启用,先启用它
async with get_async_session() as session:
task_def.if_enable = EnableStatus.ENABLED
await session.commit()
logger.info(f"已启用定时任务: {run_request.taskId}")
# 2. 通知调度器更新定时任务状态
await scheduler.update_periodic_task(run_request.taskId, True)
# 4. 更新任务定义状态为运行中(1)
async with get_async_session() as session:
from sqlalchemy import update
await session.execute(
update(VWEDTaskDef)
.where(VWEDTaskDef.id == run_request.taskId)
.values(status=TaskStatusEnum.RUNNING, user_token=tf_api_token)
)
await session.commit()
logger.info(f"更新任务定义状态为运行中: {run_request.taskId}")
# 3. 立即执行一次任务
result = await scheduler.run_task(
task_def_id=run_request.taskId,
params=params,
source_type=source_type,
source_system=source_system,
source_device=source_device,
source_ip=source_ip,
source_time=source_time,
source_client_info=client_info,
tf_api_token=tf_api_token,
map_id=map_id
)
if not result.get("success", False):
logger.error(f"启动定时任务失败: {result.get('message')}")
return None
logger.info(f"定时任务启动成功: {run_request.taskId}, 记录ID: {result.get('taskRecordId')}")
# 返回定时任务记录信息
return {
"taskRecordId": result.get("taskRecordId"),
"status": result.get("status"),
"createTime": result.get("createTime"),
"isPeriodic": True,
"period": task_def.period,
"message": "定时任务已启动,将按照设定的周期自动执行"
}
else:
# 普通任务处理流程
logger.info(f"启动普通任务: {run_request.taskId}")
async with get_async_session() as session:
from sqlalchemy import update
await session.execute(
update(VWEDTaskDef)
.where(VWEDTaskDef.id == run_request.taskId)
.values(user_token=tf_api_token)
)
await session.commit()
result = await scheduler.run_task(
task_def_id=run_request.taskId,
params=params,
source_type=source_type,
source_system=source_system,
source_device=source_device,
source_ip=source_ip,
source_time=source_time,
source_client_info=client_info,
tf_api_token=tf_api_token,
map_id=map_id
)
if not result.get("success", False):
logger.error(f"启动任务失败: {result.get('message')}")
return None
logger.info(f"普通任务启动成功: {run_request.taskId}, 记录ID: {result.get('taskRecordId')}")
# 返回任务记录信息
return {
"taskRecordId": result.get("taskRecordId"),
"status": result.get("status"),
"createTime": result.get("createTime"),
"isPeriodic": False
}
except Exception as e:
logger.log_error_with_trace("运行任务失败", e)
raise
@staticmethod
async def save_input_params(task_id: str, input_params: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
保存任务输入参数配置
Args:
task_id: 任务ID
input_params: 输入参数列表
Returns:
Dict[str, Any]: 保存结果,包含是否成功、是否有变化的信息
"""
try:
async with get_async_session() as session:
# 查询任务定义
result = await session.execute(
select(VWEDTaskDef).where(VWEDTaskDef.id == task_id)
)
task_def = result.scalars().first()
if not task_def:
logger.error(f"任务不存在: {task_id}")
return {"success": False, "changed": False, "message": "任务不存在"}
# 解析详情JSON
detail = json.loads(task_def.detail) if task_def.detail else {}
# 获取现有输入参数
current_params = detail.get("inputParams", [])
# 比较是否有变化 - 将两个列表转为JSON字符串比较
current_params_str = json.dumps(current_params, sort_keys=True, ensure_ascii=False)
new_params_str = json.dumps(input_params, sort_keys=True, ensure_ascii=False)
has_changes = current_params_str != new_params_str
if not has_changes:
return {"success": True, "changed": False, "message": "数据未发生变化"}
# 更新输入参数
detail["inputParams"] = input_params
# 更新任务定义 - 使用ensure_ascii=False确保中文直接存储而不被编码
task_def.detail = json.dumps(detail, ensure_ascii=False)
task_def.version += 1
# 保存到数据库
await session.commit()
# 获取当前时间作为更新时间
update_time = datetime.now()
return {
"success": True,
"changed": True,
"message": "保存成功",
"version": task_def.version,
"updateTime": update_time
}
except Exception as e:
logger.error(f"保存输入参数失败: {str(e)}")
raise
@staticmethod
async def save_basic_settings(task_id: str, basic_info: TaskBasicInfo) -> Dict[str, Any]:
"""
保存任务基本设置
Args:
task_id: 任务ID
basic_info: 基本信息
Returns:
Dict[str, Any]: 保存结果,包含是否成功、是否有变化的信息
"""
try:
async with get_async_session() as session:
# 查询任务定义
result = await session.execute(
select(VWEDTaskDef).where(VWEDTaskDef.id == task_id)
)
task_def = result.scalars().first()
if not task_def:
logger.error(f"任务不存在: {task_id}")
return {"success": False, "changed": False, "message": "任务不存在"}
# 检查是否有变化
release_sites_new = 1 if basic_info.releaseSites else 0
has_changes = (
task_def.label != basic_info.label or
task_def.remark != basic_info.remark or
task_def.release_sites != release_sites_new
)
if not has_changes:
return {
"success": True,
"changed": False,
"message": "数据未发生变化",
"id": task_def.id,
"label": task_def.label,
"remark": task_def.remark,
"releaseSites": bool(task_def.release_sites)
}
# 更新基本信息
task_def.label = basic_info.label
task_def.remark = basic_info.remark
task_def.release_sites = release_sites_new
# 保存到数据库
await session.commit()
# 获取当前时间作为更新时间
update_time = datetime.now()
return {
"success": True,
"changed": True,
"message": "保存成功",
"id": task_def.id,
"label": task_def.label,
"remark": task_def.remark,
"releaseSites": bool(task_def.release_sites),
"updateTime": update_time
}
except Exception as e:
logger.error(f"保存基本设置失败: {str(e)}")
raise
@staticmethod
async def get_common_params() -> List[Dict[str, Any]]:
"""
获取常用参数字段
Returns:
List[Dict[str, Any]]: 常用参数字段列表
"""
try:
# 导入内置函数管理模块
from utils.built_in_functions import get_function_list
# 获取所有内置函数
built_in_functions = get_function_list()
# 常用参数字段列表
common_params = [
{
"id": "robotId",
"label": "机器人id",
"type": "String",
"description": "机器人唯一标识",
"order": 1,
"values": [
{"id": "robot_001", "name": "AGV-001", "description": "1号AGV机器人"},
{"id": "robot_002", "name": "AGV-002", "description": "2号AGV机器人"},
{"id": "robot_003", "name": "AGV-003", "description": "3号AGV机器人"}
]
},
{
"id": "robotGroup",
"label": "机器人组",
"type": "Array",
"description": "机器人分组信息",
"order": 2,
"values": [
{"id": "group1", "name": "分拣组", "description": "负责物品分拣的机器人组"},
{"id": "group2", "name": "运输组", "description": "负责物品运输的机器人组"},
{"id": "group3", "name": "装配组", "description": "负责物品装配的机器人组"}
]
},
{
"id": "siteId",
"label": "库位id",
"type": "String",
"description": "库位唯一标识",
"order": 3,
"values": [
{"id": "site_001", "name": "A01", "description": "A区1号库位"},
{"id": "site_002", "name": "A02", "description": "A区2号库位"},
{"id": "site_003", "name": "B01", "description": "B区1号库位"}
]
},
{
"id": "area",
"label": "库区",
"type": "String",
"description": "库区信息",
"order": 4,
"values": [
{"id": "area_A", "name": "A区", "description": "A区储存区"},
{"id": "area_B", "name": "B区", "description": "B区储存区"},
{"id": "area_C", "name": "C区", "description": "C区装配区"}
]
},
{
"id": "binTask",
"label": "binTask",
"type": "String",
"description": "动作类型",
"order": 5,
"values": [
{"id": "QuickLoad", "name": "入库", "description": "物品入库"},
{"id": "QuickUnLoad", "name": "出库", "description": "物品出库"},
]
},
{
"id": "user",
"label": "用户",
"type": "String",
"description": "用户信息",
"order": 6,
"values": [
{"id": "user_001", "name": "张三", "description": "张三用户"},
{"id": "user_002", "name": "李四", "description": "李四用户"},
]
},
{
"id": "cache",
"label": "缓存",
"type": "String",
"description": "缓存信息",
"order": 7,
"values": [
{"id": "cache_001", "name": "cache1", "description": "cache1"},
{"id": "cache_002", "name": "cache2", "description": "cache2"},
]
},
{
"id": "builtInFunction",
"label": "内置函数",
"type": "String",
"description": "系统内置函数",
"order": 8,
"values": [func for func in built_in_functions]
}
]
return common_params
except Exception as e:
logger.error(f"获取常用参数字段失败: {str(e)}")
raise
@staticmethod
async def get_task_input_params(task_id: str) -> Dict[str, Any]:
"""
获取指定任务的输入参数配置
Args:
task_id: 任务ID
Returns:
Dict[str, Any]: 包含输入参数配置和系统参数类型信息
"""
try:
async with get_async_session() as session:
# 查询任务定义
result = await session.execute(
select(VWEDTaskRecord).where(VWEDTaskRecord.id == task_id)
)
task_def = result.scalars().first()
if not task_def:
logger.error(f"任务不存在: {task_id}")
return {"success": False, "message": "任务不存在"}
# 解析详情JSON
input_params = json.loads(task_def.input_params) if task_def.input_params else {}
# 返回完整信息
return {
"success": True,
"message": "获取任务输入参数配置成功",
"data": {
"inputParams": input_params
}
}
except Exception as e:
logger.error(f"获取任务输入参数配置失败: {str(e)}")
raise
@staticmethod
async def get_basic_settings(task_id: str) -> Dict[str, Any]:
"""
获取任务的基本设置信息
Args:
task_id: 任务ID
Returns:
Dict[str, Any]: 任务基本信息
"""
try:
async with get_async_session() as session:
# 查询任务定义
result = await session.execute(
select(VWEDTaskDef).where(VWEDTaskDef.id == task_id)
)
task_def = result.scalars().first()
if not task_def:
logger.error(f"任务不存在: {task_id}")
return {"success": False, "message": "任务不存在"}
# 返回基本信息
return {
"success": True,
"data": {
"id": task_def.id,
"label": task_def.label,
"remark": task_def.remark,
"releaseSites": bool(task_def.release_sites),
"version": task_def.version,
"createDate": task_def.created_at
}
}
except Exception as e:
logger.error(f"获取任务基本设置失败: {str(e)}")
raise
@staticmethod
async def check_task_changes(task_id: str, task_def_data: Dict[str, Any]) -> Dict[str, Any]:
"""
检查任务数据是否有变化
Args:
task_id: 任务ID
task_def_data: 待保存的任务数据
Returns:
Dict[str, Any]: 检查结果,包含是否发生变化的标志
"""
try:
if not task_id:
return {"changed": True} # 无法检查,默认为已变化
async with get_async_session() as session:
# 查询现有任务数据
result = await session.execute(
select(VWEDTaskDef).where(VWEDTaskDef.id == task_id)
)
task_def = result.scalars().first()
if not task_def:
return {"changed": True} # 任务不存在,视为有变化
# 检查label是否有变化
label = task_def_data.get("label")
if label and label.strip() and label.strip() != task_def.label:
return {"changed": True, "reason": "label变化"}
# 检查detail是否有变化
detail = task_def_data.get("detail")
if not detail:
# 没有提供detail可能只更新了label
return {"changed": label and label.strip() and label.strip() != task_def.label}
# 比较detail
current_detail = json.loads(task_def.detail) if task_def.detail else {}
# 将提供的detail转为JSON字符串排序键以确保一致的比较结果
current_detail_str = json.dumps(current_detail, sort_keys=True, ensure_ascii=False)
new_detail_str = json.dumps(detail, sort_keys=True, ensure_ascii=False)
if current_detail_str != new_detail_str:
return {"changed": True, "reason": "detail变化"}
# 数据未变化
return {
"changed": False,
"id": task_def.id,
"version": task_def.version
}
except Exception as e:
logger.error(f"检查任务数据变化失败: {str(e)}")
# 出错时默认为有变化,以确保数据安全
return {"changed": True, "error": str(e)}
@staticmethod
async def check_running_task_for_device(task_id: str, device_id: str) -> Dict[str, Any]:
"""
检查指定设备是否有正在运行的相同任务
Args:
task_id: 任务定义ID
device_id: 设备ID
Returns:
Dict[str, Any]: 检查结果,包含是否允许启动任务的信息
{
"has_running_task": bool, # 是否有运行中的任务
"allow_restart": bool, # 是否允许重启
"message": str # 提示消息
}
"""
try:
from data.enum.task_record_enum import TaskStatus
async with get_async_session() as session:
# 查询是否存在相同任务ID且相同设备且正在运行的任务
result = await session.execute(
select(VWEDTaskRecord).where(
VWEDTaskRecord.def_id == task_id,
VWEDTaskRecord.source_device == device_id,
VWEDTaskRecord.status == TaskStatus.RUNNING # 状态为"进行中"
)
)
running_tasks = result.scalars().all()
# 如果没有运行中的任务,直接允许启动
if not running_tasks:
return {
"has_running_task": False,
"allow_restart": True,
"message": "没有运行中的任务,可以启动"
}
# 如果存在运行中的任务检查allow_restart_same_location字段
allow_restart = True
for task in running_tasks:
# 如果任何一个任务不允许重启,则整体不允许
if not task.allow_restart_same_location:
allow_restart = False
break
return {
"has_running_task": True,
"allow_restart": allow_restart,
"message": "相同设备已有此任务正在运行中" + (",但允许再次启动" if allow_restart else ",请等待任务完成后再次启动")
}
except Exception as e:
logger.error(f"检查运行中任务失败: {str(e)}")
raise