#!/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