feat: restore source parity and harden agent runtime

This commit is contained in:
2026-06-22 11:17:26 +08:00
parent e33f08277b
commit 0793eb82d6
596 changed files with 168879 additions and 290 deletions
@@ -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: 操作人IDNone时使用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)