#!/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.backupName # backup_reques 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, map_id=source_task.map_id, user_token=source_task.user_token ) 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 { "success": True, "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 result logger.info(f"普通任务启动成功: {run_request.taskId}, 记录ID: {result.get('taskRecordId')}") # 返回任务记录信息 return { "success": True, "message": "任务启动成功", "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