Files

645 lines
24 KiB
Python
Raw Permalink 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 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()