#!/usr/bin/env python # -*- coding: utf-8 -*- """ 流程进度聚合服务 负责聚合流程执行路径、节点状态、处理人信息等 """ import logging from dataclasses import dataclass, field, asdict from typing import Dict, List, Optional from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession logger = logging.getLogger(__name__) @dataclass class HandlerInfo: """处理人信息""" user_id: str user_name: str status: str # pending/approved/rejected/transferred/delegated/waiting action: str = '' # approve/reject/transfer/delegate/add_sign comment: str = '' handled_at: str = '' signature_file_id: str = '' # 签名文件ID def to_dict(self): return asdict(self) @dataclass class ExtraAction: """额外操作(加签、转交等)""" type: str # add_sign/transfer/delegate/return from_user_id: str = '' from_user_name: str = '' to_user_id: str = '' to_user_name: str = '' sign_type: str = '' # before/after/parallel(加签类型) status: str = '' time: str = '' comment: str = '' # 操作说明/驳回原因 def to_dict(self): return asdict(self) @dataclass class ProgressNode: """进度节点""" id: str name: str type: str # start/approval/handle/copy/condition/parallel/delay/notify/service/subflow/end status: str # completed/active/pending/skipped/rejected completed_at: str = '' handlers: List[HandlerInfo] = field(default_factory=list) extra_actions: List[ExtraAction] = field(default_factory=list) # 条件分支信息 condition_result: str = '' # 条件分支选中的分支名称 # 并行分支信息 branches: List[Dict] = field(default_factory=list) # 延时节点信息 delay_until: str = '' # 延时到期时间 def to_dict(self): result = { 'id': self.id, 'name': self.name, 'type': self.type, 'status': self.status, 'completed_at': self.completed_at, 'handlers': [h.to_dict() for h in self.handlers], 'extra_actions': [a.to_dict() for a in self.extra_actions], } if self.condition_result: result['condition_result'] = self.condition_result if self.branches: result['branches'] = self.branches if self.delay_until: result['delay_until'] = self.delay_until return result @dataclass class ReturnRecord: """驳回记录""" from_node_id: str from_node_name: str to_node_id: str to_node_name: str operator_name: str reason: str time: str def to_dict(self): return asdict(self) @dataclass class FlowProgress: """流程进度""" instance_id: str instance_status: str current_node_id: str nodes: List[ProgressNode] = field(default_factory=list) returns: List[ReturnRecord] = field(default_factory=list) def to_dict(self): return { 'instance_id': self.instance_id, 'instance_status': self.instance_status, 'current_node_id': self.current_node_id, 'nodes': [n.to_dict() for n in self.nodes], 'returns': [r.to_dict() for r in self.returns], } class FlowProgressService: """流程进度服务""" @staticmethod async def get_progress(db: AsyncSession, instance_id: str) -> Optional[Dict]: """ 获取流程实例的执行进度 Args: db: 数据库会话 instance_id: 流程实例ID Returns: FlowProgress 字典 """ from online_dev.workflow.model import WorkflowInstance, WorkflowDefinition, WorkflowTask, WorkflowLog from core.user.model import User # 获取实例 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: return None # 获取流程定义 stmt = select(WorkflowDefinition).where(WorkflowDefinition.id == instance.workflow_id) result = await db.execute(stmt) workflow = result.scalar_one_or_none() if not workflow or not workflow.flow_definition: return None flow_definition = workflow.flow_definition # 获取所有日志 stmt = select(WorkflowLog).where( WorkflowLog.instance_id == instance_id, WorkflowLog.is_deleted == False, ).order_by(WorkflowLog.sys_create_datetime) result = await db.execute(stmt) logs = list(result.scalars().all()) # 获取所有任务 stmt = select(WorkflowTask).where( WorkflowTask.instance_id == instance_id, WorkflowTask.is_deleted == False, ).order_by(WorkflowTask.sys_create_datetime) result = await db.execute(stmt) tasks = list(result.scalars().all()) # 获取用户信息 user_ids = set() if instance.initiator_id: user_ids.add(instance.initiator_id) for log in logs: if log.operator_id: user_ids.add(log.operator_id) for task in tasks: if task.assignee_id: user_ids.add(task.assignee_id) if task.transferred_to_id: user_ids.add(task.transferred_to_id) users_map = {} if user_ids: stmt = select(User).where(User.id.in_(list(user_ids))) result = await db.execute(stmt) for user in result.scalars().all(): users_map[str(user.id)] = user # 加载表单数据(用于解析表单字段类型的审批人) 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}") # 构建进度 progress = FlowProgress( instance_id=str(instance.id), instance_status=instance.status, current_node_id=instance.current_node_id or '', ) # 构建日志索引(按节点ID分组) logs_by_node = {} for log in logs: node_id = log.node_id or 'start' if node_id not in logs_by_node: logs_by_node[node_id] = [] logs_by_node[node_id].append(log) # 构建任务索引(按节点ID分组) tasks_by_node = {} for task in tasks: node_id = task.node_id if node_id not in tasks_by_node: tasks_by_node[node_id] = [] tasks_by_node[node_id].append(task) # 确定每个节点的状态 executed_nodes = set() for log in logs: if log.node_id: executed_nodes.add(log.node_id) # 有任务的节点也算已触达 touched_nodes = executed_nodes | set(tasks_by_node.keys()) # 解析流程定义,提取节点列表(根据表单数据评估条件分支,展示完整审批路径) nodes_list = FlowProgressService._extract_nodes(flow_definition, form_data) # 提取驳回记录 progress.returns = FlowProgressService._extract_returns(logs, users_map, flow_definition) # 处理每个节点 for node_info in nodes_list: node_id = node_info['id'] node_type = node_info['type'] node_name = node_info['name'] node_config = node_info.get('config', {}) # 确定节点状态 status = FlowProgressService._determine_node_status( node_id=node_id, node_type=node_type, instance=instance, executed_nodes=executed_nodes, logs_by_node=logs_by_node, tasks_by_node=tasks_by_node, ) progress_node = ProgressNode( id=node_id, name=node_name, type=node_type, status=status, ) # 开始节点显示发起人 if node_type == 'start': initiator = users_map.get(str(instance.initiator_id)) if instance.initiator_id else None progress_node.handlers = [HandlerInfo( user_id=str(instance.initiator_id) if instance.initiator_id else '', user_name=initiator.name if initiator else '', status='completed', action='start', handled_at=instance.sys_create_datetime.strftime('%Y-%m-%d %H:%M:%S') if instance.sys_create_datetime else '', )] # 获取节点的处理人信息 if node_type in ['approval', 'handle', 'copy']: if status in ['completed', 'active', 'rejected']: progress_node.handlers = FlowProgressService._get_handlers( node_id, tasks_by_node, logs_by_node, users_map, status ) progress_node.extra_actions = FlowProgressService._get_extra_actions( node_id, tasks_by_node, logs_by_node, users_map, flow_definition ) elif status == 'pending': progress_node.handlers = await FlowProgressService._get_pending_handlers( db, node_config, instance, form_data, users_map ) # 获取完成时间 if status == 'completed': progress_node.completed_at = FlowProgressService._get_completed_time( node_id, logs_by_node ) # 延时节点信息 if node_type == 'delay' and status == 'active': if instance.delay_node_id == node_id and instance.delay_until: progress_node.delay_until = instance.delay_until.strftime('%Y-%m-%d %H:%M:%S') # 条件分支信息 if node_type == 'condition' and node_id in executed_nodes: progress_node.condition_result = FlowProgressService._get_condition_result( node_id, node_info, logs_by_node ) progress.nodes.append(progress_node) return progress.to_dict() @staticmethod def _extract_nodes(flow_definition: Dict, form_data: Dict = None) -> List[Dict]: """ 从流程定义中提取线性化的节点列表 对于条件分支: - 根据表单数据评估条件,展示将要执行的分支 - 这样可以在审批前就显示完整的审批路径 """ from online_dev.workflow.engine.condition_evaluator import condition_evaluator nodes = [] visited = set() form = form_data or {} root = flow_definition.get('nodes') if not root: return nodes def _evaluate_branch(branch: Dict) -> bool: """评估分支条件是否满足""" config = branch.get('config', {}) # 默认分支总是作为兜底 if config.get('isDefault'): return False # 默认分支不主动匹配,只在没有其他分支匹配时使用 # 获取条件组(字段名是 groups,与 condition_handler.py 保持一致) groups = config.get('groups', []) if not groups: # 没有条件组,视为不匹配 return False # 评估条件组(组之间是 OR 关系) return condition_evaluator.evaluate_groups(groups, form) def _find_matching_branch(branches: List[Dict]) -> Dict: """找到匹配的分支,如果没有匹配则返回默认分支""" default_branch = None for branch in branches: config = branch.get('config', {}) if config.get('isDefault'): default_branch = branch elif _evaluate_branch(branch): return branch # 没有匹配的分支,返回默认分支 return default_branch def traverse(node: Dict, depth: int = 0, in_branch: bool = False): if not node or not isinstance(node, dict): return node_id = node.get('id', '') node_type = node.get('type', '') node_name = node.get('name', '') if node_id and node_id not in visited: visited.add(node_id) nodes.append({ 'id': node_id, 'type': node_type, 'name': node_name, 'depth': depth, 'config': node.get('config', {}), 'branches': node.get('branches', []), 'in_branch': in_branch, }) if node_type == 'condition': branches = node.get('branches', []) # 根据表单数据评估条件,找到将要执行的分支 matching_branch = _find_matching_branch(branches) if matching_branch: branch_children = matching_branch.get('children') if branch_children: traverse(branch_children, depth, in_branch=in_branch) if node_type == 'parallel': branches = node.get('branches', []) for branch in branches: branch_children = branch.get('children') if branch_children: traverse(branch_children, depth + 1, in_branch=True) children = node.get('children') if children: traverse(children, depth, in_branch=in_branch) traverse(root) return nodes @staticmethod def _determine_node_status( node_id: str, node_type: str, instance, executed_nodes: set, logs_by_node: Dict, tasks_by_node: Dict, ) -> str: """确定节点状态""" if node_type == 'start': return 'completed' if instance.status == 'approved': if node_type == 'end': return 'completed' if node_id in executed_nodes: return 'completed' return 'skipped' if instance.status == 'rejected': node_logs = logs_by_node.get(node_id, []) for log in node_logs: if log.action == 'reject': return 'rejected' if node_id in executed_nodes: return 'completed' return 'skipped' if instance.status == 'canceled': if node_id in executed_nodes: return 'completed' return 'skipped' if node_id == instance.current_node_id: # 检查该节点是否还有未完成的任务 node_tasks = tasks_by_node.get(node_id, []) has_pending = any(t.status in ['pending', 'waiting'] for t in node_tasks) if has_pending: return 'active' # 所有任务已完成但 current_node_id 未更新(条件分支推进 bug 的遗留数据) if node_tasks and not has_pending: return 'completed' return 'active' if node_id in executed_nodes: return 'completed' node_tasks = tasks_by_node.get(node_id, []) for task in node_tasks: if task.status in ['pending', 'waiting']: return 'active' return 'pending' @staticmethod def _get_handlers( node_id: str, tasks_by_node: Dict, logs_by_node: Dict, users_map: Dict, node_status: str = '', ) -> List[HandlerInfo]: """获取节点的处理人信息""" handlers = [] tasks = tasks_by_node.get(node_id, []) before_sign_tasks = [] normal_tasks = [] after_sign_tasks = [] delegate_tasks = [] transfer_tasks = [] parallel_sign_tasks = [] for task in tasks: if task.sign_type == 'before': before_sign_tasks.append(task) elif task.sign_type == 'after': after_sign_tasks.append(task) elif task.sign_type == 'delegate': delegate_tasks.append(task) elif task.sign_type == 'transfer': transfer_tasks.append(task) elif task.sign_type == 'parallel': parallel_sign_tasks.append(task) else: normal_tasks.append(task) # 节点已完成时,将未处理的任务标记为已跳过(或签场景) node_completed = node_status == 'completed' def task_to_handler(task, sign_label: str = '') -> HandlerInfo: user = users_map.get(str(task.assignee_id)) if task.assignee_id else None task_status = task.status # 抄送任务显示"已抄送"而非"已跳过" if task.task_type == 'copy': task_status = 'copied' elif task_status == 'canceled' or (node_completed and task_status in ['pending', 'waiting']): task_status = 'skipped' handler = HandlerInfo( user_id=str(task.assignee_id) if task.assignee_id else '', user_name=user.name if user else '', status=task_status, ) if sign_label and task_status == 'pending': handler.user_name = f"[{sign_label}] {handler.user_name}" if task.status == 'approved': handler.action = 'approve' handler.comment = task.comment or '' handler.handled_at = task.handled_at.strftime('%Y-%m-%d %H:%M:%S') if task.handled_at else '' handler.signature_file_id = task.signature_file_id or '' elif task.status == 'rejected': handler.action = 'reject' handler.comment = task.comment or '' handler.handled_at = task.handled_at.strftime('%Y-%m-%d %H:%M:%S') if task.handled_at else '' elif task.status == 'returned': handler.action = 'return' handler.comment = task.comment or '' handler.handled_at = task.handled_at.strftime('%Y-%m-%d %H:%M:%S') if task.handled_at else '' elif task.status == 'transferred': handler.action = 'transfer' handler.comment = task.comment or '' handler.handled_at = task.handled_at.strftime('%Y-%m-%d %H:%M:%S') if task.handled_at else '' elif task.status == 'delegated': handler.action = 'delegate' handler.comment = task.comment or '' handler.handled_at = task.handled_at.strftime('%Y-%m-%d %H:%M:%S') if task.handled_at else '' elif task.status == 'handled': handler.action = 'handle' handler.comment = task.comment or '' handler.handled_at = task.handled_at.strftime('%Y-%m-%d %H:%M:%S') if task.handled_at else '' return handler for task in before_sign_tasks: handlers.append(task_to_handler(task, '前加签')) for task in normal_tasks: handlers.append(task_to_handler(task)) for task in delegate_tasks: handlers.append(task_to_handler(task, '委托')) for task in transfer_tasks: handlers.append(task_to_handler(task, '转交')) for task in after_sign_tasks: handlers.append(task_to_handler(task, '后加签')) for task in parallel_sign_tasks: handlers.append(task_to_handler(task, '并行加签')) return handlers @staticmethod async def _get_pending_handlers( db: AsyncSession, node_config: Dict, instance, form_data: Dict, users_map: Dict, ) -> List[HandlerInfo]: """获取待执行节点的预设处理人(通过 AssigneeResolver 解析具体用户)""" from online_dev.workflow.engine.assignee_resolver import assignee_resolver from core.user.model import User handlers = [] assignee_type = node_config.get('assigneeType', 'user') try: user_ids = await assignee_resolver.resolve(db, node_config, instance, form_data) except Exception as e: logger.warning(f"解析待执行节点审批人失败: {e}") user_ids = [] if user_ids: # 查询不在 users_map 中的用户 missing_ids = [uid for uid in user_ids if uid not in users_map] if missing_ids: stmt = select(User).where(User.id.in_(missing_ids)) result = await db.execute(stmt) for user in result.scalars().all(): users_map[str(user.id)] = user for uid in user_ids: user = users_map.get(uid) handlers.append(HandlerInfo( user_id=uid, user_name=user.name if user else '', status='pending', )) else: # 解析失败时的降级显示 fallback_labels = { 'role': '角色', 'department': '部门', 'superior': '上级主管', 'manager': '直属经理', 'form_field': '表单字段', 'initiator': '发起人', } if assignee_type in fallback_labels: handlers.append(HandlerInfo( user_id='', user_name=f'[{fallback_labels[assignee_type]}]', status='pending', )) return handlers @staticmethod def _get_extra_actions( node_id: str, tasks_by_node: Dict, logs_by_node: Dict, users_map: Dict, flow_definition: Dict = None, ) -> List[ExtraAction]: """获取节点的额外操作""" actions = [] logs = logs_by_node.get(node_id, []) for log in logs: extra_data = log.extra_data or {} time_str = log.sys_create_datetime.strftime('%Y-%m-%d %H:%M:%S') if log.sys_create_datetime else '' operator = users_map.get(str(log.operator_id)) if log.operator_id else None if log.action == 'transfer': action = ExtraAction( type='transfer', from_user_id=str(log.operator_id) if log.operator_id else '', from_user_name=operator.name if operator else '', to_user_id=str(extra_data.get('to_user_id', '')), to_user_name=extra_data.get('to_user_name', ''), time=time_str, comment=log.comment or '', ) actions.append(action) elif log.action == 'delegate': action = ExtraAction( type='delegate', from_user_id=str(log.operator_id) if log.operator_id else '', from_user_name=operator.name if operator else '', to_user_id=str(extra_data.get('to_user_id', '')), to_user_name=extra_data.get('to_user_name', ''), time=time_str, comment=log.comment or '', ) actions.append(action) elif log.action == 'add_sign': action = ExtraAction( type='add_sign', from_user_id=str(log.operator_id) if log.operator_id else '', from_user_name=operator.name if operator else '', to_user_id=str(extra_data.get('to_user_id', '')), to_user_name=extra_data.get('to_user_name', ''), sign_type=extra_data.get('sign_type', ''), time=time_str, comment=log.comment or '', ) actions.append(action) elif log.action == 'return': # 解析驳回目标节点名称 to_name = extra_data.get('return_to_name', '') if not to_name: return_to = extra_data.get('return_to', 'initiator') if return_to == 'initiator' or not return_to: to_name = '发起人' elif flow_definition: from online_dev.workflow.engine.utils import FlowUtils target_node = FlowUtils.find_node_by_id(flow_definition, return_to) to_name = target_node.get('name', return_to) if target_node else return_to else: to_name = return_to action = ExtraAction( type='return', from_user_id=str(log.operator_id) if log.operator_id else '', from_user_name=operator.name if operator else '', to_user_id='', to_user_name=to_name, time=time_str, comment=log.comment or '', ) actions.append(action) return actions @staticmethod def _get_completed_time(node_id: str, logs_by_node: Dict) -> str: """获取节点完成时间""" logs = logs_by_node.get(node_id, []) for log in reversed(logs): if log.action in ['approve', 'handle', 'start', 'parallel_complete', 'delay_complete', 'notify', 'service_call', 'subflow_complete', 'condition', 'copy']: return log.sys_create_datetime.strftime('%Y-%m-%d %H:%M:%S') if log.sys_create_datetime else '' return '' @staticmethod def _get_condition_result(node_id: str, node_info: Dict, logs_by_node: Dict) -> str: """获取条件分支的选择结果""" return '' @staticmethod def _extract_returns(logs: List, users_map: Dict, flow_definition: Dict = None) -> List[ReturnRecord]: """提取驳回记录""" from online_dev.workflow.engine.utils import FlowUtils returns = [] for log in logs: if log.action == 'return': extra_data = log.extra_data or {} operator = users_map.get(str(log.operator_id)) if log.operator_id else None # 解析驳回目标节点名称 to_node_name = extra_data.get('return_to_name', '') if not to_node_name: return_to = extra_data.get('return_to', 'initiator') if return_to == 'initiator' or not return_to: to_node_name = '发起人' elif flow_definition: target_node = FlowUtils.find_node_by_id(flow_definition, return_to) to_node_name = target_node.get('name', return_to) if target_node else return_to else: to_node_name = return_to record = ReturnRecord( from_node_id=log.node_id or '', from_node_name=log.node_name or '', to_node_id=extra_data.get('return_to', ''), to_node_name=to_node_name, operator_name=operator.name if operator else '', reason=log.comment or '', time=log.sys_create_datetime.strftime('%Y-%m-%d %H:%M:%S') if log.sys_create_datetime else '', ) returns.append(record) return returns