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,26 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
工作流引擎模块
"""
from online_dev.workflow.engine.workflow_engine import WorkflowEngine
from online_dev.workflow.engine.base import (
ExecutionContext,
TaskAction,
NodeType,
MultiApprovalType,
InstanceStatus,
TaskStatus,
generate_instance_no,
)
__all__ = [
'WorkflowEngine',
'ExecutionContext',
'TaskAction',
'NodeType',
'MultiApprovalType',
'InstanceStatus',
'TaskStatus',
'generate_instance_no',
]
@@ -0,0 +1,353 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
审批人解析器
负责根据节点配置解析实际的审批人列表
"""
import logging
from typing import List, Dict, Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger(__name__)
class AssigneeResolver:
"""
审批人解析器
支持的审批人类型:
- user: 指定用户
- role: 指定角色(获取角色下所有用户)
- department: 指定部门(获取部门下所有用户)
- superior: 上级主管(根据发起人的部门层级)
- manager: 直属经理(发起人的直属上级)
- initiator: 发起人自己
- form_field: 表单字段(从表单数据中获取用户)
"""
async def resolve(
self,
db: AsyncSession,
node_config: Dict,
instance: Any,
form_data: Dict,
) -> List[str]:
"""
解析审批人
Args:
db: 数据库会话
node_config: 节点配置(包含 assigneeType, assignees 等)
instance: 流程实例
form_data: 表单数据
Returns:
List[str]: 用户ID列表
"""
assignee_type = node_config.get('assigneeType', 'user')
resolvers = {
'user': self._resolve_users,
'role': self._resolve_by_role,
'department': self._resolve_by_department,
'superior': self._resolve_superior,
'manager': self._resolve_manager,
'initiator': self._resolve_initiator,
'form_field': self._resolve_from_form,
}
resolver = resolvers.get(assignee_type)
if not resolver:
logger.warning(f"未知的审批人类型: {assignee_type}")
return []
try:
return await resolver(db, node_config, instance, form_data)
except Exception as e:
logger.exception(f"解析审批人失败: {e}")
return []
async def _resolve_users(
self,
db: AsyncSession,
node_config: Dict,
instance: Any,
form_data: Dict,
) -> List[str]:
"""指定用户"""
assignees = node_config.get('assignees', [])
logger.info(f"[_resolve_users] assignees from config: {assignees}")
result = [str(uid) for uid in assignees] if assignees else []
logger.info(f"[_resolve_users] result: {result}")
return result
async def _resolve_by_role(
self,
db: AsyncSession,
node_config: Dict,
instance: Any,
form_data: Dict,
) -> List[str]:
"""根据角色获取用户(通过用户角色关联表)"""
from core.user.model import User
from core.user.user_role_model import UserRole
role_ids = node_config.get('assignees', [])
if not role_ids:
return []
# 通过 UserRole 关联表查询拥有指定角色的用户
stmt = select(User.id).join(
UserRole,
User.id == UserRole.user_id
).where(
UserRole.role_id.in_(role_ids),
User.is_deleted == False,
User.is_active == True,
).distinct()
result = await db.execute(stmt)
users = result.scalars().all()
logger.info(f"[_resolve_by_role] role_ids: {role_ids}, found users: {users}")
return [str(uid) for uid in users]
async def _resolve_by_department(
self,
db: AsyncSession,
node_config: Dict,
instance: Any,
form_data: Dict,
) -> List[str]:
"""根据部门获取用户"""
from core.user.model import User
dept_ids = node_config.get('assignees', [])
if not dept_ids:
return []
stmt = select(User.id).where(
User.dept_id.in_(dept_ids),
User.is_deleted == False,
User.is_active == True,
).distinct()
result = await db.execute(stmt)
users = result.scalars().all()
return [str(uid) for uid in users]
async def _resolve_superior(
self,
db: AsyncSession,
node_config: Dict,
instance: Any,
form_data: Dict,
) -> List[str]:
"""
获取上级主管
assigneeLevel 含义:
- 1 = 直接上级(当前部门的负责人)
- 2 = 上级的上级(父部门的负责人)
- 以此类推
"""
from core.user.model import User
from core.dept.model import Dept
level = node_config.get('assigneeLevel', 1)
initiator_id = instance.initiator_id
# 获取发起人信息
stmt = select(User).where(User.id == initiator_id)
result = await db.execute(stmt)
initiator = result.scalar_one_or_none()
if not initiator or not initiator.dept_id:
logger.warning(f"发起人 {initiator_id} 没有部门信息")
return []
# 获取部门信息
current_dept_id = initiator.dept_id
for _ in range(level - 1):
stmt = select(Dept).where(Dept.id == current_dept_id)
result = await db.execute(stmt)
dept = result.scalar_one_or_none()
if dept and dept.parent_id:
current_dept_id = dept.parent_id
else:
break
# 获取部门负责人
stmt = select(Dept).where(Dept.id == current_dept_id)
result = await db.execute(stmt)
current_dept = result.scalar_one_or_none()
if current_dept and current_dept.lead_id:
# 如果负责人是发起人自己,尝试向上找
if str(current_dept.lead_id) == str(initiator_id):
logger.info(f"部门负责人是发起人自己,尝试向上查找")
if current_dept.parent_id:
stmt = select(Dept).where(Dept.id == current_dept.parent_id)
result = await db.execute(stmt)
parent_dept = result.scalar_one_or_none()
if parent_dept and parent_dept.lead_id:
return [str(parent_dept.lead_id)]
return [str(current_dept.lead_id)]
logger.warning(f"部门没有负责人")
return []
async def _resolve_manager(
self,
db: AsyncSession,
node_config: Dict,
instance: Any,
form_data: Dict,
) -> List[str]:
"""
获取直属经理
assigneeLevel 含义:
- 1 = 直属经理(发起人的 manager)
- 2 = 经理的经理(发起人的 manager 的 manager
- 以此类推
"""
from core.user.model import User
level = node_config.get('assigneeLevel', 1)
initiator_id = instance.initiator_id
current_user_id = initiator_id
for _ in range(level):
stmt = select(User).where(User.id == current_user_id)
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if user and user.manager_id:
current_user_id = user.manager_id
else:
logger.warning(f"用户没有设置直属经理")
return []
return [str(current_user_id)]
async def _resolve_initiator(
self,
db: AsyncSession,
node_config: Dict,
instance: Any,
form_data: Dict,
) -> List[str]:
"""发起人自己"""
if instance.initiator_id:
return [str(instance.initiator_id)]
return []
async def _resolve_from_form(
self,
db: AsyncSession,
node_config: Dict,
instance: Any,
form_data: Dict,
) -> List[str]:
"""从表单字段获取用户,支持多字段收集,支持 form-selector 间接查询"""
field_names = node_config.get('assigneeFields') or node_config.get('recipientFields')
if not field_names:
single = node_config.get('assigneeField') or node_config.get('recipientField', '')
field_names = [single] if single else []
if not field_names:
return []
# 构建 form-selector 字段映射表: fieldName -> {formCode, userField}
field_mappings = {}
for mapping in (node_config.get('assigneeFieldMappings') or node_config.get('recipientFieldMappings') or []):
field_mappings[mapping['fieldName']] = mapping
result = []
seen = set()
for field_name in field_names:
value = form_data.get(field_name)
if not value:
continue
mapping = field_mappings.get(field_name)
if mapping:
# form-selector 字段: value 是引用表单记录的 ID,需要间接查询用户字段
user_ids = await self._resolve_form_selector_field(
db, value, mapping.get('formCode', ''), mapping.get('userField', '')
)
for uid_str in user_ids:
if uid_str and uid_str not in seen:
seen.add(uid_str)
result.append(uid_str)
else:
# user-selector 字段: value 直接就是用户 ID
values = value if isinstance(value, list) else [value]
for uid in values:
uid_str = str(uid)
if uid_str and uid_str not in seen:
seen.add(uid_str)
result.append(uid_str)
return result
async def _resolve_form_selector_field(
self,
db: AsyncSession,
record_ids: Any,
form_code: str,
user_field: str,
) -> List[str]:
"""
从 form-selector 引用的表单记录中提取用户字段值
Args:
db: 数据库会话
record_ids: 引用的表单记录 ID(单个或列表)
form_code: 引用的表单编码
user_field: 引用表单中的用户字段名
"""
if not form_code or not user_field:
logger.warning(f"form-selector 映射配置不完整: form_code={form_code}, user_field={user_field}")
return []
ids = record_ids if isinstance(record_ids, list) else [record_ids]
if not ids:
return []
result = []
try:
from online_dev.form_data_manager.service import FormDataService
service = await FormDataService.create_service(db, form_code)
for record_id in ids:
try:
record = await service.get(db, str(record_id))
if not record:
logger.warning(f"form-selector 引用记录不存在: form_code={form_code}, id={record_id}")
continue
user_value = record.get(user_field)
if not user_value:
continue
user_values = user_value if isinstance(user_value, list) else [user_value]
for uid in user_values:
result.append(str(uid))
except Exception as e:
logger.warning(f"查询 form-selector 引用记录失败: form_code={form_code}, id={record_id}, error={e}")
continue
except Exception as e:
logger.exception(f"form-selector 字段解析失败: {e}")
return result
# 全局实例
assignee_resolver = AssigneeResolver()
@@ -0,0 +1,100 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
工作流引擎基础模块
定义核心类型、枚举和数据类
"""
import logging
import uuid
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Dict, Any
logger = logging.getLogger(__name__)
class NodeType(Enum):
"""节点类型"""
START = 'start'
END = 'end'
APPROVAL = 'approval'
HANDLE = 'handle'
COPY = 'copy'
CONDITION = 'condition'
PARALLEL = 'parallel'
DELAY = 'delay'
NOTIFY = 'notify'
SERVICE = 'service'
SUBFLOW = 'subflow'
DATA_UPDATE = 'data_update'
class TaskAction(Enum):
"""任务操作"""
APPROVE = 'approve'
REJECT = 'reject'
TRANSFER = 'transfer'
RETURN = 'return'
DELEGATE = 'delegate'
ADD_SIGN = 'add_sign'
class MultiApprovalType(Enum):
"""多人审批类型"""
SEQUENTIAL = 'sequential'
PARALLEL = 'parallel'
ANY = 'any'
class InstanceStatus(Enum):
"""流程实例状态"""
PENDING = 'pending'
APPROVED = 'approved'
REJECTED = 'rejected'
CANCELED = 'canceled'
class TaskStatus(Enum):
"""任务状态"""
PENDING = 'pending'
WAITING = 'waiting'
APPROVED = 'approved'
REJECTED = 'rejected'
RETURNED = 'returned'
TRANSFERRED = 'transferred'
DELEGATED = 'delegated'
HANDLED = 'handled'
CANCELED = 'canceled'
READ = 'read'
@dataclass
class ExecutionContext:
"""
执行上下文
在流程执行过程中传递的上下文信息
"""
instance: Any
form_data: Dict
current_user_id: str
flow_definition: Dict
db: Any = None
def get_initiator_id(self) -> str:
"""获取发起人ID"""
return self.instance.initiator_id if self.instance else ''
def get_instance_id(self) -> str:
"""获取实例ID"""
return str(self.instance.id) if self.instance else ''
def get_current_node_id(self) -> str:
"""获取当前节点ID"""
return self.instance.current_node_id if self.instance else ''
def generate_instance_no() -> str:
"""生成流程实例编号"""
now = datetime.now()
return f"WF{now.strftime('%Y%m%d%H%M%S')}{str(uuid.uuid4())[:8].upper()}"
@@ -0,0 +1,218 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
条件表达式求值器
负责解析和执行条件分支的表达式
"""
import logging
import operator
from decimal import Decimal
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
class ConditionEvaluator:
"""
条件求值器
支持的操作符:
- eq: 等于
- ne: 不等于
- gt: 大于
- gte: 大于等于
- lt: 小于
- lte: 小于等于
- contains: 包含
- not_contains: 不包含
- in: 在...中
- not_in: 不在...中
- empty: 为空
- not_empty: 不为空
"""
# 操作符映射
OPERATORS = {
'eq': operator.eq,
'ne': operator.ne,
'gt': operator.gt,
'gte': operator.ge,
'lt': operator.lt,
'lte': operator.le,
}
def evaluate_groups(self, groups: List[Dict], form_data: Dict) -> bool:
"""
求值条件组列表(组之间是 OR 关系)
Args:
groups: 条件组列表,每个组包含 conditions 数组
form_data: 表单数据
Returns:
bool: 任一组满足则返回 True
"""
if not groups:
return True
for group in groups:
if self.evaluate_group(group, form_data):
return True
return False
def evaluate_group(self, group: Dict, form_data: Dict) -> bool:
"""
求值单个条件组(组内条件是 AND 关系)
Args:
group: 条件组,包含 conditions 数组
form_data: 表单数据
Returns:
bool: 所有条件都满足才返回 True
"""
conditions = group.get('conditions', [])
if not conditions:
return True
for condition in conditions:
if not self.evaluate_condition(condition, form_data):
return False
return True
def evaluate_condition(self, condition: Dict, form_data: Dict) -> bool:
"""
求值单个条件
Args:
condition: 条件定义 {field, operator, value}
form_data: 表单数据
Returns:
bool: 条件是否满足
"""
field = condition.get('field', '')
op = condition.get('operator', 'eq')
expected_value = condition.get('value')
actual_value = self._get_field_value(form_data, field)
logger.info(
f"条件求值: field={field}, operator={op}, "
f"expected={expected_value!r}(type={type(expected_value).__name__}), "
f"actual={actual_value!r}(type={type(actual_value).__name__})"
)
try:
result = self._compare(actual_value, op, expected_value)
logger.info(f"条件求值结果: {result}")
return result
except Exception as e:
logger.warning(f"条件求值失败: {condition}, 错误: {e}")
return False
def _get_field_value(self, data: Dict, field: str) -> Any:
"""
获取字段值,支持嵌套路径(如 user.dept.name
"""
if not field:
return None
parts = field.split('.')
value = data
for part in parts:
if isinstance(value, dict):
value = value.get(part)
else:
return None
return value
def _compare(self, actual: Any, op: str, expected: Any) -> bool:
"""
执行比较操作
"""
# 空值检查
if op == 'empty':
return self._is_empty(actual)
if op == 'not_empty':
return not self._is_empty(actual)
# 包含检查
if op == 'contains':
return self._contains(actual, expected)
if op == 'not_contains':
return not self._contains(actual, expected)
# 集合检查
if op == 'in':
return self._in_list(actual, expected)
if op == 'not_in':
return not self._in_list(actual, expected)
# 数值比较(需要类型转换)
if op in ('gt', 'gte', 'lt', 'lte'):
actual = self._to_number(actual)
expected = self._to_number(expected)
if actual is None or expected is None:
return False
# 等于/不等于:先尝试原始比较,如果类型不同则统一转字符串再比较
if op in ('eq', 'ne'):
if type(actual) != type(expected) and actual is not None and expected is not None:
result = self.OPERATORS[op](str(actual).strip(), str(expected).strip())
logger.debug(f"类型不一致,转字符串比较: {str(actual)!r} {op} {str(expected)!r} = {result}")
return result
return self.OPERATORS[op](actual, expected)
# 其他标准比较
if op in self.OPERATORS:
return self.OPERATORS[op](actual, expected)
return actual == expected
def _is_empty(self, value: Any) -> bool:
"""检查值是否为空"""
if value is None:
return True
if isinstance(value, str) and value.strip() == '':
return True
if isinstance(value, (list, dict)) and len(value) == 0:
return True
return False
def _contains(self, actual: Any, expected: Any) -> bool:
"""检查是否包含"""
if actual is None:
return False
if isinstance(actual, str):
return str(expected) in actual
if isinstance(actual, (list, tuple)):
return expected in actual
return False
def _in_list(self, actual: Any, expected: Any) -> bool:
"""检查是否在列表中"""
if not isinstance(expected, (list, tuple)):
expected = [expected]
return actual in expected
def _to_number(self, value: Any) -> Optional[float]:
"""转换为数字"""
if value is None:
return None
if isinstance(value, (int, float, Decimal)):
return float(value)
if isinstance(value, str):
try:
return float(value)
except ValueError:
return None
return None
# 全局实例
condition_evaluator = ConditionEvaluator()
@@ -0,0 +1,290 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
延时节点回调函数
当延时到期后,由调度器调用此函数推进工作流
"""
import logging
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger(__name__)
def get_delay_job_id(instance_id: str, node_id: str) -> str:
"""生成延时任务的唯一 job_id"""
return f"wf_delay_{instance_id}_{node_id}"
async def workflow_delay_callback(instance_id: str, node_id: str):
"""
延时到期回调 - 推进工作流
由调度器在延时到期后调用,负责:
1. 加载流程实例,校验状态
2. 清除延时状态
3. 记录延时完成日志
4. 推进流程到下一节点
Args:
instance_id: 流程实例ID
node_id: 延时节点ID
"""
from app.database import AsyncSessionLocal
logger.info(f"延时回调触发: instance_id={instance_id}, node_id={node_id}")
async with AsyncSessionLocal() as db:
try:
await _execute_delay_resume(db, instance_id, node_id)
await db.commit()
logger.info(f"延时回调完成: instance_id={instance_id}, node_id={node_id}")
except Exception as e:
await db.rollback()
logger.error(f"延时回调失败: instance_id={instance_id}, node_id={node_id}, error={e}", exc_info=True)
raise
async def _execute_delay_resume(db: AsyncSession, instance_id: str, node_id: str):
"""执行延时恢复逻辑"""
from online_dev.workflow.model import WorkflowInstance, WorkflowDefinition, WorkflowLog
from online_dev.workflow.engine.base import ExecutionContext
from online_dev.workflow.engine.utils import FlowUtils
# 1. 加载流程实例
stmt = select(WorkflowInstance).where(
WorkflowInstance.id == instance_id,
WorkflowInstance.is_deleted == False,
)
result = await db.execute(stmt)
instance = result.scalar_one_or_none()
if not instance:
logger.warning(f"延时回调: 流程实例不存在 {instance_id}")
return
# 2. 校验实例状态
if instance.status != 'pending':
logger.info(f"延时回调: 流程实例状态非 pending ({instance.status}),跳过 {instance_id}")
return
# 3. 校验延时节点匹配
if instance.delay_node_id != node_id:
logger.warning(
f"延时回调: 节点不匹配,期望 {node_id},实际 {instance.delay_node_id},跳过"
)
return
# 4. 加载流程定义
stmt = select(WorkflowDefinition).where(
WorkflowDefinition.id == instance.workflow_id,
WorkflowDefinition.is_deleted == False,
)
result = await db.execute(stmt)
definition = result.scalar_one_or_none()
if not definition or not definition.flow_definition:
logger.error(f"延时回调: 流程定义不存在或无效 workflow_id={instance.workflow_id}")
return
flow_def = definition.flow_definition
# 5. 查找延时节点
delay_node = FlowUtils.find_node_by_id(flow_def.get('nodes'), node_id)
if not delay_node:
logger.error(f"延时回调: 延时节点不存在 node_id={node_id}")
return
# 6. 清除实例的延时状态
instance.delay_node_id = ""
instance.delay_until = None
db.add(instance)
await db.flush()
# 7. 记录延时完成日志
log = WorkflowLog(
instance_id=str(instance.id),
node_id=node_id,
node_name=delay_node.get('name', '延时等待'),
action='delay_complete',
operator_id='',
comment='延时等待结束,流程继续',
)
db.add(log)
await db.flush()
# 8. 构建执行上下文并推进流程
from online_dev.workflow.engine.utils import FormDataUtils
form_data = {}
if instance.form_code and instance.form_data_id:
try:
form_data = await FormDataUtils.load_form_data(db, instance.form_code, instance.form_data_id)
except Exception as e:
logger.warning(f"延时回调: 加载表单数据失败: {e}")
context = ExecutionContext(
instance=instance,
form_data=form_data,
current_user_id='',
flow_definition=flow_def,
db=db,
)
# 9. 推进到下一节点
from online_dev.workflow.engine.workflow_engine import WorkflowEngine
engine = WorkflowEngine()
await engine._advance_to_next(context, delay_node)
logger.info(f"延时回调: 流程已推进 instance_id={instance_id}")
async def recover_pending_delay_tasks():
"""
应用启动时恢复未完成的延时任务
查询所有处于延时等待状态的流程实例,重新注册定时任务:
- delay_until > now: 注册定时任务等待到期
- delay_until <= now: 直接执行回调推进流程
"""
from app.database import AsyncSessionLocal
from online_dev.workflow.model import WorkflowInstance
logger.info("开始恢复未完成的延时任务...")
try:
async with AsyncSessionLocal() as db:
# 查询所有有延时状态的 pending 实例
stmt = select(WorkflowInstance).where(
WorkflowInstance.status == 'pending',
WorkflowInstance.delay_node_id != '',
WorkflowInstance.delay_node_id.isnot(None),
WorkflowInstance.delay_until.isnot(None),
WorkflowInstance.is_deleted == False,
)
result = await db.execute(stmt)
instances = list(result.scalars().all())
if not instances:
logger.info("没有需要恢复的延时任务")
return
now = datetime.now()
recovered = 0
expired = 0
for instance in instances:
instance_id = str(instance.id)
node_id = instance.delay_node_id
delay_until = instance.delay_until
if delay_until > now:
# 未到期,重新注册定时任务
success = await _register_delay_job(instance_id, node_id, delay_until)
if success:
recovered += 1
logger.info(
f"恢复延时任务: instance={instance_id}, node={node_id}, "
f"到期时间={delay_until}"
)
else:
# 已过期,直接执行回调
expired += 1
logger.info(
f"延时任务已过期,立即执行: instance={instance_id}, node={node_id}"
)
try:
await workflow_delay_callback(instance_id, node_id)
except Exception as e:
logger.error(f"执行过期延时回调失败: {e}", exc_info=True)
logger.info(f"延时任务恢复完成: 重新注册 {recovered} 个, 立即执行 {expired}")
except Exception as e:
logger.error(f"恢复延时任务失败: {e}", exc_info=True)
async def _register_delay_job(instance_id: str, node_id: str, delay_until: datetime) -> bool:
"""
注册延时定时任务到调度器
Args:
instance_id: 流程实例ID
node_id: 延时节点ID
delay_until: 到期时间
Returns:
是否注册成功
"""
try:
from scheduler.service import scheduler_service
scheduler = scheduler_service.get_scheduler()
if not scheduler:
logger.warning("调度器未初始化,无法注册延时任务")
return False
job_id = get_delay_job_id(instance_id, node_id)
# 创建回调包装函数(闭包捕获参数)
_instance_id = instance_id
_node_id = node_id
async def delay_wrapper():
await workflow_delay_callback(_instance_id, _node_id)
# 注册任务
await scheduler.configure_task(job_id, func=delay_wrapper)
# 添加一次性调度
from apscheduler.triggers.date import DateTrigger
await scheduler.add_schedule(
func_or_task_id=job_id,
trigger=DateTrigger(run_time=delay_until),
id=job_id,
)
logger.info(f"延时任务已注册: job_id={job_id}, 到期时间={delay_until}")
return True
except Exception as e:
logger.error(f"注册延时任务失败: {e}", exc_info=True)
return False
async def remove_delay_job(instance_id: str, node_id: str) -> bool:
"""
从调度器移除延时任务
Args:
instance_id: 流程实例ID
node_id: 延时节点ID
Returns:
是否移除成功
"""
try:
from scheduler.service import scheduler_service
scheduler = scheduler_service.get_scheduler()
if not scheduler:
return False
job_id = get_delay_job_id(instance_id, node_id)
try:
await scheduler.remove_schedule(job_id)
logger.info(f"延时任务已移除: job_id={job_id}")
except Exception:
# 任务可能不存在(已执行或已清理)
pass
return True
except Exception as e:
logger.error(f"移除延时任务失败: {e}", exc_info=True)
return False
@@ -0,0 +1,272 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
工作流延时节点处理器
使用 APScheduler 实现延时等待功能
"""
import json
import logging
from datetime import datetime, timedelta
from typing import Optional
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger(__name__)
def calculate_delay_datetime(duration: int, unit: str) -> datetime:
"""
计算延时后的执行时间
Args:
duration: 延时时长
unit: 延时单位 - minute/hour/day/workday
Returns:
datetime: 延时后的执行时间
"""
now = datetime.now()
if unit == 'minute':
return now + timedelta(minutes=duration)
elif unit == 'hour':
return now + timedelta(hours=duration)
elif unit == 'day':
return now + timedelta(days=duration)
elif unit == 'workday':
# 工作日计算(跳过周末)
result = now
days_added = 0
while days_added < duration:
result += timedelta(days=1)
# 周一到周五是工作日 (0-4)
if result.weekday() < 5:
days_added += 1
return result
else:
# 默认按小时
return now + timedelta(hours=duration)
async def create_delay_job(
db: AsyncSession,
instance_id: str,
node_id: str,
duration: int,
unit: str,
user_id: str,
) -> Optional[str]:
"""
创建延时任务
Args:
db: 数据库会话
instance_id: 流程实例ID
node_id: 延时节点ID
duration: 延时时长
unit: 延时单位
user_id: 操作用户ID
Returns:
str: 任务编码,失败返回 None
"""
try:
from scheduler.model import SchedulerJob
from scheduler.service import SchedulerService
# 计算执行时间
run_date = calculate_delay_datetime(duration, unit)
# 生成唯一的任务编码
job_code = f"workflow_delay_{instance_id}_{node_id}"
# 检查是否已存在
stmt = select(SchedulerJob).where(SchedulerJob.code == job_code)
result = await db.execute(stmt)
existing = result.scalar_one_or_none()
if existing:
# 更新执行时间
existing.run_date = run_date
existing.status = 1 # 启用
db.add(existing)
await db.flush()
# 更新调度器中的任务
scheduler_service = SchedulerService()
if scheduler_service.is_running():
await scheduler_service.modify_job(existing)
logger.info(f"更新延时任务: {job_code}, 执行时间: {run_date}")
return job_code
# 创建新任务
job = SchedulerJob(
name=f"工作流延时-{instance_id[:8]}",
code=job_code,
description=f"流程实例 {instance_id} 的延时节点 {node_id}",
group='workflow_delay',
trigger_type='date',
run_date=run_date,
task_func='core.workflow.engine.delay_process.execute_delay_complete',
task_kwargs=json.dumps({
'instance_id': instance_id,
'node_id': node_id,
'user_id': user_id,
}),
status=1, # 启用
max_instances=1,
coalesce=True,
)
db.add(job)
await db.flush()
# 添加到调度器
scheduler_service = SchedulerService()
if scheduler_service.is_running():
await scheduler_service.add_job(job)
logger.info(f"创建延时任务: {job_code}, 执行时间: {run_date}")
return job_code
except Exception as e:
logger.error(f"创建延时任务失败: {e}")
return None
async def cancel_delay_job(db: AsyncSession, instance_id: str, node_id: str) -> bool:
"""
取消延时任务
Args:
db: 数据库会话
instance_id: 流程实例ID
node_id: 延时节点ID
Returns:
bool: 是否成功
"""
try:
from scheduler.model import SchedulerJob
from scheduler.service import SchedulerService
job_code = f"workflow_delay_{instance_id}_{node_id}"
# 从调度器移除
scheduler_service = SchedulerService()
if scheduler_service.is_running():
await scheduler_service.remove_job(job_code)
# 禁用数据库记录
stmt = update(SchedulerJob).where(
SchedulerJob.code == job_code
).values(status=0)
await db.execute(stmt)
await db.flush()
logger.info(f"取消延时任务: {job_code}")
return True
except Exception as e:
logger.error(f"取消延时任务失败: {e}")
return False
async def execute_delay_complete(db: AsyncSession, instance_id: str, node_id: str, user_id: str):
"""
延时完成后执行的任务
Args:
db: 数据库会话
instance_id: 流程实例ID
node_id: 延时节点ID
user_id: 操作用户ID
"""
from online_dev.workflow.model import WorkflowInstance, WorkflowDefinition, WorkflowLog
from online_dev.workflow.engine.workflow_engine import WorkflowEngine
from online_dev.workflow.engine.base import ExecutionContext
from online_dev.workflow.engine.utils import FlowUtils
from core.user.model import User
logger.info(f"延时任务执行: instance={instance_id}, node={node_id}")
try:
# 获取流程实例
stmt = select(WorkflowInstance).where(
WorkflowInstance.id == instance_id,
WorkflowInstance.is_deleted == False,
)
result = await db.execute(stmt)
instance = result.scalar_one_or_none()
if not instance:
logger.error(f"流程实例不存在: {instance_id}")
return f"流程实例不存在: {instance_id}"
# 检查流程状态
if instance.status != 'pending':
logger.warning(f"流程实例 {instance_id} 状态不是 pending,跳过延时完成")
return f"流程状态不是 pending: {instance.status}"
# 检查当前节点
if instance.current_node_id != node_id:
logger.warning(f"流程实例 {instance_id} 当前节点不是 {node_id},跳过延时完成")
return f"当前节点不匹配: {instance.current_node_id}"
# 获取流程定义
stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id)
result = await db.execute(stmt)
workflow = result.scalar_one_or_none()
if not workflow:
logger.error(f"流程定义不存在: {instance.workflow_id}")
return f"流程定义不存在"
# 获取用户
stmt = select(User).where(User.id == user_id)
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if not user:
# 使用发起人
stmt = select(User).where(User.id == instance.initiator_id)
result = await db.execute(stmt)
user = result.scalar_one_or_none()
# 记录延时完成日志
log = WorkflowLog(
instance_id=str(instance.id),
node_id=node_id,
node_name=instance.current_node_name,
action='delay_complete',
operator_id=str(user.id) if user else '',
comment='延时等待完成',
)
db.add(log)
await db.flush()
# 创建执行上下文
engine = WorkflowEngine()
context = ExecutionContext(
instance=instance,
form_data={},
current_user=user,
flow_definition=workflow.flow_definition,
db=db,
)
# 查找延时节点并推进
delay_node = FlowUtils.find_node_by_id(context.flow_definition, node_id)
if delay_node:
await engine._advance_to_next(context, delay_node)
logger.info(f"延时节点 {node_id} 完成,流程继续推进")
return "延时完成,流程已推进"
else:
logger.error(f"找不到延时节点: {node_id}")
return f"找不到延时节点: {node_id}"
except Exception as e:
logger.exception(f"延时任务执行失败: {e}")
raise
@@ -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)
@@ -0,0 +1,420 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
工作流任务超时处理器
负责任务超时检测、提醒、自动处理等功能
使用 APScheduler 实现定时检查
"""
import logging
from datetime import datetime, timedelta
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger(__name__)
TIMEOUT_CHECK_TASK_FUNC = (
'online_dev.workflow.engine.task_timeout_process.execute_timeout_check'
)
TIMEOUT_CHECK_JOB_CODE = 'workflow_task_timeout_check'
def calculate_timeout_datetime(duration: int, unit: str) -> datetime:
"""
计算超时时间点
Args:
duration: 超时时长
unit: 超时单位 - minute/hour/day
Returns:
datetime: 超时时间点
"""
now = datetime.now()
if unit == 'minute':
return now + timedelta(minutes=duration)
elif unit == 'hour':
return now + timedelta(hours=duration)
elif unit == 'day':
return now + timedelta(days=duration)
else:
# 默认按小时
return now + timedelta(hours=duration)
async def set_task_timeout(db: AsyncSession, task, timeout_config: dict) -> bool:
"""
为任务设置超时时间
Args:
db: 数据库会话
task: WorkflowTask 实例
timeout_config: 超时配置 {enabled, duration, unit, action}
Returns:
bool: 是否设置成功
"""
if not timeout_config or not timeout_config.get('enabled'):
return False
try:
duration = timeout_config.get('duration', 24)
unit = timeout_config.get('unit', 'hour')
action = timeout_config.get('action', 'notify')
# 计算超时时间
timeout_at = calculate_timeout_datetime(duration, unit)
# 更新任务
task.timeout_at = timeout_at
task.timeout_action = action
task.timeout_notified = False
db.add(task)
await db.flush()
logger.info(f"任务 {task.id} 设置超时: {timeout_at}, 操作: {action}")
return True
except Exception as e:
logger.error(f"设置任务超时失败: {e}")
return False
async def check_and_handle_timeouts(db: AsyncSession):
"""
检查并处理超时任务
此函数应该由定时任务每分钟调用一次
"""
from online_dev.workflow.model import WorkflowTask
now = datetime.now()
# 查找所有已超时但未处理的待办任务
stmt = select(WorkflowTask).where(
WorkflowTask.status == 'pending',
WorkflowTask.timeout_at <= now,
WorkflowTask.timeout_at.isnot(None),
WorkflowTask.is_deleted == False,
)
result = await db.execute(stmt)
timeout_tasks = result.scalars().all()
logger.info(f"检查超时任务,发现 {len(timeout_tasks)} 个超时任务")
for task in timeout_tasks:
try:
await _handle_timeout_task(db, task)
except Exception as e:
logger.exception(f"处理超时任务 {task.id} 失败: {e}")
async def _handle_timeout_task(db: AsyncSession, task):
"""
处理单个超时任务
"""
from online_dev.workflow.model import WorkflowInstance
action = task.timeout_action or 'notify'
# 获取实例
stmt = select(WorkflowInstance).where(WorkflowInstance.id == task.instance_id)
result = await db.execute(stmt)
instance = result.scalar_one_or_none()
if not instance or instance.status != 'pending':
# 流程已结束,跳过
return
logger.info(f"处理超时任务: task={task.id}, action={action}")
if action == 'notify':
# 发送超时提醒通知
await _send_timeout_notification(db, task, instance)
elif action == 'auto_approve':
# 自动通过
await _auto_complete_task(db, task, instance, 'approve')
elif action == 'auto_reject':
# 自动拒绝
await _auto_complete_task(db, task, instance, 'reject')
async def _send_timeout_notification(db: AsyncSession, task, instance):
"""
发送超时提醒通知
"""
from online_dev.workflow.model import WorkflowLog
from online_dev.workflow.engine.handlers.notification_service import WorkflowNotificationService
from core.user.model import User
# 检查是否已发送过通知
if task.timeout_notified:
return
# 获取处理人
stmt = select(User).where(User.id == task.assignee_id)
result = await db.execute(stmt)
assignee = result.scalar_one_or_none()
if not assignee:
return
# 获取发起人
stmt = select(User).where(User.id == instance.initiator_id)
result = await db.execute(stmt)
initiator = result.scalar_one_or_none()
try:
# 发送超时通知
await WorkflowNotificationService.send_timeout_notification(
db=db,
task=task,
instance=instance,
assignee=assignee,
)
# 标记已通知
task.timeout_notified = True
db.add(task)
# 记录日志
log = WorkflowLog(
instance_id=str(instance.id),
node_id=task.node_id,
node_name=task.node_name,
action='task_timeout_notify',
operator_id=str(initiator.id) if initiator else '',
comment=f'任务超时提醒已发送给 {assignee.name or assignee.username}',
extra_data={
'task_id': str(task.id),
'assignee_id': str(assignee.id),
'assignee_name': assignee.name or assignee.username,
},
)
db.add(log)
await db.flush()
logger.info(f"已发送超时提醒: task={task.id}, assignee={assignee.id}")
except Exception as e:
logger.error(f"发送超时提醒失败: {e}")
async def _auto_complete_task(db: AsyncSession, task, instance, action_type: str):
"""
自动完成超时任务
Args:
db: 数据库会话
task: 任务
instance: 流程实例
action_type: 'approve''reject'
"""
from online_dev.workflow.model import WorkflowLog, WorkflowDefinition
from online_dev.workflow.engine.workflow_engine import WorkflowEngine
from online_dev.workflow.engine.base import ExecutionContext
from core.user.model import User
# 获取发起人作为操作者
stmt = select(User).where(User.id == instance.initiator_id)
result = await db.execute(stmt)
operator = result.scalar_one_or_none()
# 确定操作类型和日志动作
if action_type == 'approve':
log_action = 'task_auto_approve'
status = 'approved'
comment = '任务超时,系统自动通过'
else:
log_action = 'task_auto_reject'
status = 'rejected'
comment = '任务超时,系统自动拒绝'
# 更新任务状态
task.status = status
task.comment = comment
task.handled_at = datetime.now()
db.add(task)
# 记录日志
log = WorkflowLog(
instance_id=str(instance.id),
node_id=task.node_id,
node_name=task.node_name,
action=log_action,
operator_id=str(operator.id) if operator else '',
comment=comment,
extra_data={
'task_id': str(task.id),
'assignee_id': str(task.assignee_id),
'timeout_at': task.timeout_at.isoformat() if task.timeout_at else None,
},
)
db.add(log)
await db.flush()
logger.info(f"任务自动{action_type}: task={task.id}")
# 完成该任务对应的钉钉待办
try:
from core.message.service import NotifyService
await NotifyService.complete_dingtalk_todo(db, "workflow_task", str(task.id))
except Exception as e:
logger.warning(f"超时自动处理-清理钉钉待办失败 task={task.id}: {e}")
# 获取流程定义
stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id)
result = await db.execute(stmt)
workflow = result.scalar_one_or_none()
if not workflow:
return
# 使用流程引擎推进流程
engine = WorkflowEngine()
operator_id = str(operator.id) if operator else ''
context = ExecutionContext(
instance=instance,
form_data={},
current_user_id=operator_id,
flow_definition=workflow.flow_definition,
db=db,
)
if action_type == 'approve':
# 通过:按任务类型推进(办理节点走办理完成逻辑)
if task.task_type == 'handle':
await engine._handle_handle_completion(context, task)
else:
await engine._handle_approval(context, task)
else:
# 拒绝:结束流程
await engine._end_instance(context, 'rejected')
async def execute_timeout_check(**kwargs):
"""
定时任务入口函数
由调度器每分钟调用
"""
from app.database import AsyncSessionLocal
logger.info("开始执行任务超时检查...")
try:
async with AsyncSessionLocal() as db:
await check_and_handle_timeouts(db)
await db.commit()
return "任务超时检查完成"
except Exception as e:
logger.exception(f"任务超时检查失败: {e}")
raise
async def create_timeout_check_job(db: AsyncSession) -> bool:
"""
创建任务超时检查的定时任务
每分钟执行一次
Returns:
bool: 是否创建成功
"""
try:
from scheduler.model import SchedulerJob
from scheduler.service import SchedulerService
job_code = TIMEOUT_CHECK_JOB_CODE
# 检查是否已存在
stmt = select(SchedulerJob).where(SchedulerJob.code == job_code)
result = await db.execute(stmt)
existing = result.scalar_one_or_none()
scheduler_service = SchedulerService()
if existing:
updated = False
if existing.task_func != TIMEOUT_CHECK_TASK_FUNC:
existing.task_func = TIMEOUT_CHECK_TASK_FUNC
updated = True
logger.warning(
f'已修正任务超时检查 task_func: {TIMEOUT_CHECK_TASK_FUNC}'
)
if existing.status != 1:
existing.status = 1
updated = True
if updated:
db.add(existing)
await db.flush()
if scheduler_service.is_running():
try:
await scheduler_service.remove_job(job_code)
except Exception:
pass
await scheduler_service.add_job(existing)
logger.info(f'任务超时检查定时任务已就绪: {job_code}')
return True
# 创建定时任务(每分钟执行)
job = SchedulerJob(
name='工作流任务超时检查',
code=job_code,
description='每分钟检查一次超时的工作流任务,执行超时提醒或自动处理',
group='workflow',
trigger_type='interval',
interval_seconds=60, # 每60秒执行一次
task_func=TIMEOUT_CHECK_TASK_FUNC,
task_kwargs='{}',
status=1, # 启用
max_instances=1,
coalesce=True,
)
db.add(job)
await db.flush()
# 添加到调度器
if scheduler_service.is_running():
await scheduler_service.add_job(job)
logger.info(f"创建任务超时检查定时任务成功: {job_code}")
return True
except Exception as e:
logger.error(f"创建任务超时检查定时任务失败: {e}")
return False
async def remove_timeout_check_job(db: AsyncSession) -> bool:
"""
移除任务超时检查的定时任务
Returns:
bool: 是否移除成功
"""
try:
from scheduler.model import SchedulerJob
from scheduler.service import SchedulerService
job_code = TIMEOUT_CHECK_JOB_CODE
# 从调度器移除
scheduler_service = SchedulerService()
if scheduler_service.is_running():
await scheduler_service.remove_job(job_code)
# 删除数据库记录
stmt = select(SchedulerJob).where(SchedulerJob.code == job_code)
result = await db.execute(stmt)
job = result.scalar_one_or_none()
if job:
await db.delete(job)
await db.flush()
logger.info(f"移除任务超时检查定时任务成功: {job_code}")
return True
except Exception as e:
logger.error(f"移除任务超时检查定时任务失败: {e}")
return False
@@ -0,0 +1,449 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
工作流引擎辅助工具
包含节点查找、表单数据处理等通用方法
"""
import logging
from typing import Dict, Optional, Tuple
logger = logging.getLogger(__name__)
class FlowUtils:
"""流程工具类"""
@staticmethod
def find_node_by_id(flow_def: Dict, node_id: str) -> Optional[Dict]:
"""
在流程定义中查找节点
Args:
flow_def: 流程定义
node_id: 节点ID
Returns:
节点配置字典,未找到返回 None
"""
nodes = flow_def.get('nodes')
if not nodes:
return None
return FlowUtils._find_node_recursive(nodes, node_id)
@staticmethod
def _find_node_recursive(node: Dict, target_id: str) -> Optional[Dict]:
"""递归查找节点"""
if node.get('id') == target_id:
return node
# 在子节点中查找
children = node.get('children')
if children:
result = FlowUtils._find_node_recursive(children, target_id)
if result:
return result
# 在分支中查找
branches = node.get('branches', [])
for branch in branches:
branch_children = branch.get('children')
if branch_children:
result = FlowUtils._find_node_recursive(branch_children, target_id)
if result:
return result
return None
@staticmethod
def find_parallel_branch_for_node(flow_def: Dict, node_id: str) -> Optional[Tuple[Dict, Dict]]:
"""
查找节点所在的并行分支
Args:
flow_def: 流程定义
node_id: 节点ID
Returns:
(parallel_node, branch) 元组,如果节点不在并行分支内则返回 None
"""
nodes = flow_def.get('nodes')
if not nodes:
return None
return FlowUtils._find_parallel_branch_recursive(nodes, node_id, None, None)
@staticmethod
def _find_parallel_branch_recursive(
node: Dict,
target_id: str,
current_parallel: Optional[Dict],
current_branch: Optional[Dict]
) -> Optional[Tuple[Dict, Dict]]:
"""递归查找节点所在的并行分支"""
if not node or not isinstance(node, dict):
return None
# 检查当前节点
if node.get('id') == target_id:
if current_parallel and current_branch:
return (current_parallel, current_branch)
return None
node_type = node.get('type')
# 如果是并行节点,在其分支中查找
if node_type == 'parallel':
branches = node.get('branches', [])
for branch in branches:
branch_children = branch.get('children')
if branch_children:
result = FlowUtils._find_parallel_branch_recursive(
branch_children, target_id, node, branch
)
if result:
return result
# 检查子节点
children = node.get('children')
if children:
result = FlowUtils._find_parallel_branch_recursive(
children, target_id, current_parallel, current_branch
)
if result:
return result
# 检查条件分支
if node_type == 'condition':
branches = node.get('branches', [])
for branch in branches:
branch_children = branch.get('children')
if branch_children:
result = FlowUtils._find_parallel_branch_recursive(
branch_children, target_id, current_parallel, current_branch
)
if result:
return result
return None
@staticmethod
def find_condition_branch_for_node(flow_def: Dict, node_id: str) -> Optional[Dict]:
"""
查找节点所在的条件分支,返回条件节点
Args:
flow_def: 流程定义
node_id: 节点ID
Returns:
条件节点字典,如果节点不在条件分支内则返回 None
"""
nodes = flow_def.get('nodes')
if not nodes:
return None
return FlowUtils._find_condition_branch_recursive(nodes, node_id, None)
@staticmethod
def _find_condition_branch_recursive(
node: Dict,
target_id: str,
current_condition: Optional[Dict],
) -> Optional[Dict]:
"""递归查找节点所在的条件分支"""
if not node or not isinstance(node, dict):
return None
if node.get('id') == target_id:
return current_condition
node_type = node.get('type')
# 如果是条件节点,在其分支中查找(标记当前条件节点)
if node_type == 'condition':
branches = node.get('branches', [])
for branch in branches:
branch_children = branch.get('children')
if branch_children:
result = FlowUtils._find_condition_branch_recursive(
branch_children, target_id, node
)
if result is not None:
return result
# 如果是并行节点,在其分支中查找(保持当前条件上下文)
if node_type == 'parallel':
branches = node.get('branches', [])
for branch in branches:
branch_children = branch.get('children')
if branch_children:
result = FlowUtils._find_condition_branch_recursive(
branch_children, target_id, current_condition
)
if result is not None:
return result
# 检查子节点
children = node.get('children')
if children:
result = FlowUtils._find_condition_branch_recursive(
children, target_id, current_condition
)
if result is not None:
return result
return None
@staticmethod
def find_innermost_branch_for_node(
flow_def: Dict, node_id: str
) -> Optional[Tuple[str, Dict, Optional[Dict]]]:
"""
查找节点所在的最内层分支归属(可能是条件分支或并行分支)。
用于 _advance_to_next 中正确处理嵌套场景(如并行内嵌条件、条件内嵌并行)。
Returns:
('condition', condition_node, None) 或
('parallel', parallel_node, branch) 或
None
"""
nodes = flow_def.get('nodes')
if not nodes:
return None
return FlowUtils._find_innermost_branch_recursive(nodes, node_id, None)
@staticmethod
def _find_innermost_branch_recursive(
node: Dict,
target_id: str,
innermost: Optional[Tuple[str, Dict, Optional[Dict]]],
) -> Optional[Tuple[str, Dict, Optional[Dict]]]:
if not node or not isinstance(node, dict):
return None
if node.get('id') == target_id:
return innermost
node_type = node.get('type')
if node_type == 'condition':
for branch in node.get('branches', []):
branch_children = branch.get('children')
if branch_children:
result = FlowUtils._find_innermost_branch_recursive(
branch_children, target_id, ('condition', node, None)
)
if result is not None:
return result
if node_type == 'parallel':
for branch in node.get('branches', []):
branch_children = branch.get('children')
if branch_children:
result = FlowUtils._find_innermost_branch_recursive(
branch_children, target_id, ('parallel', node, branch)
)
if result is not None:
return result
children = node.get('children')
if children:
result = FlowUtils._find_innermost_branch_recursive(
children, target_id, innermost
)
if result is not None:
return result
return None
@staticmethod
def find_parent_node_id(flow_definition: Dict, target_node_id: str) -> Optional[str]:
"""
从流程定义中查找目标节点的父节点ID(仅审批/办理节点)
"""
nodes = flow_definition.get('nodes', {})
def find_parent(node: Dict, parent_id: Optional[str] = None) -> Optional[str]:
if not node or not isinstance(node, dict):
return None
node_id = node.get('id')
node_type = node.get('type')
# 检查子节点
children = node.get('children')
if children:
if isinstance(children, dict):
if children.get('id') == target_node_id:
if node_type in ('approval', 'handle'):
return node_id
return parent_id
result = find_parent(children, node_id if node_type in ('approval', 'handle') else parent_id)
if result:
return result
# 检查分支
branches = node.get('branches', [])
for branch in branches:
branch_children = branch.get('children')
if branch_children:
if isinstance(branch_children, dict) and branch_children.get('id') == target_node_id:
if node_type in ('approval', 'handle'):
return node_id
return parent_id
result = find_parent(branch_children, node_id if node_type in ('approval', 'handle') else parent_id)
if result:
return result
return None
return find_parent(nodes)
class FormDataUtils:
"""表单数据工具类"""
@staticmethod
async def save_form_data(db, form_code: str, form_data: Dict) -> str:
"""
保存表单数据
Args:
db: 数据库会话
form_code: 表单编码
form_data: 表单数据(可能是扁平结构或 {main, sub_tables} 结构)
Returns:
str: 数据ID
"""
from online_dev.form_data_manager.service import FormDataService
# 创建表单数据服务
service = await FormDataService.create_service(db, form_code)
# 判断 form_data 是否已经是结构化数据
if "main" in form_data:
# 已经是 {main, sub_tables} 结构,直接使用
data = form_data
else:
# 扁平结构,需要包装
data = {"main": form_data, "sub_tables": {}}
# 保存数据
try:
result = await service.create(db, data)
return result.get("id", "")
except Exception as e:
logger.error(f"保存表单数据失败: {e}")
await db.rollback()
raise ValueError(f"保存表单数据失败: {e}")
@staticmethod
async def load_form_data(db, form_code: str, form_data_id: str) -> Dict:
"""
加载表单数据
Args:
db: 数据库会话
form_code: 表单编码
form_data_id: 表单数据ID
Returns:
Dict: 表单数据(扁平结构,包含关联字段的 _name 后缀)
"""
if not form_code or not form_data_id:
return {}
try:
from online_dev.form_data_manager.service import FormDataService
# 创建表单数据服务
service = await FormDataService.create_service(db, form_code)
# 获取数据
result = await service.get(db, form_data_id)
# 提取主表数据,转为扁平结构
form_data = {}
if result:
# 主表字段
for key, value in result.items():
if key not in ('sub_tables', 'id'):
form_data[key] = value
# 填充关联字段的显示名称(_name 后缀)
if form_data:
# 从 form_config 中提取所有关联字段
relation_fields = {}
form_config = service._form_config
def extract_relation_fields(items):
"""递归提取关联字段"""
if not items:
return
for item in items:
item_type = item.get("type", "")
field = item.get("field", "")
# 容器类型,递归处理
if item_type == "grid":
for col in item.get("columns", []):
extract_relation_fields(col.get("children", []))
elif item_type in ["collapse", "tabs"]:
for panel in item.get("children", []) or item.get("items", []):
extract_relation_fields(panel.get("children", []))
elif item_type == "sub-table":
extract_relation_fields(item.get("children", []))
elif field and item_type in ["user-selector", "dept-selector", "post-selector", "role-selector", "region-selector"]:
# 关联字段类型映射
type_mapping = {
"user-selector": "core_user",
"dept-selector": "core_dept",
"post-selector": "core_post",
"role-selector": "core_role",
"region-selector": "core_region",
}
relation_table = type_mapping.get(item_type)
if relation_table:
relation_fields[field] = {
"display_field": f"{field}_name",
"relation_table": relation_table,
"relation_key": "id",
"display_column": "name",
}
extract_relation_fields(form_config.get("items", []))
if relation_fields:
filled = await service._fill_relation_display_names(db, [form_data], relation_fields)
if filled:
form_data = filled[0]
return form_data
except Exception as e:
logger.error(f"加载表单数据失败: {e}")
return {}
@staticmethod
async def update_form_data(db, form_code: str, form_data_id: str, form_data: Dict) -> None:
"""更新表单数据"""
if not form_code or not form_data_id:
return
try:
from online_dev.form_data_manager.service import FormDataService
# 创建表单数据服务
service = await FormDataService.create_service(db, form_code)
# 构建数据结构
data = {"main": form_data, "sub_tables": {}}
# 更新数据
await service.update(db, form_data_id, data)
except Exception as e:
logger.error(f"更新表单数据失败: {e}")
@@ -0,0 +1,644 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
工作流引擎(异步版本)
负责流程的执行、推进、状态管理等核心逻辑
"""
import logging
from datetime import datetime
from typing import Dict, Optional, Any
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from online_dev.workflow.engine.assignee_resolver import assignee_resolver
from online_dev.workflow.engine.base import (
ExecutionContext,
TaskAction,
generate_instance_no,
)
from online_dev.workflow.engine.condition_evaluator import condition_evaluator
from online_dev.workflow.engine.utils import FlowUtils, FormDataUtils
logger = logging.getLogger(__name__)
class WorkflowEngine:
"""
工作流引擎
职责:
1. 流程启动 - 创建实例,执行第一个节点
2. 任务处理 - 审批/拒绝/转交
3. 流程推进 - 根据条件判断下一节点
4. 状态管理 - 更新实例和任务状态
"""
def __init__(self):
self.condition_evaluator = condition_evaluator
self.assignee_resolver = assignee_resolver
self._handlers = {}
def _get_handler(self, node_type: str):
"""获取节点处理器(懒加载)"""
if node_type not in self._handlers:
from online_dev.workflow.engine.handlers import (
ApprovalHandler,
HandleHandler,
CopyHandler,
ConditionHandler,
ParallelHandler,
DelayNodeHandler,
NotifyHandler,
ServiceHandler,
SubflowHandler,
DataUpdateHandler,
)
handler_map = {
'approval': ApprovalHandler,
'handle': HandleHandler,
'copy': CopyHandler,
'condition': ConditionHandler,
'parallel': ParallelHandler,
'delay': DelayNodeHandler,
'notify': NotifyHandler,
'service': ServiceHandler,
'subflow': SubflowHandler,
'data_update': DataUpdateHandler,
}
handler_class = handler_map.get(node_type)
if handler_class:
self._handlers[node_type] = handler_class(self)
return self._handlers.get(node_type)
# ==================== 流程启动 ====================
async def start(
self,
db: AsyncSession,
workflow: Any,
title: str,
form_data: Dict,
initiator_id: str,
) -> Any:
"""
启动流程
Args:
db: 数据库会话
workflow: 流程定义
title: 流程标题
form_data: 表单数据
initiator_id: 发起人ID
Returns:
WorkflowInstance: 流程实例
"""
from online_dev.workflow.model import WorkflowInstance, WorkflowLog
# 缓存 workflow 属性,避免 save_form_data 中 db.commit() 导致 session 对象过期
_workflow_id = str(workflow.id)
_form_code = workflow.form_code
_flow_definition = workflow.flow_definition
# 1. 保存表单数据
form_data_id = await FormDataUtils.save_form_data(db, _form_code, form_data)
# 2. 重新加载展平后的表单数据用于条件评估等引擎逻辑
# 前端传入的 form_data 可能是 {main: {...}, sub_tables: {...}} 嵌套结构,
# 而引擎的条件评估器需要展平的 {field: value} 结构
flat_form_data = await FormDataUtils.load_form_data(db, _form_code, form_data_id)
# 3. 创建流程实例
instance = WorkflowInstance(
workflow_id=_workflow_id,
instance_no=generate_instance_no(),
title=title,
status='pending',
initiator_id=initiator_id,
form_code=_form_code,
form_data_id=form_data_id,
current_node_id='start',
current_node_name='开始',
)
db.add(instance)
await db.flush()
# 记录启动日志
log = WorkflowLog(
instance_id=str(instance.id),
node_id='start',
node_name='开始',
action='start',
operator_id=initiator_id,
comment='发起流程',
)
db.add(log)
await db.flush()
# 创建执行上下文
context = ExecutionContext(
instance=instance,
form_data=flat_form_data or form_data,
current_user_id=initiator_id,
flow_definition=_flow_definition,
db=db,
)
# 执行开始节点,推进到下一节点
start_node = context.flow_definition.get('nodes')
if start_node and start_node.get('type') == 'start':
await self._advance_to_next(context, start_node)
else:
logger.error("找不到开始节点!")
await db.refresh(instance)
return instance
async def restart_instance(self, db: AsyncSession, instance: Any, user_id: str) -> Any:
"""
重新启动流程实例(驳回修改后重新提交)
"""
from online_dev.workflow.model import WorkflowDefinition
stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id)
result = await db.execute(stmt)
workflow = result.scalar_one_or_none()
if not workflow:
raise ValueError("流程定义不存在")
flow_definition = workflow.flow_definition
if not flow_definition:
raise ValueError("流程定义为空")
# 加载表单数据
form_data = {}
if instance.form_code and instance.form_data_id:
try:
form_data = await FormDataUtils.load_form_data(db, instance.form_code, instance.form_data_id)
except Exception as e:
logger.warning(f"restart_instance: 加载表单数据失败: {e}")
context = ExecutionContext(
instance=instance,
form_data=form_data,
flow_definition=flow_definition,
current_user_id=user_id,
db=db,
)
start_node = flow_definition.get('nodes', {})
if start_node and start_node.get('type') == 'start':
instance.status = 'pending'
db.add(instance)
await db.flush()
await self._advance_to_next(context, start_node)
await db.refresh(instance)
return instance
# ==================== 任务处理 ====================
async def complete_task(
self,
db: AsyncSession,
task: Any,
action: TaskAction,
comment: str,
user_id: str,
form_data: Optional[Dict] = None,
return_to: str = None,
) -> Any:
"""
完成任务
Args:
db: 数据库会话
task: 任务
action: 操作(approve/reject/return
comment: 审批意见
user_id: 操作用户ID
form_data: 表单数据(可能被修改)
return_to: 驳回目标(当 action=RETURN 时使用)
Returns:
WorkflowTask: 更新后的任务
"""
from online_dev.workflow.model import WorkflowLog, WorkflowInstance, WorkflowDefinition
# 获取实例
stmt = select(WorkflowInstance).where(WorkflowInstance.id == task.instance_id)
result = await db.execute(stmt)
instance = result.scalar_one_or_none()
if not instance:
raise ValueError("流程实例不存在")
# 获取流程定义
stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id)
result = await db.execute(stmt)
workflow = result.scalar_one_or_none()
if not workflow:
raise ValueError("流程定义不存在")
# 更新任务状态
status_map = {
TaskAction.APPROVE: 'approved',
TaskAction.REJECT: 'rejected',
TaskAction.RETURN: 'returned',
TaskAction.DELEGATE: 'delegated',
}
task.status = status_map.get(action, action.value + 'd')
task.comment = comment
task.handled_at = datetime.now()
db.add(task)
await db.flush()
# 记录日志
log_extra_data = {}
# 如果有签名,记录到日志中
if task.signature_file_id:
log_extra_data['signature_file_id'] = task.signature_file_id
if action == TaskAction.RETURN:
log_extra_data['return_to'] = return_to or 'initiator'
# 解析实际驳回目标节点名称
if return_to == 'initiator' or not return_to:
log_extra_data['return_to_name'] = '发起人'
elif return_to == 'previous':
prev_node_id = await self._find_previous_node_id(
ExecutionContext(
instance=instance,
form_data={},
current_user_id=user_id,
flow_definition=workflow.flow_definition,
db=db,
),
task.node_id,
)
if prev_node_id:
prev_node = FlowUtils.find_node_by_id(workflow.flow_definition, prev_node_id)
log_extra_data['return_to_name'] = prev_node.get('name', prev_node_id) if prev_node else prev_node_id
else:
log_extra_data['return_to_name'] = '发起人'
else:
target_node = FlowUtils.find_node_by_id(workflow.flow_definition, return_to)
log_extra_data['return_to_name'] = target_node.get('name', return_to) if target_node else return_to
log = WorkflowLog(
instance_id=str(instance.id),
node_id=task.node_id,
node_name=task.node_name,
action=action.value,
operator_id=user_id,
comment=comment,
extra_data=log_extra_data,
)
db.add(log)
await db.flush()
# 获取表单数据
if form_data is None:
form_data = await FormDataUtils.load_form_data(db, instance.form_code, instance.form_data_id)
# 创建执行上下文
context = ExecutionContext(
instance=instance,
form_data=form_data,
current_user_id=user_id,
flow_definition=workflow.flow_definition,
db=db,
)
# 获取当前节点配置
current_node = FlowUtils.find_node_by_id(context.flow_definition, task.node_id)
# 完成该任务对应的钉钉待办
if action in (TaskAction.APPROVE, TaskAction.REJECT, TaskAction.RETURN):
try:
from core.message.service import NotifyService
await NotifyService.complete_dingtalk_todo(db, "workflow_task", str(task.id))
except Exception as e:
logger.warning(f"完成钉钉待办失败: {e}")
# 根据操作推进流程
if action == TaskAction.REJECT:
# 发送发起人通知(拒绝)
try:
from online_dev.workflow.engine.handlers.notification_service import WorkflowNotificationService
await WorkflowNotificationService.send_initiator_notification(context, current_node or {}, 'reject')
except Exception as e:
logger.error(f"发送发起人通知失败: {e}")
await self._end_instance(context, 'rejected')
elif action == TaskAction.APPROVE:
# 发送发起人通知(通过)
try:
from online_dev.workflow.engine.handlers.notification_service import WorkflowNotificationService
await WorkflowNotificationService.send_initiator_notification(context, current_node or {}, 'approve')
except Exception as e:
logger.error(f"发送发起人通知失败: {e}")
await self._handle_approval(context, task)
elif action == TaskAction.RETURN:
await self._handle_return(context, task, return_to)
return task
async def transfer_task(
self,
db: AsyncSession,
task: Any,
to_user: Any,
comment: str,
user_id: str,
) -> Any:
"""转交任务"""
from online_dev.workflow.model import WorkflowTask, WorkflowLog
task.status = 'transferred'
task.comment = comment
task.handled_at = datetime.now()
task.transferred_to_id = str(to_user.id)
db.add(task)
await db.flush()
# 完成原任务对应的钉钉待办
try:
from core.message.service import NotifyService
await NotifyService.complete_dingtalk_todo(db, "workflow_task", str(task.id))
except Exception as e:
logger.warning(f"转交-清理钉钉待办失败 task={task.id}: {e}")
new_task = WorkflowTask(
instance_id=task.instance_id,
node_id=task.node_id,
node_name=task.node_name,
task_type=task.task_type,
status='pending',
assignee_id=str(to_user.id),
parent_task_id=str(task.id),
sign_type='transfer',
timeout_at=task.timeout_at,
timeout_action=task.timeout_action or '',
timeout_notified=False,
)
db.add(new_task)
await db.flush()
try:
from online_dev.workflow.engine.handlers.notification_service import (
WorkflowNotificationService,
)
instance, flow_definition = await WorkflowNotificationService.load_instance_and_flow(
db, new_task
)
if instance:
await WorkflowNotificationService.notify_pending_task(
db=db,
task=new_task,
instance=instance,
flow_definition=flow_definition,
operator_id=user_id,
)
except Exception as e:
logger.warning(f'转办后发送任务通知失败 task={new_task.id}: {e}')
log = WorkflowLog(
instance_id=task.instance_id,
node_id=task.node_id,
node_name=task.node_name,
action='transfer',
operator_id=user_id,
comment=f"转交给 {to_user.name or to_user.username}: {comment}",
extra_data={'to_user_id': str(to_user.id)},
)
db.add(log)
await db.flush()
return new_task
# ==================== 流程推进 ====================
async def _handle_approval(self, context: ExecutionContext, task: Any) -> None:
"""处理审批通过后的流程推进"""
handler = self._get_handler('approval')
if handler:
await handler.handle_approval(context, task)
async def _handle_handle_completion(self, context: ExecutionContext, task: Any) -> None:
"""处理办理任务完成后的流程推进"""
handler = self._get_handler('handle')
if handler:
await handler.handle_completion(context, task)
async def _handle_return(self, context: ExecutionContext, task: Any, return_to: str = None) -> None:
"""处理驳回操作"""
from online_dev.workflow.model import WorkflowTask
# 先收集待取消任务的ID,用于清理钉钉待办
pending_stmt = select(WorkflowTask.id).where(
WorkflowTask.instance_id == str(context.instance.id),
WorkflowTask.node_id == task.node_id,
WorkflowTask.status == 'pending',
)
pending_result = await context.db.execute(pending_stmt)
canceled_task_ids = [str(row[0]) for row in pending_result.all()]
# 取消当前节点的所有待处理任务
stmt = update(WorkflowTask).where(
WorkflowTask.instance_id == str(context.instance.id),
WorkflowTask.node_id == task.node_id,
WorkflowTask.status == 'pending',
).values(status='canceled')
await context.db.execute(stmt)
# 清理被取消任务的钉钉待办
for tid in canceled_task_ids:
try:
from core.message.service import NotifyService
await NotifyService.complete_dingtalk_todo(context.db, "workflow_task", tid)
except Exception as e:
logger.warning(f"驳回-清理钉钉待办失败 task={tid}: {e}")
if return_to == 'initiator' or not return_to:
await self._return_to_initiator(context, task)
elif return_to == 'previous':
previous_node_id = await self._find_previous_node_id(context, task.node_id)
if previous_node_id:
await self._return_to_node(context, task, previous_node_id)
else:
await self._return_to_initiator(context, task)
else:
await self._return_to_node(context, task, return_to)
async def _return_to_initiator(self, context: ExecutionContext, task: Any) -> None:
"""驳回给发起人"""
from online_dev.workflow.model import WorkflowTask
instance = context.instance
instance.current_node_id = 'start'
instance.current_node_name = '待修改'
context.db.add(instance)
await context.db.flush()
# 创建发起人的修改任务
revise_task = WorkflowTask(
instance_id=str(instance.id),
node_id='start',
node_name='待修改',
task_type='revise',
status='pending',
assignee_id=instance.initiator_id,
)
context.db.add(revise_task)
await context.db.flush()
async def _return_to_node(self, context: ExecutionContext, task: Any, node_id: str) -> None:
"""驳回到指定节点"""
target_node = FlowUtils.find_node_by_id(context.flow_definition, node_id)
if not target_node:
logger.warning(f"找不到驳回目标节点: {node_id}")
await self._return_to_initiator(context, task)
return
context.instance.current_node_id = node_id
context.instance.current_node_name = target_node.get('name', '')
context.db.add(context.instance)
await context.db.flush()
node_type = target_node.get('type')
handler = self._get_handler(node_type)
if handler:
await handler.execute(context, target_node)
async def _find_previous_node_id(self, context: ExecutionContext, current_node_id: str) -> Optional[str]:
"""查找上一个节点的ID"""
from online_dev.workflow.model import WorkflowLog
stmt = select(WorkflowLog).where(
WorkflowLog.instance_id == str(context.instance.id),
WorkflowLog.action.in_(['approve', 'return']),
WorkflowLog.node_id != current_node_id,
).order_by(WorkflowLog.sys_create_datetime.desc()).limit(1)
result = await context.db.execute(stmt)
previous_log = result.scalar_one_or_none()
if previous_log and previous_log.node_id:
return previous_log.node_id
return FlowUtils.find_parent_node_id(context.flow_definition, current_node_id)
async def _advance_to_next(self, context: ExecutionContext, current_node: Dict) -> None:
"""推进到下一节点"""
logger.info(f"_advance_to_next - 当前节点: {current_node.get('id')}, {current_node.get('type')}")
next_node = self._get_next_node(context, current_node)
logger.info(f"_advance_to_next - 下一节点: {next_node}")
if not next_node:
current_id = current_node.get('id', '')
# 查找最内层的分支归属(正确处理嵌套:并行内嵌条件、条件内嵌并行等)
branch_info = FlowUtils.find_innermost_branch_for_node(
context.flow_definition, current_id
)
if branch_info:
branch_type, branch_node, branch = branch_info
if branch_type == 'parallel' and branch is not None:
handler = self._get_handler('parallel')
if handler:
await handler.mark_branch_complete(context, branch_node, branch)
return
elif branch_type == 'condition':
logger.info(f"条件分支内节点 {current_id} 完成,从条件节点 {branch_node.get('id')} 继续推进")
await self._advance_to_next(context, branch_node)
return
logger.warning(f"找不到下一节点,当前节点: {current_id}")
return
node_type = next_node.get('type')
if node_type == 'end':
await self._end_instance(context, 'approved')
else:
handler = self._get_handler(node_type)
if handler:
await handler.execute(context, next_node)
else:
logger.warning(f"未知节点类型: {node_type},自动跳过")
await self._advance_to_next(context, next_node)
def _get_next_node(self, context: ExecutionContext, current_node: Dict) -> Optional[Dict]:
"""获取下一节点"""
children = current_node.get('children')
if children:
return children
return None
# ==================== 流程结束 ====================
async def _end_instance(self, context: ExecutionContext, status: str, current_node: Dict = None) -> None:
"""结束流程实例"""
from online_dev.workflow.model import WorkflowTask
instance = context.instance
instance.status = status
instance.completed_at = datetime.now()
instance.current_node_id = 'end'
instance.current_node_name = '结束'
context.db.add(instance)
await context.db.flush()
# 查找即将被取消的待处理任务ID(用于清理钉钉待办)
pending_task_stmt = select(WorkflowTask.id).where(
WorkflowTask.instance_id == str(instance.id),
WorkflowTask.status == 'pending',
WorkflowTask.task_type != 'copy',
)
pending_result = await context.db.execute(pending_task_stmt)
pending_task_ids = [str(row[0]) for row in pending_result.all()]
# 取消所有待处理任务(排除抄送任务,抄送任务保留供用户查阅)
stmt = update(WorkflowTask).where(
WorkflowTask.instance_id == str(instance.id),
WorkflowTask.status == 'pending',
WorkflowTask.task_type != 'copy',
).values(status='canceled')
await context.db.execute(stmt)
# 删除/完成被取消任务对应的钉钉待办
for task_id in pending_task_ids:
try:
from core.message.service import NotifyService
if status in ('approved', 'rejected'):
await NotifyService.complete_dingtalk_todo(context.db, "workflow_task", task_id)
else:
await NotifyService.delete_dingtalk_todo(context.db, "workflow_task", task_id)
except Exception as e:
logger.warning(f"清理钉钉待办失败 task={task_id}: {e}")
# 发送流程完成通知给发起人
try:
from online_dev.workflow.engine.handlers.notification_service import WorkflowNotificationService
await WorkflowNotificationService.send_instance_complete_notification(context, status)
except Exception as e:
logger.error(f"发送流程完成通知失败: {e}")
# 如果是子流程,通知父流程继续
if instance.parent_instance_id:
logger.info(f"子流程结束,通知父流程: {instance.parent_instance_id}")
await self._resume_parent_after_subflow(context.db, instance)
async def _resume_parent_after_subflow(self, db: AsyncSession, sub_instance) -> None:
"""子流程完成后恢复父流程"""
handler = self._get_handler('subflow')
if handler:
await handler.resume_parent(db, sub_instance)
# 全局引擎实例
workflow_engine = WorkflowEngine()