feat: restore source parity and harden agent runtime
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
节点处理器模块
|
||||
每种节点类型对应一个处理器
|
||||
"""
|
||||
from online_dev.workflow.engine.handlers.approval_handler import ApprovalHandler
|
||||
from online_dev.workflow.engine.handlers.base_handler import BaseNodeHandler
|
||||
from online_dev.workflow.engine.handlers.condition_handler import ConditionHandler
|
||||
from online_dev.workflow.engine.handlers.copy_handler import CopyHandler
|
||||
from online_dev.workflow.engine.handlers.delay_handler import DelayNodeHandler
|
||||
from online_dev.workflow.engine.handlers.handle_handler import HandleHandler
|
||||
from online_dev.workflow.engine.handlers.notify_handler import NotifyHandler
|
||||
from online_dev.workflow.engine.handlers.parallel_handler import ParallelHandler
|
||||
from online_dev.workflow.engine.handlers.service_handler import ServiceHandler
|
||||
from online_dev.workflow.engine.handlers.data_update_handler import DataUpdateHandler
|
||||
from online_dev.workflow.engine.handlers.subflow_handler import SubflowHandler
|
||||
|
||||
__all__ = [
|
||||
'BaseNodeHandler',
|
||||
'ApprovalHandler',
|
||||
'HandleHandler',
|
||||
'CopyHandler',
|
||||
'ConditionHandler',
|
||||
'ParallelHandler',
|
||||
'DelayNodeHandler',
|
||||
'NotifyHandler',
|
||||
'ServiceHandler',
|
||||
'SubflowHandler',
|
||||
'DataUpdateHandler',
|
||||
]
|
||||
@@ -0,0 +1,375 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
审批节点处理器
|
||||
处理审批任务的创建和完成逻辑
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from online_dev.workflow.engine.handlers.base_handler import BaseNodeHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from online_dev.workflow.engine.base import ExecutionContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApprovalHandler(BaseNodeHandler):
|
||||
"""
|
||||
审批节点处理器
|
||||
|
||||
支持:
|
||||
- 或签(any):一人通过即可
|
||||
- 会签(parallel):所有人都要通过
|
||||
- 依次审批(sequential):按顺序审批
|
||||
"""
|
||||
|
||||
async def execute(self, context: 'ExecutionContext', node: Dict) -> None:
|
||||
"""
|
||||
执行审批节点:创建审批任务
|
||||
"""
|
||||
from online_dev.workflow.engine.assignee_resolver import assignee_resolver
|
||||
|
||||
node_id = self.get_node_id(node)
|
||||
node_name = self.get_node_name(node)
|
||||
node_config = self.get_node_config(node)
|
||||
|
||||
logger.info(f"创建审批任务 - 节点: {node_id}, 名称: {node_name}")
|
||||
|
||||
# 解析审批人
|
||||
assignee_ids = await assignee_resolver.resolve(
|
||||
context.db,
|
||||
node_config,
|
||||
context.instance,
|
||||
context.form_data,
|
||||
)
|
||||
|
||||
logger.info(f"解析到的审批人: {assignee_ids}")
|
||||
|
||||
if not assignee_ids:
|
||||
# 空审批人处理
|
||||
empty_action = node_config.get('emptyAssignee', 'error')
|
||||
if empty_action == 'skip':
|
||||
logger.warning(f"节点 {node_id} 没有审批人,自动跳过")
|
||||
await self.advance_to_next(context, node)
|
||||
return
|
||||
elif empty_action == 'admin':
|
||||
logger.warning(f"节点 {node_id} 没有审批人,转交管理员(未实现)")
|
||||
await self.advance_to_next(context, node)
|
||||
return
|
||||
else:
|
||||
logger.warning(f"节点 {node_id} 没有审批人,自动跳过")
|
||||
await self.advance_to_next(context, node)
|
||||
return
|
||||
|
||||
# 更新实例当前节点
|
||||
await self.update_instance_node(context, node)
|
||||
|
||||
# 创建任务
|
||||
multi_approval = node_config.get('multiApproval', 'any')
|
||||
|
||||
if multi_approval == 'sequential':
|
||||
# 依次审批:只创建第一个人的任务
|
||||
await self.create_task(context, node, assignee_ids[0], 'approval')
|
||||
else:
|
||||
# 或签/会签:创建所有人的任务
|
||||
for assignee_id in assignee_ids:
|
||||
await self.create_task(context, node, assignee_id, 'approval')
|
||||
|
||||
async def handle_approval(self, context: 'ExecutionContext', task: Any) -> None:
|
||||
"""
|
||||
处理审批通过后的流程推进
|
||||
|
||||
Args:
|
||||
context: 执行上下文
|
||||
task: 已完成的任务
|
||||
"""
|
||||
from online_dev.workflow.model import WorkflowTask
|
||||
from online_dev.workflow.engine.utils import FlowUtils
|
||||
|
||||
# 检查是否是加签任务
|
||||
if task.sign_type:
|
||||
await self._handle_sign_task_completion(context, task)
|
||||
return
|
||||
|
||||
node = FlowUtils.find_node_by_id(context.flow_definition, task.node_id)
|
||||
if not node:
|
||||
logger.error(f"找不到节点: {task.node_id}")
|
||||
return
|
||||
|
||||
node_config = self.get_node_config(node)
|
||||
multi_approval = node_config.get('multiApproval', 'any')
|
||||
|
||||
# 检查多人审批逻辑
|
||||
if multi_approval == 'parallel':
|
||||
# 会签:检查是否所有人都已审批
|
||||
stmt = select(WorkflowTask).where(
|
||||
WorkflowTask.instance_id == str(context.instance.id),
|
||||
WorkflowTask.node_id == task.node_id,
|
||||
WorkflowTask.status == 'pending',
|
||||
WorkflowTask.sign_type == '',
|
||||
)
|
||||
result = await context.db.execute(stmt)
|
||||
pending_tasks = result.scalars().all()
|
||||
|
||||
if len(pending_tasks) > 0:
|
||||
logger.info(f"会签模式,还有 {len(pending_tasks)} 人未审批")
|
||||
return
|
||||
|
||||
elif multi_approval == 'sequential':
|
||||
# 依次审批:检查是否还有下一个人
|
||||
next_assignee = await self._get_next_sequential_assignee(context, task)
|
||||
if next_assignee:
|
||||
await self.create_task(context, node, next_assignee, 'approval')
|
||||
return
|
||||
|
||||
# 或签模式:取消该节点其他 pending 的普通任务
|
||||
if multi_approval == 'any':
|
||||
stmt = select(WorkflowTask).where(
|
||||
WorkflowTask.instance_id == str(context.instance.id),
|
||||
WorkflowTask.node_id == task.node_id,
|
||||
WorkflowTask.status == 'pending',
|
||||
WorkflowTask.id != task.id,
|
||||
)
|
||||
result = await context.db.execute(stmt)
|
||||
other_pending = result.scalars().all()
|
||||
if other_pending:
|
||||
logger.info(f"或签模式,取消其他 {len(other_pending)} 个待审批任务")
|
||||
canceled_task_ids = []
|
||||
for t in other_pending:
|
||||
canceled_task_ids.append(str(t.id))
|
||||
t.status = 'canceled'
|
||||
t.comment = '__or_sign_canceled__'
|
||||
context.db.add(t)
|
||||
await context.db.flush()
|
||||
|
||||
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}")
|
||||
|
||||
# 检查是否还有未完成的加签任务
|
||||
# 1. waiting 状态的任务(前加签产生的原任务)
|
||||
stmt = select(WorkflowTask).where(
|
||||
WorkflowTask.instance_id == str(context.instance.id),
|
||||
WorkflowTask.node_id == task.node_id,
|
||||
WorkflowTask.status == 'waiting',
|
||||
)
|
||||
result = await context.db.execute(stmt)
|
||||
waiting_tasks = result.scalars().all()
|
||||
|
||||
# 2. 后加签任务
|
||||
stmt = select(WorkflowTask).where(
|
||||
WorkflowTask.instance_id == str(context.instance.id),
|
||||
WorkflowTask.node_id == task.node_id,
|
||||
WorkflowTask.sign_type == 'after',
|
||||
WorkflowTask.status == 'pending',
|
||||
)
|
||||
result = await context.db.execute(stmt)
|
||||
after_sign_tasks = result.scalars().all()
|
||||
|
||||
if len(waiting_tasks) > 0:
|
||||
logger.info(f"还有 {len(waiting_tasks)} 个任务在等待前加签完成")
|
||||
if multi_approval == 'any':
|
||||
canceled_ids = []
|
||||
for wt in waiting_tasks:
|
||||
canceled_ids.append(str(wt.id))
|
||||
wt.status = 'canceled'
|
||||
wt.comment = '__or_sign_canceled__'
|
||||
context.db.add(wt)
|
||||
await context.db.flush()
|
||||
for tid in canceled_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"或签取消waiting任务-清理钉钉待办失败 task={tid}: {e}")
|
||||
|
||||
if len(after_sign_tasks) > 0:
|
||||
logger.info(f"还有 {len(after_sign_tasks)} 个后加签任务未完成")
|
||||
if multi_approval == 'any':
|
||||
canceled_ids = []
|
||||
for ast in after_sign_tasks:
|
||||
canceled_ids.append(str(ast.id))
|
||||
ast.status = 'canceled'
|
||||
ast.comment = '__or_sign_canceled__'
|
||||
context.db.add(ast)
|
||||
await context.db.flush()
|
||||
for tid in canceled_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}")
|
||||
|
||||
# 推进到下一节点
|
||||
await self.advance_to_next(context, node)
|
||||
|
||||
async def _handle_sign_task_completion(self, context: 'ExecutionContext', task: Any) -> None:
|
||||
"""
|
||||
处理加签任务完成后的逻辑
|
||||
"""
|
||||
from online_dev.workflow.model import WorkflowTask
|
||||
from online_dev.workflow.engine.utils import FlowUtils
|
||||
|
||||
sign_type = task.sign_type
|
||||
parent_task_id = task.parent_task_id
|
||||
|
||||
logger.info(f"加签任务完成 - 类型: {sign_type}, 父任务ID: {parent_task_id}")
|
||||
|
||||
if sign_type == 'before':
|
||||
# 前加签完成:检查是否所有前加签任务都完成了
|
||||
stmt = select(WorkflowTask).where(
|
||||
WorkflowTask.instance_id == str(context.instance.id),
|
||||
WorkflowTask.node_id == task.node_id,
|
||||
WorkflowTask.sign_type == 'before',
|
||||
WorkflowTask.parent_task_id == parent_task_id,
|
||||
WorkflowTask.status == 'pending',
|
||||
)
|
||||
result = await context.db.execute(stmt)
|
||||
pending_before_signs = result.scalars().all()
|
||||
|
||||
if len(pending_before_signs) > 0:
|
||||
logger.info(f"还有 {len(pending_before_signs)} 个前加签任务未完成")
|
||||
return
|
||||
|
||||
# 所有前加签任务完成,恢复原任务
|
||||
if parent_task_id:
|
||||
stmt = select(WorkflowTask).where(WorkflowTask.id == parent_task_id)
|
||||
result = await context.db.execute(stmt)
|
||||
parent_task = result.scalar_one_or_none()
|
||||
if parent_task and parent_task.status == 'waiting':
|
||||
parent_task.status = 'pending'
|
||||
context.db.add(parent_task)
|
||||
await context.db.flush()
|
||||
logger.info(f"前加签完成,恢复原任务: {parent_task_id}")
|
||||
|
||||
elif sign_type == 'after':
|
||||
# 后加签完成:检查是否所有后加签任务都完成了
|
||||
stmt = select(WorkflowTask).where(
|
||||
WorkflowTask.instance_id == str(context.instance.id),
|
||||
WorkflowTask.node_id == task.node_id,
|
||||
WorkflowTask.sign_type == 'after',
|
||||
WorkflowTask.parent_task_id == parent_task_id,
|
||||
WorkflowTask.status == 'pending',
|
||||
)
|
||||
result = await context.db.execute(stmt)
|
||||
pending_after_signs = result.scalars().all()
|
||||
|
||||
if len(pending_after_signs) > 0:
|
||||
logger.info(f"还有 {len(pending_after_signs)} 个后加签任务未完成")
|
||||
return
|
||||
|
||||
# 所有后加签任务完成,推进流程
|
||||
node = FlowUtils.find_node_by_id(context.flow_definition, task.node_id)
|
||||
if node:
|
||||
await self.advance_to_next(context, node)
|
||||
|
||||
elif sign_type == 'parallel':
|
||||
# 并行加签完成
|
||||
node = FlowUtils.find_node_by_id(context.flow_definition, task.node_id)
|
||||
if not node:
|
||||
return
|
||||
|
||||
node_config = self.get_node_config(node)
|
||||
multi_approval = node_config.get('multiApproval', 'any')
|
||||
|
||||
stmt = select(WorkflowTask).where(
|
||||
WorkflowTask.instance_id == str(context.instance.id),
|
||||
WorkflowTask.node_id == task.node_id,
|
||||
WorkflowTask.status == 'pending',
|
||||
)
|
||||
result = await context.db.execute(stmt)
|
||||
pending_tasks = result.scalars().all()
|
||||
|
||||
if multi_approval == 'any':
|
||||
await self.advance_to_next(context, node)
|
||||
elif multi_approval == 'parallel' and len(pending_tasks) == 0:
|
||||
await self.advance_to_next(context, node)
|
||||
|
||||
elif sign_type == 'delegate':
|
||||
# 委托任务完成
|
||||
if parent_task_id:
|
||||
stmt = select(WorkflowTask).where(WorkflowTask.id == parent_task_id)
|
||||
result = await context.db.execute(stmt)
|
||||
parent_task = result.scalar_one_or_none()
|
||||
if parent_task and parent_task.status == 'delegated':
|
||||
parent_task.status = 'pending'
|
||||
parent_task.comment = f'委托人已审批通过,请确认'
|
||||
context.db.add(parent_task)
|
||||
await context.db.flush()
|
||||
logger.info(f"委托任务完成,恢复原任务: {parent_task_id}")
|
||||
|
||||
elif sign_type == 'transfer':
|
||||
# 转交任务完成
|
||||
node = FlowUtils.find_node_by_id(context.flow_definition, task.node_id)
|
||||
if node:
|
||||
node_config = self.get_node_config(node)
|
||||
multi_approval = node_config.get('multiApproval', 'any')
|
||||
|
||||
if multi_approval == 'any':
|
||||
await self.advance_to_next(context, node)
|
||||
elif multi_approval == 'parallel':
|
||||
stmt = select(WorkflowTask).where(
|
||||
WorkflowTask.instance_id == str(context.instance.id),
|
||||
WorkflowTask.node_id == task.node_id,
|
||||
WorkflowTask.status == 'pending',
|
||||
)
|
||||
result = await context.db.execute(stmt)
|
||||
pending_tasks = result.scalars().all()
|
||||
if len(pending_tasks) == 0:
|
||||
await self.advance_to_next(context, node)
|
||||
else:
|
||||
await self.advance_to_next(context, node)
|
||||
|
||||
async def _get_next_sequential_assignee(
|
||||
self,
|
||||
context: 'ExecutionContext',
|
||||
current_task: Any,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
获取依次审批的下一个审批人
|
||||
"""
|
||||
from online_dev.workflow.model import WorkflowTask
|
||||
from online_dev.workflow.engine.assignee_resolver import assignee_resolver
|
||||
from online_dev.workflow.engine.utils import FlowUtils
|
||||
|
||||
node = FlowUtils.find_node_by_id(context.flow_definition, current_task.node_id)
|
||||
if not node:
|
||||
return None
|
||||
|
||||
node_config = self.get_node_config(node)
|
||||
|
||||
# 获取所有审批人列表
|
||||
all_assignees = await assignee_resolver.resolve(
|
||||
context.db,
|
||||
node_config,
|
||||
context.instance,
|
||||
context.form_data,
|
||||
)
|
||||
|
||||
if not all_assignees:
|
||||
return None
|
||||
|
||||
# 获取已处理的任务
|
||||
stmt = select(WorkflowTask.assignee_id).where(
|
||||
WorkflowTask.instance_id == str(context.instance.id),
|
||||
WorkflowTask.node_id == current_task.node_id,
|
||||
WorkflowTask.status.in_(['approved', 'transferred']),
|
||||
).order_by(WorkflowTask.sys_create_datetime)
|
||||
result = await context.db.execute(stmt)
|
||||
handled_ids = [str(uid) for uid in result.scalars().all()]
|
||||
|
||||
handled_set = set(handled_ids)
|
||||
|
||||
# 找到下一个未处理的审批人
|
||||
for assignee_id in all_assignees:
|
||||
if assignee_id not in handled_set:
|
||||
return assignee_id
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
节点处理器基类
|
||||
定义所有节点处理器的通用接口和方法
|
||||
"""
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any, TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from online_dev.workflow.engine.base import ExecutionContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 与前端 NODE_TYPE_CONFIGS 默认名称保持一致
|
||||
DEFAULT_NODE_NAMES = {
|
||||
'start': '发起人',
|
||||
'approval': '审批人',
|
||||
'handle': '办理人',
|
||||
'copy': '抄送人',
|
||||
'delay': '延时等待',
|
||||
'notify': '发送通知',
|
||||
'service': '服务调用',
|
||||
'subflow': '子流程',
|
||||
'data_update': '字段更新',
|
||||
'condition': '条件分支',
|
||||
'parallel': '并行分支',
|
||||
'route': '路由',
|
||||
'end': '结束',
|
||||
}
|
||||
|
||||
|
||||
class BaseNodeHandler(ABC):
|
||||
"""
|
||||
节点处理器基类
|
||||
|
||||
所有节点处理器都应继承此类并实现 execute 方法
|
||||
"""
|
||||
|
||||
def __init__(self, engine: Any):
|
||||
"""
|
||||
初始化处理器
|
||||
|
||||
Args:
|
||||
engine: 工作流引擎实例,用于调用引擎方法
|
||||
"""
|
||||
self.engine = engine
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self, context: 'ExecutionContext', node: Dict) -> None:
|
||||
"""
|
||||
执行节点逻辑
|
||||
|
||||
Args:
|
||||
context: 执行上下文
|
||||
node: 节点配置
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_node_config(self, node: Dict) -> Dict:
|
||||
"""获取节点配置"""
|
||||
return node.get('config', {})
|
||||
|
||||
def get_node_id(self, node: Dict) -> str:
|
||||
"""获取节点ID"""
|
||||
return node.get('id', '')
|
||||
|
||||
def get_node_name(self, node: Dict) -> str:
|
||||
"""获取节点名称(空名时回退为类型默认名)"""
|
||||
name = (node.get('name') or '').strip()
|
||||
if name:
|
||||
return name
|
||||
node_type = node.get('type', '')
|
||||
return DEFAULT_NODE_NAMES.get(node_type, node_type or '节点')
|
||||
|
||||
def get_node_type(self, node: Dict) -> str:
|
||||
"""获取节点类型"""
|
||||
return node.get('type', '')
|
||||
|
||||
async def update_instance_node(self, context: 'ExecutionContext', node: Dict) -> None:
|
||||
"""更新实例当前节点"""
|
||||
context.instance.current_node_id = self.get_node_id(node)
|
||||
context.instance.current_node_name = self.get_node_name(node)
|
||||
context.db.add(context.instance)
|
||||
await context.db.flush()
|
||||
|
||||
async def create_log(
|
||||
self,
|
||||
context: 'ExecutionContext',
|
||||
node: Dict,
|
||||
action: str,
|
||||
comment: str = '',
|
||||
extra_data: Dict = None,
|
||||
operator_id: str = None,
|
||||
) -> None:
|
||||
"""
|
||||
创建流程日志
|
||||
|
||||
Args:
|
||||
context: 执行上下文
|
||||
node: 节点配置
|
||||
action: 操作类型
|
||||
comment: 备注
|
||||
extra_data: 额外数据
|
||||
operator_id: 操作人ID,None时使用context.current_user_id,传空字符串表示系统自动执行
|
||||
"""
|
||||
from online_dev.workflow.model import WorkflowLog
|
||||
|
||||
log = WorkflowLog(
|
||||
instance_id=str(context.instance.id),
|
||||
node_id=self.get_node_id(node),
|
||||
node_name=self.get_node_name(node),
|
||||
action=action,
|
||||
operator_id=operator_id if operator_id is not None else (context.current_user_id or ''),
|
||||
comment=comment,
|
||||
extra_data=extra_data or {},
|
||||
)
|
||||
context.db.add(log)
|
||||
await context.db.flush()
|
||||
|
||||
async def create_task(
|
||||
self,
|
||||
context: 'ExecutionContext',
|
||||
node: Dict,
|
||||
assignee_id: str,
|
||||
task_type: str,
|
||||
) -> Any:
|
||||
"""
|
||||
创建任务
|
||||
|
||||
Args:
|
||||
context: 执行上下文
|
||||
node: 节点配置
|
||||
assignee_id: 处理人ID
|
||||
task_type: 任务类型
|
||||
|
||||
Returns:
|
||||
创建的任务对象
|
||||
"""
|
||||
from online_dev.workflow.model import WorkflowTask
|
||||
from core.user.model import User
|
||||
|
||||
# 验证用户存在
|
||||
stmt = select(User).where(User.id == assignee_id)
|
||||
result = await context.db.execute(stmt)
|
||||
assignee = result.scalar_one_or_none()
|
||||
|
||||
if not assignee:
|
||||
logger.warning(f"用户不存在: {assignee_id}")
|
||||
return None
|
||||
|
||||
# 计算超时时间
|
||||
node_config = self.get_node_config(node)
|
||||
timeout_config = node_config.get('timeout', {})
|
||||
timeout_at = None
|
||||
timeout_action = ''
|
||||
|
||||
if timeout_config.get('enabled'):
|
||||
duration = timeout_config.get('duration', 24)
|
||||
unit = timeout_config.get('unit', 'hour')
|
||||
timeout_action = timeout_config.get('action', 'notify')
|
||||
timeout_at = self._calculate_timeout(duration, unit)
|
||||
|
||||
task = WorkflowTask(
|
||||
instance_id=str(context.instance.id),
|
||||
node_id=self.get_node_id(node),
|
||||
node_name=self.get_node_name(node),
|
||||
task_type=task_type,
|
||||
status='pending',
|
||||
assignee_id=assignee_id,
|
||||
timeout_at=timeout_at,
|
||||
timeout_action=timeout_action,
|
||||
)
|
||||
context.db.add(task)
|
||||
await context.db.flush()
|
||||
|
||||
# 发送通知
|
||||
await self._send_task_notification(context, task, assignee_id, task_type, node)
|
||||
|
||||
return task
|
||||
|
||||
def _calculate_timeout(self, duration: int, unit: str) -> 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)
|
||||
return now + timedelta(hours=duration)
|
||||
|
||||
async def _send_task_notification(
|
||||
self,
|
||||
context: 'ExecutionContext',
|
||||
task: Any,
|
||||
assignee_id: str,
|
||||
task_type: str,
|
||||
node: Dict,
|
||||
) -> None:
|
||||
"""发送任务通知"""
|
||||
from online_dev.workflow.engine.handlers.notification_service import WorkflowNotificationService
|
||||
|
||||
try:
|
||||
await WorkflowNotificationService.send_task_notification(
|
||||
context=context,
|
||||
task=task,
|
||||
assignee_id=assignee_id,
|
||||
task_type=task_type,
|
||||
node=node,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"发送任务通知失败: {e}")
|
||||
|
||||
async def advance_to_next(self, context: 'ExecutionContext', current_node: Dict) -> None:
|
||||
"""推进到下一节点(委托给引擎)"""
|
||||
await self.engine._advance_to_next(context, current_node)
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
条件分支节点处理器
|
||||
处理条件判断和分支选择
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
|
||||
from online_dev.workflow.engine.handlers.base_handler import BaseNodeHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from online_dev.workflow.engine.base import ExecutionContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConditionHandler(BaseNodeHandler):
|
||||
"""
|
||||
条件分支节点处理器
|
||||
|
||||
根据条件表达式选择执行的分支
|
||||
- 条件组之间是 OR 关系
|
||||
- 组内条件是 AND 关系
|
||||
- 支持默认分支
|
||||
"""
|
||||
|
||||
async def execute(self, context: 'ExecutionContext', node: Dict) -> None:
|
||||
"""
|
||||
执行条件节点:评估条件并选择分支
|
||||
"""
|
||||
from online_dev.workflow.engine.condition_evaluator import condition_evaluator
|
||||
|
||||
branches = node.get('branches', [])
|
||||
node_id = self.get_node_id(node)
|
||||
|
||||
logger.info(f"处理条件分支 - 节点: {node_id}, 分支数: {len(branches)}")
|
||||
|
||||
# 按优先级评估每个分支
|
||||
for branch in branches:
|
||||
config = branch.get('config', {})
|
||||
|
||||
# 默认分支
|
||||
if config.get('isDefault'):
|
||||
logger.info(f"进入默认分支: {branch.get('name')}")
|
||||
await self._enter_branch(context, node, branch)
|
||||
return
|
||||
|
||||
# 评估条件
|
||||
groups = config.get('groups', [])
|
||||
if condition_evaluator.evaluate_groups(groups, context.form_data):
|
||||
logger.info(f"条件满足,进入分支: {branch.get('name')}")
|
||||
await self._enter_branch(context, node, branch)
|
||||
return
|
||||
|
||||
# 没有分支满足条件
|
||||
logger.warning(f"条件节点 {node_id} 没有满足的分支,结束流程")
|
||||
await self.engine._end_instance(context, 'rejected')
|
||||
|
||||
async def _enter_branch(
|
||||
self,
|
||||
context: 'ExecutionContext',
|
||||
condition_node: Dict,
|
||||
branch: Dict,
|
||||
) -> None:
|
||||
"""
|
||||
进入分支
|
||||
|
||||
Args:
|
||||
context: 执行上下文
|
||||
condition_node: 条件节点
|
||||
branch: 选中的分支
|
||||
"""
|
||||
# 记录条件分支选择日志(系统自动执行,不关联用户)
|
||||
await self.create_log(
|
||||
context, condition_node, 'condition',
|
||||
comment=f'进入分支: {branch.get("name", "")}',
|
||||
extra_data={
|
||||
'branch_name': branch.get('name', ''),
|
||||
'branch_id': branch.get('id', ''),
|
||||
},
|
||||
operator_id='',
|
||||
)
|
||||
|
||||
branch_children = branch.get('children')
|
||||
|
||||
if branch_children:
|
||||
# 分支有子节点,推进到分支内的第一个节点
|
||||
await self.advance_to_next(context, {'children': branch_children})
|
||||
else:
|
||||
# 分支无子节点,继续推进条件节点的下一节点
|
||||
await self.advance_to_next(context, condition_node)
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
抄送节点处理器
|
||||
处理抄送任务的创建
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
|
||||
from online_dev.workflow.engine.handlers.base_handler import BaseNodeHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from online_dev.workflow.engine.base import ExecutionContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CopyHandler(BaseNodeHandler):
|
||||
"""
|
||||
抄送节点处理器
|
||||
|
||||
抄送节点创建抄送任务后自动推进到下一节点
|
||||
"""
|
||||
|
||||
async def execute(self, context: 'ExecutionContext', node: Dict) -> None:
|
||||
"""
|
||||
执行抄送节点:创建抄送任务并继续推进
|
||||
"""
|
||||
from online_dev.workflow.engine.assignee_resolver import assignee_resolver
|
||||
|
||||
node_config = self.get_node_config(node)
|
||||
|
||||
# 解析抄送人
|
||||
assignee_ids = await assignee_resolver.resolve(
|
||||
context.db,
|
||||
node_config,
|
||||
context.instance,
|
||||
context.form_data,
|
||||
)
|
||||
|
||||
logger.info(f"创建抄送任务 - 节点: {self.get_node_id(node)}, 抄送人数: {len(assignee_ids)}")
|
||||
|
||||
# 创建抄送任务
|
||||
for assignee_id in assignee_ids:
|
||||
await self.create_task(context, node, assignee_id, 'copy')
|
||||
|
||||
# 记录抄送日志(系统自动执行,不关联用户)
|
||||
await self.create_log(
|
||||
context, node, 'copy',
|
||||
comment=f'抄送给 {len(assignee_ids)} 人',
|
||||
extra_data={
|
||||
'assignee_ids': assignee_ids,
|
||||
},
|
||||
operator_id='',
|
||||
)
|
||||
|
||||
# 抄送后继续推进(不等待)
|
||||
await self.advance_to_next(context, node)
|
||||
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
字段更新节点处理器
|
||||
在流程执行过程中自动修改表单字段值
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
from online_dev.workflow.engine.handlers.base_handler import BaseNodeHandler
|
||||
from app.timezone import APP_TIMEZONE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from online_dev.workflow.engine.base import ExecutionContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DataUpdateHandler(BaseNodeHandler):
|
||||
"""
|
||||
字段更新节点处理器
|
||||
|
||||
支持的值类型:
|
||||
- constant: 常量值(直接赋值)
|
||||
- field: 引用其他表单字段的值
|
||||
- formula: 简单表达式(支持变量替换和四则运算)
|
||||
- system: 系统变量(当前时间、当前用户、流程编号等)
|
||||
"""
|
||||
|
||||
# 支持的系统变量
|
||||
SYSTEM_VARIABLES = {
|
||||
'current_time': '当前时间',
|
||||
'current_date': '当前日期',
|
||||
'current_user': '当前操作人',
|
||||
'initiator': '流程发起人',
|
||||
'instance_no': '流程编号',
|
||||
'instance_title': '流程标题',
|
||||
}
|
||||
|
||||
async def execute(self, context: 'ExecutionContext', node: Dict) -> None:
|
||||
"""
|
||||
执行字段更新节点:根据规则修改表单字段值并回写数据库。
|
||||
支持更新当前表单或跨表单(同应用下的其他表单)。
|
||||
"""
|
||||
node_id = self.get_node_id(node)
|
||||
node_name = self.get_node_name(node)
|
||||
node_config = self.get_node_config(node)
|
||||
|
||||
rules = node_config.get('rules', [])
|
||||
target_form_code = node_config.get('targetFormCode', '') or ''
|
||||
is_cross_form = bool(target_form_code)
|
||||
|
||||
logger.info(
|
||||
f"字段更新节点 - 节点: {node_id}, 名称: {node_name}, "
|
||||
f"规则数: {len(rules)}, 跨表单: {is_cross_form}, 目标: {target_form_code or '当前表单'}"
|
||||
)
|
||||
|
||||
await self.update_instance_node(context, node)
|
||||
|
||||
if is_cross_form:
|
||||
updated_count = await self._update_other_form(context, node_config)
|
||||
log_comment = f'跨表单更新({target_form_code}): 更新了 {updated_count} 条记录'
|
||||
else:
|
||||
updated_count = await self._update_current_form(context, rules)
|
||||
log_comment = f'字段更新: 更新了 {updated_count} 个字段'
|
||||
|
||||
await self.create_log(
|
||||
context, node, 'data_update',
|
||||
comment=log_comment,
|
||||
extra_data={'updated_count': updated_count, 'target_form_code': target_form_code or context.instance.form_code},
|
||||
operator_id='',
|
||||
)
|
||||
|
||||
await self.advance_to_next(context, node)
|
||||
|
||||
async def _update_current_form(self, context: 'ExecutionContext', rules: List[Dict]) -> int:
|
||||
"""更新当前表单字段(原有逻辑)"""
|
||||
updated_fields: Dict[str, Any] = {}
|
||||
for rule in rules:
|
||||
field = rule.get('field', '')
|
||||
if not field:
|
||||
continue
|
||||
try:
|
||||
value = self._resolve_value(rule, context)
|
||||
context.form_data[field] = value
|
||||
updated_fields[field] = value
|
||||
logger.info(f"字段更新: {field} = {value}")
|
||||
except Exception as e:
|
||||
logger.error(f"字段更新失败 - 字段: {field}, 错误: {e}")
|
||||
|
||||
if updated_fields:
|
||||
try:
|
||||
from online_dev.workflow.engine.utils import FormDataUtils
|
||||
await FormDataUtils.update_form_data(
|
||||
context.db,
|
||||
context.instance.form_code,
|
||||
context.instance.form_data_id,
|
||||
context.form_data,
|
||||
)
|
||||
logger.info(f"表单数据已回写数据库,更新了 {len(updated_fields)} 个字段")
|
||||
except Exception as e:
|
||||
logger.error(f"回写表单数据失败: {e}")
|
||||
|
||||
return len(updated_fields)
|
||||
|
||||
async def _update_other_form(self, context: 'ExecutionContext', node_config: Dict) -> int:
|
||||
"""
|
||||
跨表单更新:根据匹配条件查找目标表单的记录,并批量更新字段。
|
||||
"""
|
||||
from online_dev.form_data_manager.service import FormDataService
|
||||
|
||||
target_form_code: str = node_config['targetFormCode']
|
||||
match_condition: Optional[Dict] = node_config.get('matchCondition')
|
||||
update_scope: str = node_config.get('updateScope', 'first')
|
||||
rules: List[Dict] = node_config.get('rules', [])
|
||||
|
||||
if not match_condition or not match_condition.get('sourceField') or not match_condition.get('targetField'):
|
||||
logger.warning("跨表单更新缺少匹配条件,跳过")
|
||||
return 0
|
||||
|
||||
source_field = match_condition['sourceField']
|
||||
target_field = match_condition['targetField']
|
||||
match_value = context.form_data.get(source_field)
|
||||
|
||||
if match_value is None:
|
||||
logger.warning(f"当前表单字段 {source_field} 值为空,跳过跨表单更新")
|
||||
return 0
|
||||
|
||||
# 构建筛选条件查询目标表单数据
|
||||
filters = {target_field: match_value}
|
||||
service = await FormDataService.create_service(context.db, target_form_code)
|
||||
result = await service.list(context.db, page=1, page_size=1000, filters=filters)
|
||||
records = result.get('items', [])
|
||||
|
||||
if not records:
|
||||
logger.info(f"跨表单更新: 未找到匹配的记录 ({target_field}={match_value})")
|
||||
return 0
|
||||
|
||||
if update_scope == 'first':
|
||||
records = records[:1]
|
||||
|
||||
logger.info(f"跨表单更新: 匹配到 {len(records)} 条记录, 更新范围: {update_scope}")
|
||||
|
||||
# 计算更新值
|
||||
update_values: Dict[str, Any] = {}
|
||||
for rule in rules:
|
||||
field = rule.get('field', '')
|
||||
if not field:
|
||||
continue
|
||||
try:
|
||||
value = self._resolve_value(rule, context)
|
||||
update_values[field] = value
|
||||
except Exception as e:
|
||||
logger.error(f"跨表单字段值解析失败 - 字段: {field}, 错误: {e}")
|
||||
|
||||
if not update_values:
|
||||
return 0
|
||||
|
||||
updated_count = 0
|
||||
for record in records:
|
||||
record_id = record.get('id')
|
||||
if not record_id:
|
||||
continue
|
||||
try:
|
||||
merged = {**record, **update_values}
|
||||
data = {"main": merged, "sub_tables": {}}
|
||||
await service.update(context.db, record_id, data)
|
||||
updated_count += 1
|
||||
logger.info(f"跨表单更新记录 {record_id}: {update_values}")
|
||||
except Exception as e:
|
||||
logger.error(f"跨表单更新记录 {record_id} 失败: {e}")
|
||||
|
||||
return updated_count
|
||||
|
||||
def _resolve_value(self, rule: Dict, context: 'ExecutionContext') -> Any:
|
||||
"""
|
||||
根据规则解析值
|
||||
|
||||
Args:
|
||||
rule: 更新规则配置
|
||||
context: 执行上下文
|
||||
|
||||
Returns:
|
||||
解析后的值
|
||||
"""
|
||||
value_type = rule.get('valueType', 'constant')
|
||||
value = rule.get('value')
|
||||
|
||||
if value_type == 'constant':
|
||||
return value
|
||||
|
||||
elif value_type == 'field':
|
||||
# 引用其他字段的值
|
||||
source_field = str(value) if value else ''
|
||||
return context.form_data.get(source_field)
|
||||
|
||||
elif value_type == 'formula':
|
||||
# 表达式计算
|
||||
return self._evaluate_formula(str(value) if value else '', context.form_data)
|
||||
|
||||
elif value_type == 'system':
|
||||
# 系统变量
|
||||
return self._get_system_variable(str(value) if value else '', context)
|
||||
|
||||
else:
|
||||
logger.warning(f"未知的值类型: {value_type}")
|
||||
return value
|
||||
|
||||
def _get_system_variable(self, var_name: str, context: 'ExecutionContext') -> Any:
|
||||
"""
|
||||
获取系统变量值
|
||||
|
||||
Args:
|
||||
var_name: 变量名
|
||||
context: 执行上下文
|
||||
|
||||
Returns:
|
||||
系统变量值
|
||||
"""
|
||||
now = datetime.now(APP_TIMEZONE)
|
||||
|
||||
if var_name == 'current_time':
|
||||
return now.strftime('%Y-%m-%d %H:%M:%S')
|
||||
elif var_name == 'current_date':
|
||||
return now.strftime('%Y-%m-%d')
|
||||
elif var_name == 'current_user':
|
||||
return context.current_user_id or ''
|
||||
elif var_name == 'initiator':
|
||||
return context.get_initiator_id()
|
||||
elif var_name == 'instance_no':
|
||||
return context.instance.instance_no if context.instance else ''
|
||||
elif var_name == 'instance_title':
|
||||
return context.instance.title if context.instance else ''
|
||||
else:
|
||||
logger.warning(f"未知的系统变量: {var_name}")
|
||||
return ''
|
||||
|
||||
def _evaluate_formula(self, formula: str, form_data: Dict) -> Any:
|
||||
"""
|
||||
计算表达式
|
||||
|
||||
支持:
|
||||
- 变量引用: {{field_name}}
|
||||
- 四则运算: +, -, *, /
|
||||
- 字符串拼接: 含非数字变量时自动拼接
|
||||
|
||||
Args:
|
||||
formula: 表达式字符串
|
||||
form_data: 表单数据
|
||||
|
||||
Returns:
|
||||
计算结果
|
||||
"""
|
||||
if not formula:
|
||||
return ''
|
||||
|
||||
# 替换变量
|
||||
def replace_var(match):
|
||||
var_name = match.group(1).strip()
|
||||
val = form_data.get(var_name, '')
|
||||
return str(val) if val is not None else ''
|
||||
|
||||
replaced = re.sub(r'\{\{(\w+)\}\}', replace_var, formula)
|
||||
|
||||
# 尝试数学运算
|
||||
try:
|
||||
# 安全地评估简单数学表达式
|
||||
if re.match(r'^[\d\s\+\-\*\/\.\(\)]+$', replaced.strip()):
|
||||
result = eval(replaced.strip(), {"__builtins__": {}}, {})
|
||||
# 如果结果是整数,返回整数类型
|
||||
if isinstance(result, float) and result == int(result):
|
||||
return int(result)
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 非数学表达式,返回替换后的字符串
|
||||
return replaced
|
||||
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
延时节点处理器
|
||||
处理流程延时等待
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
|
||||
from online_dev.workflow.engine.handlers.base_handler import BaseNodeHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from online_dev.workflow.engine.base import ExecutionContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DelayNodeHandler(BaseNodeHandler):
|
||||
"""
|
||||
延时节点处理器
|
||||
|
||||
支持延时单位:
|
||||
- minute: 分钟
|
||||
- hour: 小时
|
||||
- day: 天
|
||||
- workday: 工作日(跳过周六日)
|
||||
"""
|
||||
|
||||
async def execute(self, context: 'ExecutionContext', node: Dict) -> None:
|
||||
"""
|
||||
执行延时节点:计算到期时间,注册定时任务
|
||||
"""
|
||||
node_id = self.get_node_id(node)
|
||||
node_name = self.get_node_name(node)
|
||||
node_config = self.get_node_config(node)
|
||||
|
||||
duration = node_config.get('duration', 1)
|
||||
unit = node_config.get('unit', 'hour')
|
||||
|
||||
unit_names = {
|
||||
'minute': '分钟',
|
||||
'hour': '小时',
|
||||
'day': '天',
|
||||
'workday': '工作日',
|
||||
}
|
||||
unit_name = unit_names.get(unit, unit)
|
||||
|
||||
logger.info(f"延时节点 - 等待 {duration} {unit_name}")
|
||||
|
||||
# 更新实例当前节点
|
||||
await self.update_instance_node(context, node)
|
||||
|
||||
# 计算到期时间
|
||||
delay_until = self._calculate_delay_until(duration, unit)
|
||||
|
||||
# 更新实例延时状态
|
||||
context.instance.delay_node_id = node_id
|
||||
context.instance.delay_until = delay_until
|
||||
context.db.add(context.instance)
|
||||
await context.db.flush()
|
||||
|
||||
# 记录延时开始日志
|
||||
await self.create_log(
|
||||
context, node, 'delay_start',
|
||||
comment=f'开始延时等待 {duration} {unit_name},预计 {delay_until.strftime("%Y-%m-%d %H:%M:%S")} 恢复',
|
||||
extra_data={
|
||||
'duration': duration,
|
||||
'unit': unit,
|
||||
'delay_until': delay_until.isoformat(),
|
||||
},
|
||||
operator_id='',
|
||||
)
|
||||
|
||||
# 注册定时任务
|
||||
from online_dev.workflow.engine.delay_callback import _register_delay_job
|
||||
|
||||
instance_id = str(context.instance.id)
|
||||
success = await _register_delay_job(instance_id, node_id, delay_until)
|
||||
|
||||
if not success:
|
||||
# 调度器不可用时,直接跳过延时继续推进(降级处理)
|
||||
logger.warning(f"调度器不可用,延时节点直接跳过: instance={instance_id}, node={node_id}")
|
||||
|
||||
# 清除延时状态
|
||||
context.instance.delay_node_id = ""
|
||||
context.instance.delay_until = None
|
||||
context.db.add(context.instance)
|
||||
await context.db.flush()
|
||||
|
||||
await self.create_log(
|
||||
context, node, 'delay_skip',
|
||||
comment='调度器不可用,延时跳过',
|
||||
operator_id='',
|
||||
)
|
||||
await self.advance_to_next(context, node)
|
||||
|
||||
@staticmethod
|
||||
def _calculate_delay_until(duration: int, unit: str) -> datetime:
|
||||
"""
|
||||
计算延时到期时间
|
||||
|
||||
Args:
|
||||
duration: 延时时长
|
||||
unit: 延时单位 (minute/hour/day/workday)
|
||||
|
||||
Returns:
|
||||
到期时间
|
||||
"""
|
||||
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)
|
||||
elif unit == 'workday':
|
||||
return DelayNodeHandler._add_workdays(now, duration)
|
||||
else:
|
||||
# 默认按小时处理
|
||||
return now + timedelta(hours=duration)
|
||||
|
||||
@staticmethod
|
||||
def _add_workdays(start: datetime, days: int) -> datetime:
|
||||
"""
|
||||
添加工作日(跳过周六日)
|
||||
|
||||
Args:
|
||||
start: 起始时间
|
||||
days: 工作日天数
|
||||
|
||||
Returns:
|
||||
目标时间(保持原始时分秒)
|
||||
"""
|
||||
current = start
|
||||
added = 0
|
||||
while added < days:
|
||||
current += timedelta(days=1)
|
||||
# weekday(): 0=周一, 5=周六, 6=周日
|
||||
if current.weekday() < 5:
|
||||
added += 1
|
||||
return current
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
办理节点处理器
|
||||
处理办理任务的创建和完成逻辑
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from online_dev.workflow.engine.handlers.base_handler import BaseNodeHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from online_dev.workflow.engine.base import ExecutionContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HandleHandler(BaseNodeHandler):
|
||||
"""
|
||||
办理节点处理器
|
||||
|
||||
办理节点类似审批节点,但办理完成后自动推进流程
|
||||
支持:
|
||||
- 任一办理(any)
|
||||
- 全部办理(all)
|
||||
- 依次办理(sequential)
|
||||
"""
|
||||
|
||||
async def execute(self, context: 'ExecutionContext', node: Dict) -> None:
|
||||
"""
|
||||
执行办理节点:创建办理任务
|
||||
"""
|
||||
from online_dev.workflow.engine.assignee_resolver import assignee_resolver
|
||||
|
||||
node_id = self.get_node_id(node)
|
||||
node_name = self.get_node_name(node)
|
||||
node_config = self.get_node_config(node)
|
||||
|
||||
logger.info(f"创建办理任务 - 节点: {node_id}, 名称: {node_name}")
|
||||
|
||||
# 解析办理人
|
||||
assignee_ids = await assignee_resolver.resolve(
|
||||
context.db,
|
||||
node_config,
|
||||
context.instance,
|
||||
context.form_data,
|
||||
)
|
||||
|
||||
if not assignee_ids:
|
||||
logger.warning(f"节点 {node_id} 没有办理人,自动跳过")
|
||||
await self.advance_to_next(context, node)
|
||||
return
|
||||
|
||||
# 更新实例当前节点
|
||||
await self.update_instance_node(context, node)
|
||||
|
||||
# 创建任务
|
||||
multi_handle = node_config.get('multiHandle', 'any')
|
||||
|
||||
if multi_handle == 'sequential':
|
||||
# 依次办理:只创建第一个人的任务
|
||||
await self.create_task(context, node, assignee_ids[0], 'handle')
|
||||
else:
|
||||
# 任一办理/全部办理:创建所有人的任务
|
||||
for assignee_id in assignee_ids:
|
||||
await self.create_task(context, node, assignee_id, 'handle')
|
||||
|
||||
async def handle_completion(self, context: 'ExecutionContext', task: Any) -> None:
|
||||
"""
|
||||
处理办理完成后的流程推进
|
||||
|
||||
Args:
|
||||
context: 执行上下文
|
||||
task: 已完成的任务
|
||||
"""
|
||||
from online_dev.workflow.model import WorkflowTask
|
||||
from online_dev.workflow.engine.utils import FlowUtils
|
||||
|
||||
node = FlowUtils.find_node_by_id(context.flow_definition, task.node_id)
|
||||
if not node:
|
||||
logger.error(f"找不到节点: {task.node_id}")
|
||||
return
|
||||
|
||||
node_config = self.get_node_config(node)
|
||||
multi_handle = node_config.get('multiHandle', 'any')
|
||||
|
||||
# 检查多人办理逻辑
|
||||
if multi_handle == 'all':
|
||||
# 全部办理:检查是否所有人都已办理
|
||||
stmt = select(WorkflowTask).where(
|
||||
WorkflowTask.instance_id == str(context.instance.id),
|
||||
WorkflowTask.node_id == task.node_id,
|
||||
WorkflowTask.status == 'pending',
|
||||
)
|
||||
result = await context.db.execute(stmt)
|
||||
pending_tasks = result.scalars().all()
|
||||
|
||||
if len(pending_tasks) > 0:
|
||||
logger.info(f"全部办理模式,还有 {len(pending_tasks)} 人未办理")
|
||||
return
|
||||
|
||||
elif multi_handle == 'sequential':
|
||||
# 依次办理:检查是否还有下一个人
|
||||
next_assignee = await self._get_next_sequential_assignee(context, task)
|
||||
if next_assignee:
|
||||
await self.create_task(context, node, next_assignee, 'handle')
|
||||
return
|
||||
|
||||
# 办理完成,推进到下一节点
|
||||
await self.advance_to_next(context, node)
|
||||
|
||||
async def _get_next_sequential_assignee(
|
||||
self,
|
||||
context: 'ExecutionContext',
|
||||
current_task: Any,
|
||||
) -> Optional[str]:
|
||||
"""获取依次办理的下一个办理人"""
|
||||
from online_dev.workflow.model import WorkflowTask
|
||||
from online_dev.workflow.engine.assignee_resolver import assignee_resolver
|
||||
from online_dev.workflow.engine.utils import FlowUtils
|
||||
|
||||
node = FlowUtils.find_node_by_id(context.flow_definition, current_task.node_id)
|
||||
if not node:
|
||||
return None
|
||||
|
||||
node_config = self.get_node_config(node)
|
||||
|
||||
all_assignees = await assignee_resolver.resolve(
|
||||
context.db,
|
||||
node_config,
|
||||
context.instance,
|
||||
context.form_data,
|
||||
)
|
||||
|
||||
if not all_assignees:
|
||||
return None
|
||||
|
||||
stmt = select(WorkflowTask.assignee_id).where(
|
||||
WorkflowTask.instance_id == str(context.instance.id),
|
||||
WorkflowTask.node_id == current_task.node_id,
|
||||
WorkflowTask.status == 'handled',
|
||||
).order_by(WorkflowTask.sys_create_datetime)
|
||||
result = await context.db.execute(stmt)
|
||||
handled_ids = [str(uid) for uid in result.scalars().all()]
|
||||
|
||||
handled_set = set(handled_ids)
|
||||
|
||||
for assignee_id in all_assignees:
|
||||
if assignee_id not in handled_set:
|
||||
return assignee_id
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,412 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
通知服务
|
||||
处理流程中各种通知的发送
|
||||
集成 core.message 消息服务
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from online_dev.workflow.engine.base import ExecutionContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _resolve_task_notify_channels(node: Dict) -> Optional[List[str]]:
|
||||
"""
|
||||
解析节点 taskNotify 渠道。
|
||||
返回 None 表示明确关闭任务通知。
|
||||
"""
|
||||
node_config = node.get('config', {}) if node else {}
|
||||
task_notify = node_config.get('taskNotify') or {}
|
||||
if task_notify.get('enabled') is False:
|
||||
return None
|
||||
return task_notify.get('channels') or ['site']
|
||||
|
||||
|
||||
class WorkflowNotificationService:
|
||||
"""
|
||||
工作流通知服务
|
||||
|
||||
统一处理流程中的各种通知:
|
||||
- 任务通知(待审批/待办理/抄送)
|
||||
- 发起人通知(通过/拒绝/完成)
|
||||
- 超时通知
|
||||
- 流程完成通知
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
async def send_task_notification(
|
||||
context: 'ExecutionContext',
|
||||
task: Any,
|
||||
assignee_id: str,
|
||||
task_type: str,
|
||||
node: Dict,
|
||||
) -> None:
|
||||
"""
|
||||
发送任务通知
|
||||
|
||||
Args:
|
||||
context: 执行上下文
|
||||
task: 任务对象
|
||||
assignee_id: 处理人ID
|
||||
task_type: 任务类型
|
||||
node: 节点配置
|
||||
"""
|
||||
from core.message.service import NotifyService
|
||||
|
||||
channels = _resolve_task_notify_channels(node)
|
||||
if channels is None:
|
||||
return
|
||||
|
||||
type_labels = {
|
||||
'approval': '审批',
|
||||
'handle': '办理',
|
||||
'copy': '抄送',
|
||||
}
|
||||
type_label = type_labels.get(task_type, '处理')
|
||||
|
||||
# 获取发起人名称
|
||||
initiator_name = '未知'
|
||||
if context.instance.initiator_id:
|
||||
from core.user.model import User
|
||||
from sqlalchemy import select
|
||||
stmt = select(User).where(User.id == context.instance.initiator_id)
|
||||
result = await context.db.execute(stmt)
|
||||
initiator = result.scalar_one_or_none()
|
||||
if initiator:
|
||||
initiator_name = initiator.name or initiator.username
|
||||
|
||||
instance_title = context.instance.title
|
||||
title = f"【{instance_title}】待{type_label}"
|
||||
content = f"{initiator_name} 发起的【{instance_title}】需要您{type_label},请及时处理。"
|
||||
|
||||
try:
|
||||
await NotifyService.send(
|
||||
db=context.db,
|
||||
recipient_ids=[assignee_id],
|
||||
title=title,
|
||||
content=content,
|
||||
channels=channels,
|
||||
msg_type='workflow',
|
||||
link_type='workflow_task',
|
||||
link_id=str(task.id),
|
||||
sender_id=context.current_user_id or None,
|
||||
)
|
||||
logger.info(f"任务通知已发送: {assignee_id}, 类型: {task_type}, 渠道: {channels}")
|
||||
except Exception as e:
|
||||
logger.error(f"发送任务通知失败: {e}")
|
||||
|
||||
@staticmethod
|
||||
async def send_initiator_notification(
|
||||
context: 'ExecutionContext',
|
||||
node: Dict,
|
||||
action: str,
|
||||
) -> None:
|
||||
"""
|
||||
发送发起人通知
|
||||
|
||||
Args:
|
||||
context: 执行上下文
|
||||
node: 当前节点
|
||||
action: 操作类型 (approve/reject/complete)
|
||||
"""
|
||||
from core.message.service import NotifyService
|
||||
|
||||
node_config = node.get('config', {}) if node else {}
|
||||
initiator_notify = node_config.get('initiatorNotify', {})
|
||||
|
||||
# 检查是否需要通知
|
||||
should_notify = False
|
||||
if action == 'approve':
|
||||
should_notify = initiator_notify.get('onApprove', False)
|
||||
elif action == 'reject':
|
||||
should_notify = initiator_notify.get('onReject', True)
|
||||
elif action == 'complete':
|
||||
should_notify = initiator_notify.get('onComplete', True)
|
||||
|
||||
if not should_notify:
|
||||
return
|
||||
|
||||
initiator_id = context.instance.initiator_id
|
||||
if not initiator_id:
|
||||
return
|
||||
|
||||
# 读取节点配置的通知渠道,默认站内信
|
||||
channels = initiator_notify.get('channels') or ['site']
|
||||
|
||||
node_name = node.get('name', '节点')
|
||||
instance_title = context.instance.title
|
||||
|
||||
action_labels = {
|
||||
'approve': '已通过',
|
||||
'reject': '已拒绝',
|
||||
'complete': '已完成',
|
||||
}
|
||||
action_label = action_labels.get(action, '已处理')
|
||||
|
||||
title = f"【{instance_title}】{action_label}"
|
||||
content = f"您发起的【{instance_title}】在【{node_name}】{action_label}。"
|
||||
|
||||
if action == 'reject':
|
||||
content += "请查看详情了解原因。"
|
||||
|
||||
try:
|
||||
await NotifyService.send(
|
||||
db=context.db,
|
||||
recipient_ids=[str(initiator_id)],
|
||||
title=title,
|
||||
content=content,
|
||||
channels=channels,
|
||||
msg_type='workflow',
|
||||
link_type='workflow_instance',
|
||||
link_id=str(context.instance.id),
|
||||
sender_id=context.current_user_id or None,
|
||||
)
|
||||
logger.info(f"发起人通知已发送: {initiator_id}, 操作: {action}, 渠道: {channels}")
|
||||
except Exception as e:
|
||||
logger.error(f"发送发起人通知失败: {e}")
|
||||
|
||||
@staticmethod
|
||||
async def send_instance_complete_notification(
|
||||
context: 'ExecutionContext',
|
||||
status: str,
|
||||
) -> None:
|
||||
"""
|
||||
发送流程完成通知给发起人
|
||||
|
||||
Args:
|
||||
context: 执行上下文
|
||||
status: 流程状态 (approved/rejected)
|
||||
"""
|
||||
from core.message.service import NotifyService
|
||||
|
||||
initiator_id = context.instance.initiator_id
|
||||
if not initiator_id:
|
||||
return
|
||||
|
||||
instance_title = context.instance.title
|
||||
|
||||
if status == 'approved':
|
||||
title = f"【{instance_title}】已通过"
|
||||
content = f"您发起的【{instance_title}】已全部审批通过。"
|
||||
elif status == 'rejected':
|
||||
title = f"【{instance_title}】已被拒绝"
|
||||
content = f"您发起的【{instance_title}】已被拒绝,请查看详情了解原因。"
|
||||
else:
|
||||
title = f"【{instance_title}】已结束"
|
||||
content = f"您发起的【{instance_title}】已结束。"
|
||||
|
||||
# 流程完成通知使用站内信(无节点配置可读取)
|
||||
try:
|
||||
await NotifyService.send(
|
||||
db=context.db,
|
||||
recipient_ids=[str(initiator_id)],
|
||||
title=title,
|
||||
content=content,
|
||||
channels=['site'],
|
||||
msg_type='workflow',
|
||||
link_type='workflow_instance',
|
||||
link_id=str(context.instance.id),
|
||||
)
|
||||
logger.info(f"流程完成通知已发送: {initiator_id}, 状态: {status}")
|
||||
except Exception as e:
|
||||
logger.error(f"发送流程完成通知失败: {e}")
|
||||
|
||||
@staticmethod
|
||||
async def send_timeout_notification(
|
||||
db,
|
||||
task,
|
||||
instance,
|
||||
assignee,
|
||||
) -> None:
|
||||
"""
|
||||
发送任务超时通知
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
task: 任务对象
|
||||
instance: 流程实例
|
||||
assignee: 处理人
|
||||
"""
|
||||
from core.message.service import NotifyService
|
||||
|
||||
if not assignee:
|
||||
return
|
||||
|
||||
# 获取发起人名称
|
||||
initiator_name = ''
|
||||
if instance.initiator_id:
|
||||
from core.user.model import User
|
||||
from sqlalchemy import select
|
||||
stmt = select(User).where(User.id == instance.initiator_id)
|
||||
result = await db.execute(stmt)
|
||||
initiator = result.scalar_one_or_none()
|
||||
if initiator:
|
||||
initiator_name = initiator.name or initiator.username
|
||||
|
||||
title = '任务超时提醒'
|
||||
content = (
|
||||
f'您有一个待办任务已超时,请尽快处理。\n'
|
||||
f'流程标题:{instance.title}\n'
|
||||
f'当前节点:{task.node_name}\n'
|
||||
f'发起人:{initiator_name}'
|
||||
)
|
||||
|
||||
# 超时通知使用站内信
|
||||
try:
|
||||
await NotifyService.send(
|
||||
db=db,
|
||||
recipient_ids=[str(assignee.id)],
|
||||
title=title,
|
||||
content=content,
|
||||
channels=['site'],
|
||||
msg_type='workflow',
|
||||
link_type='workflow_task',
|
||||
link_id=str(task.id),
|
||||
sender_id=str(instance.initiator_id) if instance.initiator_id else None,
|
||||
)
|
||||
logger.info(f"超时通知已发送: task={task.id}, assignee={assignee.id}")
|
||||
except Exception as e:
|
||||
logger.error(f"发送超时通知失败: {e}")
|
||||
|
||||
@staticmethod
|
||||
async def load_instance_and_flow(db, task: Any):
|
||||
"""加载任务关联的实例与流程定义。"""
|
||||
from online_dev.workflow.model import WorkflowDefinition, WorkflowInstance
|
||||
from sqlalchemy import select
|
||||
|
||||
stmt = select(WorkflowInstance).where(
|
||||
WorkflowInstance.id == task.instance_id,
|
||||
WorkflowInstance.is_deleted == False,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
instance = result.scalar_one_or_none()
|
||||
if not instance:
|
||||
return None, {}
|
||||
|
||||
stmt = select(WorkflowDefinition).where(
|
||||
WorkflowDefinition.id == instance.workflow_id,
|
||||
WorkflowDefinition.is_deleted == False,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
workflow = result.scalar_one_or_none()
|
||||
flow_definition = workflow.flow_definition if workflow else {}
|
||||
return instance, flow_definition or {}
|
||||
|
||||
@staticmethod
|
||||
async def notify_pending_task(
|
||||
db,
|
||||
task: Any,
|
||||
instance: Any,
|
||||
flow_definition: Dict,
|
||||
operator_id: str = None,
|
||||
) -> None:
|
||||
"""
|
||||
为已创建的任务发送通知(转办/委派等场景复用 create_task 逻辑)。
|
||||
"""
|
||||
from online_dev.workflow.engine.base import ExecutionContext
|
||||
from online_dev.workflow.engine.utils import FlowUtils
|
||||
|
||||
node = FlowUtils.find_node_by_id(flow_definition or {}, task.node_id)
|
||||
if not node:
|
||||
node = {
|
||||
'id': task.node_id,
|
||||
'name': task.node_name,
|
||||
'type': task.task_type,
|
||||
'config': {},
|
||||
}
|
||||
|
||||
context = ExecutionContext(
|
||||
instance=instance,
|
||||
form_data={},
|
||||
current_user_id=operator_id or '',
|
||||
flow_definition=flow_definition or {},
|
||||
db=db,
|
||||
)
|
||||
await WorkflowNotificationService.send_task_notification(
|
||||
context=context,
|
||||
task=task,
|
||||
assignee_id=str(task.assignee_id),
|
||||
task_type=task.task_type,
|
||||
node=node,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def send_urge_notifications(
|
||||
db,
|
||||
instance: Any,
|
||||
pending_tasks: list,
|
||||
flow_definition: Dict,
|
||||
operator_id: str,
|
||||
) -> int:
|
||||
"""
|
||||
催办:向待办人发送通知,渠道与节点 taskNotify 一致。
|
||||
|
||||
Returns:
|
||||
成功发送通知的去重处理人数量
|
||||
"""
|
||||
from core.message.service import NotifyService
|
||||
from core.user.model import User
|
||||
from online_dev.workflow.engine.utils import FlowUtils
|
||||
from sqlalchemy import select
|
||||
|
||||
if not pending_tasks:
|
||||
return 0
|
||||
|
||||
stmt = select(User).where(User.id == operator_id)
|
||||
result = await db.execute(stmt)
|
||||
initiator = result.scalar_one_or_none()
|
||||
initiator_name = (
|
||||
(initiator.name or initiator.username) if initiator else '发起人'
|
||||
)
|
||||
|
||||
instance_title = instance.title or '流程'
|
||||
notified_assignees: set[str] = set()
|
||||
sent_count = 0
|
||||
|
||||
for task in pending_tasks:
|
||||
assignee_id = str(task.assignee_id)
|
||||
if assignee_id in notified_assignees:
|
||||
continue
|
||||
|
||||
node = FlowUtils.find_node_by_id(flow_definition or {}, task.node_id)
|
||||
channels = _resolve_task_notify_channels(node or {})
|
||||
if channels is None:
|
||||
continue
|
||||
|
||||
type_labels = {
|
||||
'approval': '审批',
|
||||
'handle': '办理',
|
||||
}
|
||||
type_label = type_labels.get(task.task_type, '处理')
|
||||
title = f'【{instance_title}】催办提醒'
|
||||
content = (
|
||||
f'{initiator_name} 催办您尽快完成【{instance_title}】的{type_label},'
|
||||
f'当前节点:{task.node_name or "待处理"}。'
|
||||
)
|
||||
|
||||
try:
|
||||
await NotifyService.send(
|
||||
db=db,
|
||||
recipient_ids=[assignee_id],
|
||||
title=title,
|
||||
content=content,
|
||||
channels=channels,
|
||||
msg_type='workflow',
|
||||
link_type='workflow_task',
|
||||
link_id=str(task.id),
|
||||
sender_id=operator_id,
|
||||
)
|
||||
notified_assignees.add(assignee_id)
|
||||
sent_count += 1
|
||||
logger.info(
|
||||
f'催办通知已发送: instance={instance.id}, assignee={assignee_id}, '
|
||||
f'channels={channels}'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'发送催办通知失败 assignee={assignee_id}: {e}')
|
||||
|
||||
return sent_count
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
通知节点处理器
|
||||
处理流程中的通知发送
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
|
||||
from online_dev.workflow.engine.handlers.base_handler import BaseNodeHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from online_dev.workflow.engine.base import ExecutionContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NotifyHandler(BaseNodeHandler):
|
||||
"""
|
||||
通知节点处理器
|
||||
|
||||
支持多种通知渠道:
|
||||
- site: 站内信
|
||||
- email: 邮件
|
||||
- sms: 短信
|
||||
- wechat: 微信
|
||||
- dingtalk: 钉钉
|
||||
- feishu: 飞书
|
||||
"""
|
||||
|
||||
async def execute(self, context: 'ExecutionContext', node: Dict) -> None:
|
||||
"""
|
||||
执行通知节点:发送通知
|
||||
"""
|
||||
from online_dev.workflow.engine.assignee_resolver import assignee_resolver
|
||||
|
||||
node_id = self.get_node_id(node)
|
||||
node_name = self.get_node_name(node)
|
||||
node_config = self.get_node_config(node)
|
||||
|
||||
# 解析通知对象
|
||||
recipient_ids = await assignee_resolver.resolve(
|
||||
context.db,
|
||||
{
|
||||
'assigneeType': node_config.get('recipientType', 'user'),
|
||||
'assignees': node_config.get('recipients', []),
|
||||
'assigneeLevel': node_config.get('recipientLevel'),
|
||||
'assigneeField': node_config.get('recipientField'),
|
||||
'assigneeFields': node_config.get('recipientFields'),
|
||||
'assigneeFieldMappings': node_config.get('recipientFieldMappings'),
|
||||
},
|
||||
context.instance,
|
||||
context.form_data,
|
||||
)
|
||||
|
||||
channels = node_config.get('channels', ['site'])
|
||||
title = node_config.get('title', '')
|
||||
content = node_config.get('content', '')
|
||||
|
||||
logger.info(f"通知节点 - 发送给 {len(recipient_ids)} 人,渠道: {channels}")
|
||||
|
||||
# 构建模板变量上下文
|
||||
notify_context = {
|
||||
'initiator': '',
|
||||
'title': context.instance.title,
|
||||
'instance_no': context.instance.instance_no,
|
||||
'node_name': node_name,
|
||||
'form': context.form_data,
|
||||
}
|
||||
|
||||
# 调用通知服务发送
|
||||
if recipient_ids:
|
||||
from core.message.service import NotifyService
|
||||
await NotifyService.send(
|
||||
db=context.db,
|
||||
recipient_ids=recipient_ids,
|
||||
title=title,
|
||||
content=content,
|
||||
channels=channels,
|
||||
msg_type="workflow",
|
||||
context=notify_context,
|
||||
sender_id=context.instance.initiator_id,
|
||||
)
|
||||
|
||||
# 记录通知日志
|
||||
await self.create_log(
|
||||
context, node, 'notify',
|
||||
comment=f'发送通知: {title}',
|
||||
extra_data={
|
||||
'recipients': recipient_ids,
|
||||
'channels': channels,
|
||||
'title': title,
|
||||
'content': content,
|
||||
},
|
||||
)
|
||||
|
||||
logger.info(f"通知发送完成")
|
||||
|
||||
# 通知发送后继续推进
|
||||
await self.advance_to_next(context, node)
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
并行分支节点处理器
|
||||
处理并行分支的执行和汇聚
|
||||
"""
|
||||
import copy
|
||||
import logging
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from online_dev.workflow.engine.handlers.base_handler import BaseNodeHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from online_dev.workflow.engine.base import ExecutionContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ParallelHandler(BaseNodeHandler):
|
||||
"""
|
||||
并行分支节点处理器
|
||||
|
||||
同时执行所有分支,所有分支完成后汇聚继续
|
||||
"""
|
||||
|
||||
async def execute(self, context: 'ExecutionContext', node: Dict) -> None:
|
||||
"""
|
||||
执行并行节点:同时启动所有分支
|
||||
"""
|
||||
node_id = self.get_node_id(node)
|
||||
node_name = self.get_node_name(node)
|
||||
branches = node.get('branches', [])
|
||||
|
||||
logger.info(f"处理并行分支 - 节点: {node_id}, 分支数: {len(branches)}")
|
||||
|
||||
if not branches:
|
||||
logger.warning(f"并行节点 {node_id} 没有分支,自动跳过")
|
||||
await self.advance_to_next(context, node)
|
||||
return
|
||||
|
||||
# 更新实例当前节点
|
||||
await self.update_instance_node(context, node)
|
||||
|
||||
# 初始化并行分支状态(深拷贝确保 SQLAlchemy 检测到变更)
|
||||
parallel_status = copy.deepcopy(context.instance.parallel_branch_status or {})
|
||||
parallel_status[node_id] = {
|
||||
branch.get('id', ''): 'pending' for branch in branches
|
||||
}
|
||||
context.instance.parallel_branch_status = parallel_status
|
||||
flag_modified(context.instance, 'parallel_branch_status')
|
||||
context.db.add(context.instance)
|
||||
await context.db.flush()
|
||||
|
||||
# 记录进入并行分支
|
||||
await self.create_log(
|
||||
context, node, 'parallel_start',
|
||||
comment=f'进入并行分支,共 {len(branches)} 个分支',
|
||||
extra_data={
|
||||
'branch_count': len(branches),
|
||||
'branch_ids': [b.get('id') for b in branches],
|
||||
},
|
||||
)
|
||||
|
||||
# 同时执行所有分支
|
||||
for branch in branches:
|
||||
branch_children = branch.get('children')
|
||||
if branch_children:
|
||||
await self._execute_branch(context, node, branch, branch_children)
|
||||
else:
|
||||
# 空分支,标记为完成
|
||||
await self.mark_branch_complete(context, node, branch)
|
||||
|
||||
# 检查是否所有分支都已完成
|
||||
await self.check_completion(context, node)
|
||||
|
||||
async def _execute_branch(
|
||||
self,
|
||||
context: 'ExecutionContext',
|
||||
parallel_node: Dict,
|
||||
branch: Dict,
|
||||
first_node: Dict,
|
||||
) -> None:
|
||||
"""
|
||||
执行并行分支中的第一个节点。
|
||||
统一通过引擎 handler 分发,支持所有节点类型(approval, handle, copy, notify,
|
||||
condition, delay, service, subflow, data_update, parallel 等)。
|
||||
非阻塞节点执行后会自动调用 advance_to_next 推进;阻塞节点(approval/handle)
|
||||
在任务完成时由引擎推进。当分支走到末尾时,引擎的 _advance_to_next 会通过
|
||||
find_parallel_branch_for_node 找到所属并行分支并 mark_branch_complete。
|
||||
"""
|
||||
node_type = first_node.get('type')
|
||||
handler = self.engine._get_handler(node_type)
|
||||
|
||||
if handler:
|
||||
await handler.execute(context, first_node)
|
||||
else:
|
||||
logger.warning(f"并行分支内未知节点类型: {node_type},标记分支完成")
|
||||
await self.mark_branch_complete(context, parallel_node, branch)
|
||||
|
||||
async def mark_branch_complete(
|
||||
self,
|
||||
context: 'ExecutionContext',
|
||||
parallel_node: Dict,
|
||||
branch: Dict,
|
||||
) -> None:
|
||||
"""
|
||||
标记并行分支完成
|
||||
"""
|
||||
node_id = self.get_node_id(parallel_node)
|
||||
branch_id = branch.get('id', '')
|
||||
branch_name = branch.get('name', '')
|
||||
|
||||
# 更新分支状态(深拷贝确保 SQLAlchemy 检测到 JSON 字段变更)
|
||||
await context.db.refresh(context.instance)
|
||||
parallel_status = copy.deepcopy(context.instance.parallel_branch_status or {})
|
||||
|
||||
if node_id in parallel_status:
|
||||
parallel_status[node_id][branch_id] = 'completed'
|
||||
context.instance.parallel_branch_status = parallel_status
|
||||
flag_modified(context.instance, 'parallel_branch_status')
|
||||
context.db.add(context.instance)
|
||||
await context.db.flush()
|
||||
|
||||
logger.info(f"并行分支完成 - 节点: {node_id}, 分支: {branch_id}")
|
||||
|
||||
# 记录分支完成
|
||||
await self.create_log(
|
||||
context, parallel_node, 'parallel_branch_complete',
|
||||
comment=f'并行分支 {branch_name} 完成',
|
||||
extra_data={'branch_id': branch_id},
|
||||
)
|
||||
|
||||
# 检查是否所有分支都已完成
|
||||
await self.check_completion(context, parallel_node)
|
||||
|
||||
async def check_completion(self, context: 'ExecutionContext', parallel_node: Dict) -> None:
|
||||
"""
|
||||
检查并行分支是否全部完成
|
||||
"""
|
||||
node_id = self.get_node_id(parallel_node)
|
||||
|
||||
# 刷新实例获取最新状态
|
||||
await context.db.refresh(context.instance)
|
||||
parallel_status = copy.deepcopy(context.instance.parallel_branch_status or {})
|
||||
|
||||
branch_statuses = parallel_status.get(node_id, {})
|
||||
|
||||
if not branch_statuses:
|
||||
logger.warning(f"并行节点 {node_id} 没有分支状态记录")
|
||||
return
|
||||
|
||||
# 检查是否所有分支都已完成
|
||||
all_completed = all(status == 'completed' for status in branch_statuses.values())
|
||||
pending_branches = [bid for bid, status in branch_statuses.items() if status != 'completed']
|
||||
|
||||
logger.info(f"并行节点 {node_id} 状态检查: 全部完成={all_completed}, 待完成={pending_branches}")
|
||||
|
||||
if all_completed:
|
||||
logger.info(f"并行节点 {node_id} 所有分支完成,继续推进")
|
||||
|
||||
# 清理已完成的并行节点状态(深拷贝确保变更检测)
|
||||
parallel_status = copy.deepcopy(parallel_status)
|
||||
del parallel_status[node_id]
|
||||
context.instance.parallel_branch_status = parallel_status
|
||||
flag_modified(context.instance, 'parallel_branch_status')
|
||||
context.db.add(context.instance)
|
||||
await context.db.flush()
|
||||
|
||||
# 记录日志
|
||||
await self.create_log(
|
||||
context, parallel_node, 'parallel_complete',
|
||||
comment='所有并行分支完成',
|
||||
)
|
||||
|
||||
# 推进到下一节点
|
||||
await self.advance_to_next(context, parallel_node)
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
服务调用节点处理器
|
||||
处理外部 HTTP 服务调用
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import Dict, Optional, TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
|
||||
from online_dev.workflow.engine.handlers.base_handler import BaseNodeHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from online_dev.workflow.engine.base import ExecutionContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ServiceHandler(BaseNodeHandler):
|
||||
"""
|
||||
服务调用节点处理器
|
||||
|
||||
支持:
|
||||
- HTTP 方法: GET/POST/PUT/DELETE/PATCH
|
||||
- 请求头配置
|
||||
- 参数变量替换
|
||||
- 重试机制
|
||||
- 失败处理策略
|
||||
"""
|
||||
|
||||
async def execute(self, context: 'ExecutionContext', node: Dict) -> None:
|
||||
"""
|
||||
执行服务调用节点
|
||||
"""
|
||||
node_id = self.get_node_id(node)
|
||||
node_name = self.get_node_name(node)
|
||||
node_config = self.get_node_config(node)
|
||||
|
||||
url = node_config.get('url', '')
|
||||
method = node_config.get('method', 'POST')
|
||||
headers = {h['key']: h['value'] for h in node_config.get('headers', []) if h.get('key')}
|
||||
params = node_config.get('params', '')
|
||||
body = node_config.get('body', '')
|
||||
timeout = node_config.get('timeout', 30)
|
||||
retry_count = node_config.get('retryCount', 0)
|
||||
fail_action = node_config.get('failAction', 'stop')
|
||||
result_variable = node_config.get('resultVariable', '')
|
||||
|
||||
logger.info(f"服务调用节点 - {method} {url}")
|
||||
|
||||
# 更新实例当前节点
|
||||
await self.update_instance_node(context, node)
|
||||
|
||||
success = False
|
||||
response_data = None
|
||||
error_message = ''
|
||||
|
||||
# 尝试调用服务
|
||||
async with httpx.AsyncClient() as client:
|
||||
for attempt in range(retry_count + 1):
|
||||
try:
|
||||
# 解析参数和请求体(支持变量替换)
|
||||
parsed_params = self._parse_params(params, context.form_data)
|
||||
parsed_body = self._parse_params(body, context.form_data)
|
||||
|
||||
response = await client.request(
|
||||
method=method,
|
||||
url=url,
|
||||
headers=headers,
|
||||
params=parsed_params if method == 'GET' else None,
|
||||
json=parsed_body if method != 'GET' and parsed_body else None,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
response_data = response.json() if response.text else {}
|
||||
success = True
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
logger.warning(f"服务调用失败 (尝试 {attempt + 1}/{retry_count + 1}): {e}")
|
||||
|
||||
# 记录日志
|
||||
await self.create_log(
|
||||
context, node, 'service_call',
|
||||
comment=f"{'成功' if success else '失败'}: {method} {url}",
|
||||
extra_data={
|
||||
'url': url,
|
||||
'method': method,
|
||||
'success': success,
|
||||
'response': response_data,
|
||||
'error': error_message,
|
||||
},
|
||||
)
|
||||
|
||||
if success:
|
||||
# 存储结果到流程变量
|
||||
if result_variable and response_data:
|
||||
context.form_data[result_variable] = response_data
|
||||
await self.advance_to_next(context, node)
|
||||
else:
|
||||
# 处理失败
|
||||
if fail_action == 'continue':
|
||||
logger.warning(f"服务调用失败,继续流程")
|
||||
await self.advance_to_next(context, node)
|
||||
elif fail_action == 'stop':
|
||||
logger.error(f"服务调用失败,终止流程")
|
||||
await self.engine._end_instance(context, 'rejected')
|
||||
|
||||
def _parse_params(self, params_str: str, form_data: Dict) -> Optional[Dict]:
|
||||
"""
|
||||
解析服务调用参数,支持变量替换
|
||||
|
||||
变量格式: ${field_name}
|
||||
"""
|
||||
if not params_str:
|
||||
return None
|
||||
|
||||
# 替换变量
|
||||
def replace_var(match):
|
||||
var_name = match.group(1)
|
||||
return str(form_data.get(var_name, ''))
|
||||
|
||||
replaced = re.sub(r'\$\{(\w+)\}', replace_var, params_str)
|
||||
|
||||
try:
|
||||
return json.loads(replaced)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
子流程节点处理器
|
||||
处理子流程的启动和完成
|
||||
"""
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from online_dev.workflow.engine.handlers.base_handler import BaseNodeHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from online_dev.workflow.engine.base import ExecutionContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SubflowHandler(BaseNodeHandler):
|
||||
"""
|
||||
子流程节点处理器
|
||||
|
||||
支持:
|
||||
- 变量传递(全部/选择/无)
|
||||
- 等待子流程完成
|
||||
- 超时处理
|
||||
- 结果回传
|
||||
"""
|
||||
|
||||
async def execute(self, context: 'ExecutionContext', node: Dict) -> None:
|
||||
"""
|
||||
执行子流程节点:启动子流程
|
||||
"""
|
||||
from online_dev.workflow.model import WorkflowDefinition, WorkflowInstance
|
||||
from online_dev.workflow.engine.base import ExecutionContext as EC
|
||||
|
||||
node_id = self.get_node_id(node)
|
||||
node_name = self.get_node_name(node)
|
||||
node_config = self.get_node_config(node)
|
||||
|
||||
subflow_id = node_config.get('subflowId', '')
|
||||
subflow_name = node_config.get('subflowName', '')
|
||||
var_pass_mode = node_config.get('varPassMode', 'all')
|
||||
selected_vars = node_config.get('selectedVars', [])
|
||||
wait_for_completion = node_config.get('waitForCompletion', True)
|
||||
|
||||
logger.info(f"子流程节点 - 启动子流程: {subflow_name} ({subflow_id})")
|
||||
|
||||
if not subflow_id:
|
||||
logger.warning(f"子流程节点 {node_id} 未配置子流程,自动跳过")
|
||||
await self.advance_to_next(context, node)
|
||||
return
|
||||
|
||||
# 获取子流程定义
|
||||
stmt = select(WorkflowDefinition).where(
|
||||
WorkflowDefinition.id == subflow_id,
|
||||
WorkflowDefinition.status == 'published',
|
||||
WorkflowDefinition.is_deleted == False,
|
||||
)
|
||||
result = await context.db.execute(stmt)
|
||||
subflow_definition = result.scalar_one_or_none()
|
||||
|
||||
if not subflow_definition:
|
||||
logger.error(f"子流程定义不存在或未发布: {subflow_id}")
|
||||
await self.advance_to_next(context, node)
|
||||
return
|
||||
|
||||
# 更新实例当前节点
|
||||
await self.update_instance_node(context, node)
|
||||
|
||||
# 准备传递给子流程的变量
|
||||
if var_pass_mode == 'all':
|
||||
subflow_data = context.form_data.copy()
|
||||
elif var_pass_mode == 'selected':
|
||||
subflow_data = {k: v for k, v in context.form_data.items() if k in selected_vars}
|
||||
else:
|
||||
subflow_data = {}
|
||||
|
||||
# 生成子流程实例编号
|
||||
sub_instance_no = f"SUB-{context.instance.instance_no}-{uuid.uuid4().hex[:6].upper()}"
|
||||
|
||||
# 获取超时配置
|
||||
timeout = self._calculate_timeout(node_config)
|
||||
timeout_action = node_config.get('timeoutAction', 'skip')
|
||||
|
||||
# 创建子流程实例
|
||||
sub_instance = WorkflowInstance(
|
||||
workflow_id=str(subflow_definition.id),
|
||||
instance_no=sub_instance_no,
|
||||
title=f"[子流程] {subflow_name} - {context.instance.title}",
|
||||
status='pending',
|
||||
initiator_id=context.current_user_id or '',
|
||||
form_code=subflow_definition.form_code,
|
||||
form_data_id=context.instance.form_data_id,
|
||||
is_subflow=True,
|
||||
parent_instance_id=str(context.instance.id),
|
||||
parent_node_id=node_id,
|
||||
subflow_timeout=timeout,
|
||||
subflow_timeout_action=timeout_action,
|
||||
)
|
||||
context.db.add(sub_instance)
|
||||
await context.db.flush()
|
||||
|
||||
# 记录日志
|
||||
await self.create_log(
|
||||
context, node, 'subflow_start',
|
||||
comment=f'启动子流程: {subflow_name}',
|
||||
extra_data={
|
||||
'subflow_id': subflow_id,
|
||||
'subflow_name': subflow_name,
|
||||
'sub_instance_id': str(sub_instance.id),
|
||||
'sub_instance_no': sub_instance_no,
|
||||
'var_pass_mode': var_pass_mode,
|
||||
},
|
||||
)
|
||||
|
||||
logger.info(f"子流程实例已创建: {sub_instance_no}")
|
||||
|
||||
# 启动子流程
|
||||
sub_flow_def = subflow_definition.flow_definition
|
||||
sub_context = EC(
|
||||
instance=sub_instance,
|
||||
form_data=subflow_data,
|
||||
current_user_id=context.current_user_id,
|
||||
flow_definition=sub_flow_def,
|
||||
db=context.db,
|
||||
)
|
||||
|
||||
# 找到子流程的开始节点并执行
|
||||
start_node = sub_flow_def.get('nodes')
|
||||
|
||||
if start_node and start_node.get('type') == 'start':
|
||||
await self.engine._advance_to_next(sub_context, start_node)
|
||||
else:
|
||||
logger.error(f"子流程没有开始节点: {subflow_id}")
|
||||
|
||||
# 如果不等待完成,主流程继续推进
|
||||
if not wait_for_completion:
|
||||
logger.info(f"子流程不等待完成,主流程继续推进")
|
||||
await self.advance_to_next(context, node)
|
||||
else:
|
||||
logger.info(f"主流程等待子流程完成: {sub_instance_no}")
|
||||
|
||||
def _calculate_timeout(self, node_config: Dict) -> int:
|
||||
"""计算超时秒数"""
|
||||
timeout_enabled = node_config.get('timeoutEnabled', False)
|
||||
timeout_value = node_config.get('timeout', 24)
|
||||
timeout_unit = node_config.get('timeoutUnit', 'hour')
|
||||
|
||||
if not timeout_enabled or not timeout_value:
|
||||
return None
|
||||
|
||||
if timeout_unit == 'minute':
|
||||
return timeout_value * 60
|
||||
elif timeout_unit == 'hour':
|
||||
return timeout_value * 3600
|
||||
elif timeout_unit == 'day':
|
||||
return timeout_value * 86400
|
||||
return timeout_value * 3600
|
||||
|
||||
async def resume_parent(self, db, sub_instance) -> None:
|
||||
"""
|
||||
子流程完成后恢复父流程
|
||||
"""
|
||||
from online_dev.workflow.model import WorkflowLog, WorkflowInstance, WorkflowDefinition
|
||||
from online_dev.workflow.engine.base import ExecutionContext as EC
|
||||
from online_dev.workflow.engine.utils import FlowUtils, FormDataUtils
|
||||
|
||||
if not sub_instance.parent_instance_id:
|
||||
return
|
||||
|
||||
parent_node_id = sub_instance.parent_node_id
|
||||
if not parent_node_id:
|
||||
return
|
||||
|
||||
# 获取父流程实例
|
||||
stmt = select(WorkflowInstance).where(WorkflowInstance.id == sub_instance.parent_instance_id)
|
||||
result = await db.execute(stmt)
|
||||
parent_instance = result.scalar_one_or_none()
|
||||
|
||||
if not parent_instance:
|
||||
return
|
||||
|
||||
logger.info(f"子流程完成,恢复父流程: {parent_instance.instance_no}")
|
||||
|
||||
# 获取父流程定义
|
||||
stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == parent_instance.workflow_id)
|
||||
result = await db.execute(stmt)
|
||||
parent_workflow = result.scalar_one_or_none()
|
||||
|
||||
if not parent_workflow:
|
||||
return
|
||||
|
||||
parent_flow_def = parent_workflow.flow_definition
|
||||
|
||||
# 查找父流程等待的节点
|
||||
parent_node = FlowUtils.find_node_by_id(parent_flow_def, parent_node_id)
|
||||
if not parent_node:
|
||||
logger.error(f"父流程节点不存在: {parent_node_id}")
|
||||
return
|
||||
|
||||
# 获取表单数据
|
||||
form_data = await FormDataUtils.load_form_data(
|
||||
db,
|
||||
parent_instance.form_code,
|
||||
parent_instance.form_data_id,
|
||||
)
|
||||
|
||||
# 结果回传
|
||||
node_config = parent_node.get('config', {})
|
||||
result_pass_mode = node_config.get('resultPassMode', 'none')
|
||||
result_vars = node_config.get('resultVars', [])
|
||||
|
||||
if result_pass_mode != 'none':
|
||||
sub_form_data = await FormDataUtils.load_form_data(
|
||||
db,
|
||||
sub_instance.form_code,
|
||||
sub_instance.form_data_id,
|
||||
)
|
||||
if sub_form_data:
|
||||
if result_pass_mode == 'all':
|
||||
form_data.update(sub_form_data)
|
||||
elif result_pass_mode == 'selected':
|
||||
for var in result_vars:
|
||||
if var in sub_form_data:
|
||||
form_data[var] = sub_form_data[var]
|
||||
|
||||
# 记录日志
|
||||
log = WorkflowLog(
|
||||
instance_id=str(parent_instance.id),
|
||||
node_id=parent_node_id,
|
||||
node_name=parent_node.get('name', '子流程'),
|
||||
action='subflow_complete',
|
||||
operator_id=sub_instance.initiator_id,
|
||||
comment=f'子流程完成: {sub_instance.instance_no}, 状态: {sub_instance.status}',
|
||||
extra_data={
|
||||
'sub_instance_id': str(sub_instance.id),
|
||||
'sub_instance_no': sub_instance.instance_no,
|
||||
'sub_status': sub_instance.status,
|
||||
},
|
||||
)
|
||||
db.add(log)
|
||||
await db.flush()
|
||||
|
||||
# 构建上下文并推进父流程
|
||||
parent_context = EC(
|
||||
instance=parent_instance,
|
||||
form_data=form_data,
|
||||
current_user_id=sub_instance.initiator_id or '',
|
||||
flow_definition=parent_flow_def,
|
||||
db=db,
|
||||
)
|
||||
|
||||
# 根据子流程结果决定父流程走向
|
||||
if sub_instance.status == 'approved':
|
||||
await self.engine._advance_to_next(parent_context, parent_node)
|
||||
elif sub_instance.status == 'rejected':
|
||||
await self.engine._end_instance(parent_context, 'rejected')
|
||||
else:
|
||||
await self.engine._advance_to_next(parent_context, parent_node)
|
||||
Reference in New Issue
Block a user