#!/usr/bin/env python # -*- coding: utf-8 -*- """ 工作流任务超时处理器 负责任务超时检测、提醒、自动处理等功能 使用 APScheduler 实现定时检查 """ import logging from datetime import datetime, timedelta from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession logger = logging.getLogger(__name__) TIMEOUT_CHECK_TASK_FUNC = ( 'online_dev.workflow.engine.task_timeout_process.execute_timeout_check' ) TIMEOUT_CHECK_JOB_CODE = 'workflow_task_timeout_check' def calculate_timeout_datetime(duration: int, unit: str) -> datetime: """ 计算超时时间点 Args: duration: 超时时长 unit: 超时单位 - minute/hour/day Returns: datetime: 超时时间点 """ now = datetime.now() if unit == 'minute': return now + timedelta(minutes=duration) elif unit == 'hour': return now + timedelta(hours=duration) elif unit == 'day': return now + timedelta(days=duration) else: # 默认按小时 return now + timedelta(hours=duration) async def set_task_timeout(db: AsyncSession, task, timeout_config: dict) -> bool: """ 为任务设置超时时间 Args: db: 数据库会话 task: WorkflowTask 实例 timeout_config: 超时配置 {enabled, duration, unit, action} Returns: bool: 是否设置成功 """ if not timeout_config or not timeout_config.get('enabled'): return False try: duration = timeout_config.get('duration', 24) unit = timeout_config.get('unit', 'hour') action = timeout_config.get('action', 'notify') # 计算超时时间 timeout_at = calculate_timeout_datetime(duration, unit) # 更新任务 task.timeout_at = timeout_at task.timeout_action = action task.timeout_notified = False db.add(task) await db.flush() logger.info(f"任务 {task.id} 设置超时: {timeout_at}, 操作: {action}") return True except Exception as e: logger.error(f"设置任务超时失败: {e}") return False async def check_and_handle_timeouts(db: AsyncSession): """ 检查并处理超时任务 此函数应该由定时任务每分钟调用一次 """ from online_dev.workflow.model import WorkflowTask now = datetime.now() # 查找所有已超时但未处理的待办任务 stmt = select(WorkflowTask).where( WorkflowTask.status == 'pending', WorkflowTask.timeout_at <= now, WorkflowTask.timeout_at.isnot(None), WorkflowTask.is_deleted == False, ) result = await db.execute(stmt) timeout_tasks = result.scalars().all() logger.info(f"检查超时任务,发现 {len(timeout_tasks)} 个超时任务") for task in timeout_tasks: try: await _handle_timeout_task(db, task) except Exception as e: logger.exception(f"处理超时任务 {task.id} 失败: {e}") async def _handle_timeout_task(db: AsyncSession, task): """ 处理单个超时任务 """ from online_dev.workflow.model import WorkflowInstance action = task.timeout_action or 'notify' # 获取实例 stmt = select(WorkflowInstance).where(WorkflowInstance.id == task.instance_id) result = await db.execute(stmt) instance = result.scalar_one_or_none() if not instance or instance.status != 'pending': # 流程已结束,跳过 return logger.info(f"处理超时任务: task={task.id}, action={action}") if action == 'notify': # 发送超时提醒通知 await _send_timeout_notification(db, task, instance) elif action == 'auto_approve': # 自动通过 await _auto_complete_task(db, task, instance, 'approve') elif action == 'auto_reject': # 自动拒绝 await _auto_complete_task(db, task, instance, 'reject') async def _send_timeout_notification(db: AsyncSession, task, instance): """ 发送超时提醒通知 """ from online_dev.workflow.model import WorkflowLog from online_dev.workflow.engine.handlers.notification_service import WorkflowNotificationService from core.user.model import User # 检查是否已发送过通知 if task.timeout_notified: return # 获取处理人 stmt = select(User).where(User.id == task.assignee_id) result = await db.execute(stmt) assignee = result.scalar_one_or_none() if not assignee: return # 获取发起人 stmt = select(User).where(User.id == instance.initiator_id) result = await db.execute(stmt) initiator = result.scalar_one_or_none() try: # 发送超时通知 await WorkflowNotificationService.send_timeout_notification( db=db, task=task, instance=instance, assignee=assignee, ) # 标记已通知 task.timeout_notified = True db.add(task) # 记录日志 log = WorkflowLog( instance_id=str(instance.id), node_id=task.node_id, node_name=task.node_name, action='task_timeout_notify', operator_id=str(initiator.id) if initiator else '', comment=f'任务超时提醒已发送给 {assignee.name or assignee.username}', extra_data={ 'task_id': str(task.id), 'assignee_id': str(assignee.id), 'assignee_name': assignee.name or assignee.username, }, ) db.add(log) await db.flush() logger.info(f"已发送超时提醒: task={task.id}, assignee={assignee.id}") except Exception as e: logger.error(f"发送超时提醒失败: {e}") async def _auto_complete_task(db: AsyncSession, task, instance, action_type: str): """ 自动完成超时任务 Args: db: 数据库会话 task: 任务 instance: 流程实例 action_type: 'approve' 或 'reject' """ from online_dev.workflow.model import WorkflowLog, WorkflowDefinition from online_dev.workflow.engine.workflow_engine import WorkflowEngine from online_dev.workflow.engine.base import ExecutionContext from core.user.model import User # 获取发起人作为操作者 stmt = select(User).where(User.id == instance.initiator_id) result = await db.execute(stmt) operator = result.scalar_one_or_none() # 确定操作类型和日志动作 if action_type == 'approve': log_action = 'task_auto_approve' status = 'approved' comment = '任务超时,系统自动通过' else: log_action = 'task_auto_reject' status = 'rejected' comment = '任务超时,系统自动拒绝' # 更新任务状态 task.status = status task.comment = comment task.handled_at = datetime.now() db.add(task) # 记录日志 log = WorkflowLog( instance_id=str(instance.id), node_id=task.node_id, node_name=task.node_name, action=log_action, operator_id=str(operator.id) if operator else '', comment=comment, extra_data={ 'task_id': str(task.id), 'assignee_id': str(task.assignee_id), 'timeout_at': task.timeout_at.isoformat() if task.timeout_at else None, }, ) db.add(log) await db.flush() logger.info(f"任务自动{action_type}: task={task.id}") # 完成该任务对应的钉钉待办 try: from core.message.service import NotifyService await NotifyService.complete_dingtalk_todo(db, "workflow_task", str(task.id)) except Exception as e: logger.warning(f"超时自动处理-清理钉钉待办失败 task={task.id}: {e}") # 获取流程定义 stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id) result = await db.execute(stmt) workflow = result.scalar_one_or_none() if not workflow: return # 使用流程引擎推进流程 engine = WorkflowEngine() operator_id = str(operator.id) if operator else '' context = ExecutionContext( instance=instance, form_data={}, current_user_id=operator_id, flow_definition=workflow.flow_definition, db=db, ) if action_type == 'approve': # 通过:按任务类型推进(办理节点走办理完成逻辑) if task.task_type == 'handle': await engine._handle_handle_completion(context, task) else: await engine._handle_approval(context, task) else: # 拒绝:结束流程 await engine._end_instance(context, 'rejected') async def execute_timeout_check(**kwargs): """ 定时任务入口函数 由调度器每分钟调用 """ from app.database import AsyncSessionLocal logger.info("开始执行任务超时检查...") try: async with AsyncSessionLocal() as db: await check_and_handle_timeouts(db) await db.commit() return "任务超时检查完成" except Exception as e: logger.exception(f"任务超时检查失败: {e}") raise async def create_timeout_check_job(db: AsyncSession) -> bool: """ 创建任务超时检查的定时任务 每分钟执行一次 Returns: bool: 是否创建成功 """ try: from scheduler.model import SchedulerJob from scheduler.service import SchedulerService job_code = TIMEOUT_CHECK_JOB_CODE # 检查是否已存在 stmt = select(SchedulerJob).where(SchedulerJob.code == job_code) result = await db.execute(stmt) existing = result.scalar_one_or_none() scheduler_service = SchedulerService() if existing: updated = False if existing.task_func != TIMEOUT_CHECK_TASK_FUNC: existing.task_func = TIMEOUT_CHECK_TASK_FUNC updated = True logger.warning( f'已修正任务超时检查 task_func: {TIMEOUT_CHECK_TASK_FUNC}' ) if existing.status != 1: existing.status = 1 updated = True if updated: db.add(existing) await db.flush() if scheduler_service.is_running(): try: await scheduler_service.remove_job(job_code) except Exception: pass await scheduler_service.add_job(existing) logger.info(f'任务超时检查定时任务已就绪: {job_code}') return True # 创建定时任务(每分钟执行) job = SchedulerJob( name='工作流任务超时检查', code=job_code, description='每分钟检查一次超时的工作流任务,执行超时提醒或自动处理', group='workflow', trigger_type='interval', interval_seconds=60, # 每60秒执行一次 task_func=TIMEOUT_CHECK_TASK_FUNC, task_kwargs='{}', status=1, # 启用 max_instances=1, coalesce=True, ) db.add(job) await db.flush() # 添加到调度器 if scheduler_service.is_running(): await scheduler_service.add_job(job) logger.info(f"创建任务超时检查定时任务成功: {job_code}") return True except Exception as e: logger.error(f"创建任务超时检查定时任务失败: {e}") return False async def remove_timeout_check_job(db: AsyncSession) -> bool: """ 移除任务超时检查的定时任务 Returns: bool: 是否移除成功 """ try: from scheduler.model import SchedulerJob from scheduler.service import SchedulerService job_code = TIMEOUT_CHECK_JOB_CODE # 从调度器移除 scheduler_service = SchedulerService() if scheduler_service.is_running(): await scheduler_service.remove_job(job_code) # 删除数据库记录 stmt = select(SchedulerJob).where(SchedulerJob.code == job_code) result = await db.execute(stmt) job = result.scalar_one_or_none() if job: await db.delete(job) await db.flush() logger.info(f"移除任务超时检查定时任务成功: {job_code}") return True except Exception as e: logger.error(f"移除任务超时检查定时任务失败: {e}") return False