#!/usr/bin/env python # -*- coding: utf-8 -*- """ 延时节点回调函数 当延时到期后,由调度器调用此函数推进工作流 """ import logging from datetime import datetime from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession logger = logging.getLogger(__name__) def get_delay_job_id(instance_id: str, node_id: str) -> str: """生成延时任务的唯一 job_id""" return f"wf_delay_{instance_id}_{node_id}" async def workflow_delay_callback(instance_id: str, node_id: str): """ 延时到期回调 - 推进工作流 由调度器在延时到期后调用,负责: 1. 加载流程实例,校验状态 2. 清除延时状态 3. 记录延时完成日志 4. 推进流程到下一节点 Args: instance_id: 流程实例ID node_id: 延时节点ID """ from app.database import AsyncSessionLocal logger.info(f"延时回调触发: instance_id={instance_id}, node_id={node_id}") async with AsyncSessionLocal() as db: try: await _execute_delay_resume(db, instance_id, node_id) await db.commit() logger.info(f"延时回调完成: instance_id={instance_id}, node_id={node_id}") except Exception as e: await db.rollback() logger.error(f"延时回调失败: instance_id={instance_id}, node_id={node_id}, error={e}", exc_info=True) raise async def _execute_delay_resume(db: AsyncSession, instance_id: str, node_id: str): """执行延时恢复逻辑""" from online_dev.workflow.model import WorkflowInstance, WorkflowDefinition, WorkflowLog from online_dev.workflow.engine.base import ExecutionContext from online_dev.workflow.engine.utils import FlowUtils # 1. 加载流程实例 stmt = select(WorkflowInstance).where( WorkflowInstance.id == instance_id, WorkflowInstance.is_deleted == False, ) result = await db.execute(stmt) instance = result.scalar_one_or_none() if not instance: logger.warning(f"延时回调: 流程实例不存在 {instance_id}") return # 2. 校验实例状态 if instance.status != 'pending': logger.info(f"延时回调: 流程实例状态非 pending ({instance.status}),跳过 {instance_id}") return # 3. 校验延时节点匹配 if instance.delay_node_id != node_id: logger.warning( f"延时回调: 节点不匹配,期望 {node_id},实际 {instance.delay_node_id},跳过" ) return # 4. 加载流程定义 stmt = select(WorkflowDefinition).where( WorkflowDefinition.id == instance.workflow_id, WorkflowDefinition.is_deleted == False, ) result = await db.execute(stmt) definition = result.scalar_one_or_none() if not definition or not definition.flow_definition: logger.error(f"延时回调: 流程定义不存在或无效 workflow_id={instance.workflow_id}") return flow_def = definition.flow_definition # 5. 查找延时节点 delay_node = FlowUtils.find_node_by_id(flow_def.get('nodes'), node_id) if not delay_node: logger.error(f"延时回调: 延时节点不存在 node_id={node_id}") return # 6. 清除实例的延时状态 instance.delay_node_id = "" instance.delay_until = None db.add(instance) await db.flush() # 7. 记录延时完成日志 log = WorkflowLog( instance_id=str(instance.id), node_id=node_id, node_name=delay_node.get('name', '延时等待'), action='delay_complete', operator_id='', comment='延时等待结束,流程继续', ) db.add(log) await db.flush() # 8. 构建执行上下文并推进流程 from online_dev.workflow.engine.utils import FormDataUtils form_data = {} if instance.form_code and instance.form_data_id: try: form_data = await FormDataUtils.load_form_data(db, instance.form_code, instance.form_data_id) except Exception as e: logger.warning(f"延时回调: 加载表单数据失败: {e}") context = ExecutionContext( instance=instance, form_data=form_data, current_user_id='', flow_definition=flow_def, db=db, ) # 9. 推进到下一节点 from online_dev.workflow.engine.workflow_engine import WorkflowEngine engine = WorkflowEngine() await engine._advance_to_next(context, delay_node) logger.info(f"延时回调: 流程已推进 instance_id={instance_id}") async def recover_pending_delay_tasks(): """ 应用启动时恢复未完成的延时任务 查询所有处于延时等待状态的流程实例,重新注册定时任务: - delay_until > now: 注册定时任务等待到期 - delay_until <= now: 直接执行回调推进流程 """ from app.database import AsyncSessionLocal from online_dev.workflow.model import WorkflowInstance logger.info("开始恢复未完成的延时任务...") try: async with AsyncSessionLocal() as db: # 查询所有有延时状态的 pending 实例 stmt = select(WorkflowInstance).where( WorkflowInstance.status == 'pending', WorkflowInstance.delay_node_id != '', WorkflowInstance.delay_node_id.isnot(None), WorkflowInstance.delay_until.isnot(None), WorkflowInstance.is_deleted == False, ) result = await db.execute(stmt) instances = list(result.scalars().all()) if not instances: logger.info("没有需要恢复的延时任务") return now = datetime.now() recovered = 0 expired = 0 for instance in instances: instance_id = str(instance.id) node_id = instance.delay_node_id delay_until = instance.delay_until if delay_until > now: # 未到期,重新注册定时任务 success = await _register_delay_job(instance_id, node_id, delay_until) if success: recovered += 1 logger.info( f"恢复延时任务: instance={instance_id}, node={node_id}, " f"到期时间={delay_until}" ) else: # 已过期,直接执行回调 expired += 1 logger.info( f"延时任务已过期,立即执行: instance={instance_id}, node={node_id}" ) try: await workflow_delay_callback(instance_id, node_id) except Exception as e: logger.error(f"执行过期延时回调失败: {e}", exc_info=True) logger.info(f"延时任务恢复完成: 重新注册 {recovered} 个, 立即执行 {expired} 个") except Exception as e: logger.error(f"恢复延时任务失败: {e}", exc_info=True) async def _register_delay_job(instance_id: str, node_id: str, delay_until: datetime) -> bool: """ 注册延时定时任务到调度器 Args: instance_id: 流程实例ID node_id: 延时节点ID delay_until: 到期时间 Returns: 是否注册成功 """ try: from scheduler.service import scheduler_service scheduler = scheduler_service.get_scheduler() if not scheduler: logger.warning("调度器未初始化,无法注册延时任务") return False job_id = get_delay_job_id(instance_id, node_id) # 创建回调包装函数(闭包捕获参数) _instance_id = instance_id _node_id = node_id async def delay_wrapper(): await workflow_delay_callback(_instance_id, _node_id) # 注册任务 await scheduler.configure_task(job_id, func=delay_wrapper) # 添加一次性调度 from apscheduler.triggers.date import DateTrigger await scheduler.add_schedule( func_or_task_id=job_id, trigger=DateTrigger(run_time=delay_until), id=job_id, ) logger.info(f"延时任务已注册: job_id={job_id}, 到期时间={delay_until}") return True except Exception as e: logger.error(f"注册延时任务失败: {e}", exc_info=True) return False async def remove_delay_job(instance_id: str, node_id: str) -> bool: """ 从调度器移除延时任务 Args: instance_id: 流程实例ID node_id: 延时节点ID Returns: 是否移除成功 """ try: from scheduler.service import scheduler_service scheduler = scheduler_service.get_scheduler() if not scheduler: return False job_id = get_delay_job_id(instance_id, node_id) try: await scheduler.remove_schedule(job_id) logger.info(f"延时任务已移除: job_id={job_id}") except Exception: # 任务可能不存在(已执行或已清理) pass return True except Exception as e: logger.error(f"移除延时任务失败: {e}", exc_info=True) return False