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