#!/usr/bin/env python # -*- coding: utf-8 -*- """ 工作流引擎(异步版本) 负责流程的执行、推进、状态管理等核心逻辑 """ import logging from datetime import datetime from typing import Dict, Optional, Any from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession from online_dev.workflow.engine.assignee_resolver import assignee_resolver from online_dev.workflow.engine.base import ( ExecutionContext, TaskAction, generate_instance_no, ) from online_dev.workflow.engine.condition_evaluator import condition_evaluator from online_dev.workflow.engine.utils import FlowUtils, FormDataUtils logger = logging.getLogger(__name__) class WorkflowEngine: """ 工作流引擎 职责: 1. 流程启动 - 创建实例,执行第一个节点 2. 任务处理 - 审批/拒绝/转交 3. 流程推进 - 根据条件判断下一节点 4. 状态管理 - 更新实例和任务状态 """ def __init__(self): self.condition_evaluator = condition_evaluator self.assignee_resolver = assignee_resolver self._handlers = {} def _get_handler(self, node_type: str): """获取节点处理器(懒加载)""" if node_type not in self._handlers: from online_dev.workflow.engine.handlers import ( ApprovalHandler, HandleHandler, CopyHandler, ConditionHandler, ParallelHandler, DelayNodeHandler, NotifyHandler, ServiceHandler, SubflowHandler, DataUpdateHandler, ) handler_map = { 'approval': ApprovalHandler, 'handle': HandleHandler, 'copy': CopyHandler, 'condition': ConditionHandler, 'parallel': ParallelHandler, 'delay': DelayNodeHandler, 'notify': NotifyHandler, 'service': ServiceHandler, 'subflow': SubflowHandler, 'data_update': DataUpdateHandler, } handler_class = handler_map.get(node_type) if handler_class: self._handlers[node_type] = handler_class(self) return self._handlers.get(node_type) # ==================== 流程启动 ==================== async def start( self, db: AsyncSession, workflow: Any, title: str, form_data: Dict, initiator_id: str, ) -> Any: """ 启动流程 Args: db: 数据库会话 workflow: 流程定义 title: 流程标题 form_data: 表单数据 initiator_id: 发起人ID Returns: WorkflowInstance: 流程实例 """ from online_dev.workflow.model import WorkflowInstance, WorkflowLog # 缓存 workflow 属性,避免 save_form_data 中 db.commit() 导致 session 对象过期 _workflow_id = str(workflow.id) _form_code = workflow.form_code _flow_definition = workflow.flow_definition # 1. 保存表单数据 form_data_id = await FormDataUtils.save_form_data(db, _form_code, form_data) # 2. 重新加载展平后的表单数据用于条件评估等引擎逻辑 # 前端传入的 form_data 可能是 {main: {...}, sub_tables: {...}} 嵌套结构, # 而引擎的条件评估器需要展平的 {field: value} 结构 flat_form_data = await FormDataUtils.load_form_data(db, _form_code, form_data_id) # 3. 创建流程实例 instance = WorkflowInstance( workflow_id=_workflow_id, instance_no=generate_instance_no(), title=title, status='pending', initiator_id=initiator_id, form_code=_form_code, form_data_id=form_data_id, current_node_id='start', current_node_name='开始', ) db.add(instance) await db.flush() # 记录启动日志 log = WorkflowLog( instance_id=str(instance.id), node_id='start', node_name='开始', action='start', operator_id=initiator_id, comment='发起流程', ) db.add(log) await db.flush() # 创建执行上下文 context = ExecutionContext( instance=instance, form_data=flat_form_data or form_data, current_user_id=initiator_id, flow_definition=_flow_definition, db=db, ) # 执行开始节点,推进到下一节点 start_node = context.flow_definition.get('nodes') if start_node and start_node.get('type') == 'start': await self._advance_to_next(context, start_node) else: logger.error("找不到开始节点!") await db.refresh(instance) return instance async def restart_instance(self, db: AsyncSession, instance: Any, user_id: str) -> Any: """ 重新启动流程实例(驳回修改后重新提交) """ from online_dev.workflow.model import WorkflowDefinition stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id) result = await db.execute(stmt) workflow = result.scalar_one_or_none() if not workflow: raise ValueError("流程定义不存在") flow_definition = workflow.flow_definition if not flow_definition: raise ValueError("流程定义为空") # 加载表单数据 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"restart_instance: 加载表单数据失败: {e}") context = ExecutionContext( instance=instance, form_data=form_data, flow_definition=flow_definition, current_user_id=user_id, db=db, ) start_node = flow_definition.get('nodes', {}) if start_node and start_node.get('type') == 'start': instance.status = 'pending' db.add(instance) await db.flush() await self._advance_to_next(context, start_node) await db.refresh(instance) return instance # ==================== 任务处理 ==================== async def complete_task( self, db: AsyncSession, task: Any, action: TaskAction, comment: str, user_id: str, form_data: Optional[Dict] = None, return_to: str = None, ) -> Any: """ 完成任务 Args: db: 数据库会话 task: 任务 action: 操作(approve/reject/return) comment: 审批意见 user_id: 操作用户ID form_data: 表单数据(可能被修改) return_to: 驳回目标(当 action=RETURN 时使用) Returns: WorkflowTask: 更新后的任务 """ from online_dev.workflow.model import WorkflowLog, WorkflowInstance, WorkflowDefinition # 获取实例 stmt = select(WorkflowInstance).where(WorkflowInstance.id == task.instance_id) result = await db.execute(stmt) instance = result.scalar_one_or_none() if not instance: raise ValueError("流程实例不存在") # 获取流程定义 stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id) result = await db.execute(stmt) workflow = result.scalar_one_or_none() if not workflow: raise ValueError("流程定义不存在") # 更新任务状态 status_map = { TaskAction.APPROVE: 'approved', TaskAction.REJECT: 'rejected', TaskAction.RETURN: 'returned', TaskAction.DELEGATE: 'delegated', } task.status = status_map.get(action, action.value + 'd') task.comment = comment task.handled_at = datetime.now() db.add(task) await db.flush() # 记录日志 log_extra_data = {} # 如果有签名,记录到日志中 if task.signature_file_id: log_extra_data['signature_file_id'] = task.signature_file_id if action == TaskAction.RETURN: log_extra_data['return_to'] = return_to or 'initiator' # 解析实际驳回目标节点名称 if return_to == 'initiator' or not return_to: log_extra_data['return_to_name'] = '发起人' elif return_to == 'previous': prev_node_id = await self._find_previous_node_id( ExecutionContext( instance=instance, form_data={}, current_user_id=user_id, flow_definition=workflow.flow_definition, db=db, ), task.node_id, ) if prev_node_id: prev_node = FlowUtils.find_node_by_id(workflow.flow_definition, prev_node_id) log_extra_data['return_to_name'] = prev_node.get('name', prev_node_id) if prev_node else prev_node_id else: log_extra_data['return_to_name'] = '发起人' else: target_node = FlowUtils.find_node_by_id(workflow.flow_definition, return_to) log_extra_data['return_to_name'] = target_node.get('name', return_to) if target_node else return_to log = WorkflowLog( instance_id=str(instance.id), node_id=task.node_id, node_name=task.node_name, action=action.value, operator_id=user_id, comment=comment, extra_data=log_extra_data, ) db.add(log) await db.flush() # 获取表单数据 if form_data is None: form_data = await FormDataUtils.load_form_data(db, instance.form_code, instance.form_data_id) # 创建执行上下文 context = ExecutionContext( instance=instance, form_data=form_data, current_user_id=user_id, flow_definition=workflow.flow_definition, db=db, ) # 获取当前节点配置 current_node = FlowUtils.find_node_by_id(context.flow_definition, task.node_id) # 完成该任务对应的钉钉待办 if action in (TaskAction.APPROVE, TaskAction.REJECT, TaskAction.RETURN): 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"完成钉钉待办失败: {e}") # 根据操作推进流程 if action == TaskAction.REJECT: # 发送发起人通知(拒绝) try: from online_dev.workflow.engine.handlers.notification_service import WorkflowNotificationService await WorkflowNotificationService.send_initiator_notification(context, current_node or {}, 'reject') except Exception as e: logger.error(f"发送发起人通知失败: {e}") await self._end_instance(context, 'rejected') elif action == TaskAction.APPROVE: # 发送发起人通知(通过) try: from online_dev.workflow.engine.handlers.notification_service import WorkflowNotificationService await WorkflowNotificationService.send_initiator_notification(context, current_node or {}, 'approve') except Exception as e: logger.error(f"发送发起人通知失败: {e}") await self._handle_approval(context, task) elif action == TaskAction.RETURN: await self._handle_return(context, task, return_to) return task async def transfer_task( self, db: AsyncSession, task: Any, to_user: Any, comment: str, user_id: str, ) -> Any: """转交任务""" from online_dev.workflow.model import WorkflowTask, WorkflowLog task.status = 'transferred' task.comment = comment task.handled_at = datetime.now() task.transferred_to_id = str(to_user.id) db.add(task) await db.flush() # 完成原任务对应的钉钉待办 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}") new_task = WorkflowTask( instance_id=task.instance_id, node_id=task.node_id, node_name=task.node_name, task_type=task.task_type, status='pending', assignee_id=str(to_user.id), parent_task_id=str(task.id), sign_type='transfer', timeout_at=task.timeout_at, timeout_action=task.timeout_action or '', timeout_notified=False, ) db.add(new_task) await db.flush() try: from online_dev.workflow.engine.handlers.notification_service import ( WorkflowNotificationService, ) instance, flow_definition = await WorkflowNotificationService.load_instance_and_flow( db, new_task ) if instance: await WorkflowNotificationService.notify_pending_task( db=db, task=new_task, instance=instance, flow_definition=flow_definition, operator_id=user_id, ) except Exception as e: logger.warning(f'转办后发送任务通知失败 task={new_task.id}: {e}') log = WorkflowLog( instance_id=task.instance_id, node_id=task.node_id, node_name=task.node_name, action='transfer', operator_id=user_id, comment=f"转交给 {to_user.name or to_user.username}: {comment}", extra_data={'to_user_id': str(to_user.id)}, ) db.add(log) await db.flush() return new_task # ==================== 流程推进 ==================== async def _handle_approval(self, context: ExecutionContext, task: Any) -> None: """处理审批通过后的流程推进""" handler = self._get_handler('approval') if handler: await handler.handle_approval(context, task) async def _handle_handle_completion(self, context: ExecutionContext, task: Any) -> None: """处理办理任务完成后的流程推进""" handler = self._get_handler('handle') if handler: await handler.handle_completion(context, task) async def _handle_return(self, context: ExecutionContext, task: Any, return_to: str = None) -> None: """处理驳回操作""" from online_dev.workflow.model import WorkflowTask # 先收集待取消任务的ID,用于清理钉钉待办 pending_stmt = select(WorkflowTask.id).where( WorkflowTask.instance_id == str(context.instance.id), WorkflowTask.node_id == task.node_id, WorkflowTask.status == 'pending', ) pending_result = await context.db.execute(pending_stmt) canceled_task_ids = [str(row[0]) for row in pending_result.all()] # 取消当前节点的所有待处理任务 stmt = update(WorkflowTask).where( WorkflowTask.instance_id == str(context.instance.id), WorkflowTask.node_id == task.node_id, WorkflowTask.status == 'pending', ).values(status='canceled') await context.db.execute(stmt) # 清理被取消任务的钉钉待办 for tid in canceled_task_ids: try: from core.message.service import NotifyService await NotifyService.complete_dingtalk_todo(context.db, "workflow_task", tid) except Exception as e: logger.warning(f"驳回-清理钉钉待办失败 task={tid}: {e}") if return_to == 'initiator' or not return_to: await self._return_to_initiator(context, task) elif return_to == 'previous': previous_node_id = await self._find_previous_node_id(context, task.node_id) if previous_node_id: await self._return_to_node(context, task, previous_node_id) else: await self._return_to_initiator(context, task) else: await self._return_to_node(context, task, return_to) async def _return_to_initiator(self, context: ExecutionContext, task: Any) -> None: """驳回给发起人""" from online_dev.workflow.model import WorkflowTask instance = context.instance instance.current_node_id = 'start' instance.current_node_name = '待修改' context.db.add(instance) await context.db.flush() # 创建发起人的修改任务 revise_task = WorkflowTask( instance_id=str(instance.id), node_id='start', node_name='待修改', task_type='revise', status='pending', assignee_id=instance.initiator_id, ) context.db.add(revise_task) await context.db.flush() async def _return_to_node(self, context: ExecutionContext, task: Any, node_id: str) -> None: """驳回到指定节点""" target_node = FlowUtils.find_node_by_id(context.flow_definition, node_id) if not target_node: logger.warning(f"找不到驳回目标节点: {node_id}") await self._return_to_initiator(context, task) return context.instance.current_node_id = node_id context.instance.current_node_name = target_node.get('name', '') context.db.add(context.instance) await context.db.flush() node_type = target_node.get('type') handler = self._get_handler(node_type) if handler: await handler.execute(context, target_node) async def _find_previous_node_id(self, context: ExecutionContext, current_node_id: str) -> Optional[str]: """查找上一个节点的ID""" from online_dev.workflow.model import WorkflowLog stmt = select(WorkflowLog).where( WorkflowLog.instance_id == str(context.instance.id), WorkflowLog.action.in_(['approve', 'return']), WorkflowLog.node_id != current_node_id, ).order_by(WorkflowLog.sys_create_datetime.desc()).limit(1) result = await context.db.execute(stmt) previous_log = result.scalar_one_or_none() if previous_log and previous_log.node_id: return previous_log.node_id return FlowUtils.find_parent_node_id(context.flow_definition, current_node_id) async def _advance_to_next(self, context: ExecutionContext, current_node: Dict) -> None: """推进到下一节点""" logger.info(f"_advance_to_next - 当前节点: {current_node.get('id')}, {current_node.get('type')}") next_node = self._get_next_node(context, current_node) logger.info(f"_advance_to_next - 下一节点: {next_node}") if not next_node: current_id = current_node.get('id', '') # 查找最内层的分支归属(正确处理嵌套:并行内嵌条件、条件内嵌并行等) branch_info = FlowUtils.find_innermost_branch_for_node( context.flow_definition, current_id ) if branch_info: branch_type, branch_node, branch = branch_info if branch_type == 'parallel' and branch is not None: handler = self._get_handler('parallel') if handler: await handler.mark_branch_complete(context, branch_node, branch) return elif branch_type == 'condition': logger.info(f"条件分支内节点 {current_id} 完成,从条件节点 {branch_node.get('id')} 继续推进") await self._advance_to_next(context, branch_node) return logger.warning(f"找不到下一节点,当前节点: {current_id}") return node_type = next_node.get('type') if node_type == 'end': await self._end_instance(context, 'approved') else: handler = self._get_handler(node_type) if handler: await handler.execute(context, next_node) else: logger.warning(f"未知节点类型: {node_type},自动跳过") await self._advance_to_next(context, next_node) def _get_next_node(self, context: ExecutionContext, current_node: Dict) -> Optional[Dict]: """获取下一节点""" children = current_node.get('children') if children: return children return None # ==================== 流程结束 ==================== async def _end_instance(self, context: ExecutionContext, status: str, current_node: Dict = None) -> None: """结束流程实例""" from online_dev.workflow.model import WorkflowTask instance = context.instance instance.status = status instance.completed_at = datetime.now() instance.current_node_id = 'end' instance.current_node_name = '结束' context.db.add(instance) await context.db.flush() # 查找即将被取消的待处理任务ID(用于清理钉钉待办) pending_task_stmt = select(WorkflowTask.id).where( WorkflowTask.instance_id == str(instance.id), WorkflowTask.status == 'pending', WorkflowTask.task_type != 'copy', ) pending_result = await context.db.execute(pending_task_stmt) pending_task_ids = [str(row[0]) for row in pending_result.all()] # 取消所有待处理任务(排除抄送任务,抄送任务保留供用户查阅) stmt = update(WorkflowTask).where( WorkflowTask.instance_id == str(instance.id), WorkflowTask.status == 'pending', WorkflowTask.task_type != 'copy', ).values(status='canceled') await context.db.execute(stmt) # 删除/完成被取消任务对应的钉钉待办 for task_id in pending_task_ids: try: from core.message.service import NotifyService if status in ('approved', 'rejected'): await NotifyService.complete_dingtalk_todo(context.db, "workflow_task", task_id) else: await NotifyService.delete_dingtalk_todo(context.db, "workflow_task", task_id) except Exception as e: logger.warning(f"清理钉钉待办失败 task={task_id}: {e}") # 发送流程完成通知给发起人 try: from online_dev.workflow.engine.handlers.notification_service import WorkflowNotificationService await WorkflowNotificationService.send_instance_complete_notification(context, status) except Exception as e: logger.error(f"发送流程完成通知失败: {e}") # 如果是子流程,通知父流程继续 if instance.parent_instance_id: logger.info(f"子流程结束,通知父流程: {instance.parent_instance_id}") await self._resume_parent_after_subflow(context.db, instance) async def _resume_parent_after_subflow(self, db: AsyncSession, sub_instance) -> None: """子流程完成后恢复父流程""" handler = self._get_handler('subflow') if handler: await handler.resume_parent(db, sub_instance) # 全局引擎实例 workflow_engine = WorkflowEngine()