#!/usr/bin/env python # -*- coding: utf-8 -*- """ 工作流服务层 数据权限: - 使用 get_list_with_data_scope() 自动应用数据权限 - 支持本人、本部门、本部门及下级、全部等数据范围 """ from datetime import datetime from typing import Optional, List, Tuple from sqlalchemy import select, update, func, and_ from sqlalchemy.ext.asyncio import AsyncSession from online_dev.workflow.model import ( WorkflowDefinition, WorkflowInstance, WorkflowTask, WorkflowLog, ) from app.data_scope_utils import get_data_scope_filter, apply_data_scope_to_conditions from utils.context import get_current_user_info_from_context # 资源类型(用于数据权限配置) RESOURCE_TYPE = "workflow" RESOURCE_DISPLAY_NAME = "工作流管理" class WorkflowDefinitionService: """流程定义服务""" @staticmethod async def get_list( db: AsyncSession, page: int = 1, page_size: int = 20, application_id: str = None, all_apps: bool = False, name: str = None, code: str = None, workflow_type: str = None, category: str = None, status: str = None, form_code: str = None, ) -> Tuple[List[WorkflowDefinition], int]: """获取流程定义列表""" stmt = select(WorkflowDefinition).where(WorkflowDefinition.is_deleted == False) # 应用过滤 if all_apps: pass # 不过滤,返回所有应用的流程 elif application_id: stmt = stmt.where(WorkflowDefinition.application_id == application_id) else: # 如果没有指定 application_id,只返回主应用的工作流(application_id 为 NULL) stmt = stmt.where(WorkflowDefinition.application_id.is_(None)) if name: stmt = stmt.where(WorkflowDefinition.name.ilike(f"%{name}%")) if code: stmt = stmt.where(WorkflowDefinition.code.ilike(f"%{code}%")) if workflow_type: stmt = stmt.where(WorkflowDefinition.workflow_type == workflow_type) if category: stmt = stmt.where(WorkflowDefinition.category == category) if status: stmt = stmt.where(WorkflowDefinition.status == status) if form_code: stmt = stmt.where(WorkflowDefinition.form_code == form_code) # 计算总数 count_stmt = select(func.count()).select_from(stmt.subquery()) total_result = await db.execute(count_stmt) total = total_result.scalar() or 0 # 分页 stmt = stmt.order_by(WorkflowDefinition.sort, WorkflowDefinition.sys_create_datetime.desc()) stmt = stmt.offset((page - 1) * page_size).limit(page_size) result = await db.execute(stmt) items = list(result.scalars().all()) return items, total @staticmethod async def get_list_with_data_scope( db: AsyncSession, page: int = 1, page_size: int = 20, application_id: str = None, all_apps: bool = False, name: str = None, code: str = None, workflow_type: str = None, category: str = None, status: str = None, form_code: str = None, ) -> Tuple[List[WorkflowDefinition], int]: """ 获取流程定义列表(带数据权限过滤) 自动从上下文获取当前用户信息,应用数据权限过滤 """ conditions = [WorkflowDefinition.is_deleted == False] # 应用过滤 if all_apps: pass # 不过滤,返回所有应用的流程 elif application_id: conditions.append(WorkflowDefinition.application_id == application_id) else: conditions.append(WorkflowDefinition.application_id.is_(None)) if name: conditions.append(WorkflowDefinition.name.ilike(f"%{name}%")) if code: conditions.append(WorkflowDefinition.code.ilike(f"%{code}%")) if workflow_type: conditions.append(WorkflowDefinition.workflow_type == workflow_type) if category: conditions.append(WorkflowDefinition.category == category) if status: conditions.append(WorkflowDefinition.status == status) if form_code: conditions.append(WorkflowDefinition.form_code == form_code) # 获取数据权限过滤条件并应用 data_scope_filter = await get_data_scope_filter(db, RESOURCE_TYPE) scope_conditions = apply_data_scope_to_conditions(WorkflowDefinition, data_scope_filter) conditions.extend(scope_conditions) # 计算总数 stmt = select(WorkflowDefinition).where(and_(*conditions)) count_stmt = select(func.count()).select_from(stmt.subquery()) total_result = await db.execute(count_stmt) total = total_result.scalar() or 0 # 分页 stmt = stmt.order_by(WorkflowDefinition.sort, WorkflowDefinition.sys_create_datetime.desc()) stmt = stmt.offset((page - 1) * page_size).limit(page_size) result = await db.execute(stmt) items = list(result.scalars().all()) return items, total @staticmethod async def get_by_id(db: AsyncSession, workflow_id: str) -> Optional[WorkflowDefinition]: """根据ID获取流程定义""" stmt = select(WorkflowDefinition).where( WorkflowDefinition.id == workflow_id, WorkflowDefinition.is_deleted == False, ) result = await db.execute(stmt) return result.scalar_one_or_none() @staticmethod async def get_by_code(db: AsyncSession, code: str) -> Optional[WorkflowDefinition]: """根据编码获取流程定义""" stmt = select(WorkflowDefinition).where( WorkflowDefinition.code == code, WorkflowDefinition.is_deleted == False, ) result = await db.execute(stmt) return result.scalar_one_or_none() @staticmethod async def create(db: AsyncSession, data: dict, user_id: str) -> WorkflowDefinition: """创建流程定义""" from online_dev.workflow.flow_definition_validator import validate_flow_definition_names flow_def = data.get('flow_definition') if flow_def: validate_flow_definition_names(flow_def) # TODO: 获取表单名称 form_name = "" # 从上下文获取用户信息 user_info = get_current_user_info_from_context() workflow = WorkflowDefinition( application_id=data.get('application_id'), name=data['name'], code=data['code'], workflow_type=data.get('workflow_type', 'other'), icon=data.get('icon', ''), icon_bg_color=data.get('icon_bg_color', ''), category=data.get('category', ''), description=data.get('description', ''), form_code=data['form_code'], form_name=form_name, document_template_codes=data.get('document_template_codes', []), flow_definition=data.get('flow_definition', {}), sort=data.get('sort', 0), ) # 自动填充创建人和部门 workflow.sys_creator_id = user_id or (user_info.get('user_id') if user_info else None) if user_info and user_info.get('dept_id'): workflow.sys_dept_id = user_info.get('dept_id') db.add(workflow) await db.flush() await db.refresh(workflow) return workflow @staticmethod async def update(db: AsyncSession, workflow_id: str, data: dict, user_id: str) -> Optional[WorkflowDefinition]: """更新流程定义""" workflow = await WorkflowDefinitionService.get_by_id(db, workflow_id) if not workflow: return None if 'name' in data and data['name']: workflow.name = data['name'] if 'workflow_type' in data and data['workflow_type']: workflow.workflow_type = data['workflow_type'] if 'icon' in data: workflow.icon = data['icon'] or '' if 'icon_bg_color' in data: workflow.icon_bg_color = data['icon_bg_color'] or '' if 'category' in data: workflow.category = data['category'] or '' if 'description' in data: workflow.description = data['description'] or '' if 'form_code' in data and data['form_code']: workflow.form_code = data['form_code'] # TODO: 更新表单名称 if 'document_template_codes' in data: workflow.document_template_codes = data['document_template_codes'] or [] if 'flow_definition' in data and data['flow_definition']: from online_dev.workflow.flow_definition_validator import validate_flow_definition_names validate_flow_definition_names(data['flow_definition']) workflow.flow_definition = data['flow_definition'] if 'sort' in data: workflow.sort = data['sort'] if data['sort'] is not None else 0 db.add(workflow) await db.flush() await db.refresh(workflow) return workflow @staticmethod async def delete(db: AsyncSession, workflow_id: str) -> bool: """删除流程定义(软删除)""" workflow = await WorkflowDefinitionService.get_by_id(db, workflow_id) if not workflow: return False workflow.is_deleted = True db.add(workflow) await db.flush() return True @staticmethod async def batch_delete(db: AsyncSession, ids: List[str]) -> int: """批量删除""" stmt = update(WorkflowDefinition).where( WorkflowDefinition.id.in_(ids), WorkflowDefinition.is_deleted == False, ).values(is_deleted=True) result = await db.execute(stmt) return result.rowcount @staticmethod async def publish(db: AsyncSession, workflow_id: str, user_id: str) -> Optional[WorkflowDefinition]: """发布流程""" from online_dev.workflow.flow_definition_validator import ( validate_flow_definition_for_publish, validate_flow_definition_names, validate_flow_definition_subflows_for_publish, ) workflow = await WorkflowDefinitionService.get_by_id(db, workflow_id) if not workflow: return None flow_def = workflow.flow_definition if not flow_def: raise ValueError('流程定义为空,无法发布') validate_flow_definition_names(flow_def) validate_flow_definition_for_publish(flow_def) await validate_flow_definition_subflows_for_publish(db, flow_def) workflow.status = 'published' workflow.version += 1 db.add(workflow) await db.flush() await db.refresh(workflow) return workflow @staticmethod async def disable(db: AsyncSession, workflow_id: str, user_id: str) -> Optional[WorkflowDefinition]: """停用流程""" workflow = await WorkflowDefinitionService.get_by_id(db, workflow_id) if not workflow: return None workflow.status = 'disabled' db.add(workflow) await db.flush() await db.refresh(workflow) return workflow @staticmethod async def copy(db: AsyncSession, workflow_id: str, new_code: str, new_name: str, user_id: str) -> Optional[WorkflowDefinition]: """复制流程""" workflow = await WorkflowDefinitionService.get_by_id(db, workflow_id) if not workflow: return None new_workflow = WorkflowDefinition( application_id=workflow.application_id, name=new_name or f"{workflow.name}_副本", code=new_code, category=workflow.category, description=workflow.description, form_code=workflow.form_code, form_name=workflow.form_name, flow_definition=workflow.flow_definition, status='draft', ) db.add(new_workflow) await db.flush() await db.refresh(new_workflow) return new_workflow @staticmethod def _definition_to_export_dict(workflow: WorkflowDefinition) -> dict: """将流程定义转为可导出的 JSON 结构""" return { "name": workflow.name, "code": workflow.code, "workflow_type": workflow.workflow_type or "other", "icon": workflow.icon or "", "icon_bg_color": workflow.icon_bg_color or "", "category": workflow.category or "", "description": workflow.description or "", "form_code": workflow.form_code, "document_template_codes": workflow.document_template_codes or [], "flow_definition": workflow.flow_definition or {}, "sort": workflow.sort or 0, } @classmethod async def export_config(cls, db: AsyncSession, workflow_id: str) -> Optional[dict]: """导出流程配置""" workflow = await cls.get_by_id(db, workflow_id) if not workflow: return None return cls._definition_to_export_dict(workflow) @classmethod async def check_import(cls, db: AsyncSession, code: str) -> dict: """导入预检查:编码是否冲突""" existing = await cls.get_by_code(db, code) if code else None return { "code_exists": existing is not None, "can_import": existing is None, } @classmethod async def _resolve_form_name(cls, db: AsyncSession, form_code: str) -> str: """根据表单编码解析表单名称""" if not form_code: return "" from online_dev.form_manager.model import FormMeta stmt = select(FormMeta.name).where( FormMeta.code == form_code, FormMeta.is_deleted == False, ) result = await db.execute(stmt) return result.scalar_one_or_none() or "" @classmethod async def import_config( cls, db: AsyncSession, data: dict, user_id: str, ) -> WorkflowDefinition: """导入流程配置(创建新草稿流程)""" if not data.get("name") or not data.get("code"): raise ValueError("缺少必要字段: name 或 code") if not data.get("form_code"): raise ValueError("缺少必要字段: form_code") existing = await cls.get_by_code(db, data["code"]) if existing: raise ValueError(f"流程编码已存在: {data['code']}") data = dict(data) data["form_name"] = await cls._resolve_form_name(db, data["form_code"]) return await cls.create(db, data, user_id) @staticmethod async def get_categories(db: AsyncSession) -> List[str]: """获取所有分类""" stmt = select(WorkflowDefinition.category).where( WorkflowDefinition.is_deleted == False, WorkflowDefinition.category != '', ).distinct() result = await db.execute(stmt) return [row[0] for row in result.all()] class WorkflowInstanceService: """流程实例服务""" @staticmethod async def start( db: AsyncSession, workflow_code: str, title: str, form_data: dict, user_id: str, ) -> WorkflowInstance: """发起流程""" from online_dev.workflow.engine import WorkflowEngine # 获取流程定义 workflow = await WorkflowDefinitionService.get_by_code(db, workflow_code) if not workflow: raise ValueError(f"流程 {workflow_code} 不存在") if workflow.status != 'published': raise ValueError(f"流程 {workflow_code} 未发布") # 使用流程引擎启动流程 engine = WorkflowEngine() instance = await engine.start( db=db, workflow=workflow, title=title, form_data=form_data, initiator_id=user_id, ) return instance @staticmethod async def get_list( db: AsyncSession, page: int = 1, page_size: int = 20, status: str = None, initiator_id: str = None, workflow_id: str = None, title: str = None, workflow_name: str = None, instance_no: str = None, initiator_name: str = None, application_id: str = None, ) -> Tuple[List[WorkflowInstance], int]: """获取流程实例列表""" from core.user.model import User joined_workflow = False joined_user = False stmt = select(WorkflowInstance).where(WorkflowInstance.is_deleted == False) if status: stmt = stmt.where(WorkflowInstance.status == status) if initiator_id: stmt = stmt.where(WorkflowInstance.initiator_id == initiator_id) if workflow_id: stmt = stmt.where(WorkflowInstance.workflow_id == workflow_id) if title: stmt = stmt.where(WorkflowInstance.title.ilike(f"%{title}%")) if instance_no: stmt = stmt.where(WorkflowInstance.instance_no.ilike(f"%{instance_no}%")) if workflow_name or application_id: stmt = stmt.join(WorkflowDefinition, WorkflowInstance.workflow_id == WorkflowDefinition.id) joined_workflow = True if workflow_name: stmt = stmt.where(WorkflowDefinition.name.ilike(f"%{workflow_name}%")) if application_id: stmt = stmt.where(WorkflowDefinition.application_id == application_id) if initiator_name: if not joined_user: stmt = stmt.join(User, WorkflowInstance.initiator_id == User.id) joined_user = True stmt = stmt.where(User.name.ilike(f"%{initiator_name}%")) # 计算总数 count_stmt = select(func.count()).select_from(stmt.subquery()) total_result = await db.execute(count_stmt) total = total_result.scalar() or 0 # 分页 stmt = stmt.order_by(WorkflowInstance.started_at.desc()) stmt = stmt.offset((page - 1) * page_size).limit(page_size) result = await db.execute(stmt) items = list(result.scalars().all()) return items, total @staticmethod async def get_by_id(db: AsyncSession, instance_id: str) -> Optional[WorkflowInstance]: """根据ID获取流程实例""" stmt = select(WorkflowInstance).where( WorkflowInstance.id == instance_id, WorkflowInstance.is_deleted == False, ) result = await db.execute(stmt) return result.scalar_one_or_none() @staticmethod async def cancel(db: AsyncSession, instance_id: str, user_id: str) -> Optional[WorkflowInstance]: """撤回流程""" instance = await WorkflowInstanceService.get_by_id(db, instance_id) if not instance: return None if instance.initiator_id != user_id: raise ValueError("只能撤回自己发起的流程") if instance.status != 'pending': raise ValueError("只能撤回审批中的流程") # 如果实例处于延时等待状态,清理延时定时任务 if instance.delay_node_id: try: from online_dev.workflow.engine.delay_callback import remove_delay_job await remove_delay_job(str(instance.id), instance.delay_node_id) except Exception as e: import logging logging.getLogger(__name__).warning(f"清理延时任务失败: {e}") instance.delay_node_id = "" instance.delay_until = None instance.status = 'canceled' instance.completed_at = datetime.now() db.add(instance) # 先收集待取消任务的ID,用于清理钉钉待办 pending_stmt = select(WorkflowTask.id).where( WorkflowTask.instance_id == str(instance.id), WorkflowTask.status == 'pending', ) pending_result = await db.execute(pending_stmt) canceled_task_ids = [str(row[0]) for row in pending_result.all()] # 取消所有待处理任务 stmt = update(WorkflowTask).where( WorkflowTask.instance_id == str(instance.id), WorkflowTask.status == 'pending', ).values(status='canceled') await db.execute(stmt) # 清理被取消任务的钉钉待办 for tid in canceled_task_ids: try: from core.message.service import NotifyService await NotifyService.delete_dingtalk_todo(db, "workflow_task", tid) except Exception as e: import logging logging.getLogger(__name__).warning(f"撤回-清理钉钉待办失败 task={tid}: {e}") # 记录日志 log = WorkflowLog( instance_id=str(instance.id), action='cancel', operator_id=user_id, comment='撤回流程', ) db.add(log) await db.flush() await db.refresh(instance) return instance @staticmethod async def urge(db: AsyncSession, instance_id: str, user_id: str) -> None: """催办流程""" instance = await WorkflowInstanceService.get_by_id(db, instance_id) if not instance: raise ValueError("流程实例不存在") if instance.initiator_id != user_id: raise ValueError("只能催办自己发起的流程") if instance.status != 'pending': raise ValueError("只能催办审批中的流程") # 获取当前待处理的任务(审批 + 办理) stmt = select(WorkflowTask).where( WorkflowTask.instance_id == str(instance.id), WorkflowTask.status == 'pending', WorkflowTask.task_type.in_(('approval', 'handle')), WorkflowTask.is_deleted == False, ) result = await db.execute(stmt) pending_tasks = result.scalars().all() if not pending_tasks: raise ValueError('当前没有待处理的任务') from online_dev.workflow.model import WorkflowDefinition from online_dev.workflow.engine.handlers.notification_service import ( WorkflowNotificationService, ) stmt = select(WorkflowDefinition).where( WorkflowDefinition.id == instance.workflow_id, WorkflowDefinition.is_deleted == False, ) wf_result = await db.execute(stmt) workflow = wf_result.scalar_one_or_none() flow_definition = workflow.flow_definition if workflow else {} sent_count = await WorkflowNotificationService.send_urge_notifications( db=db, instance=instance, pending_tasks=pending_tasks, flow_definition=flow_definition, operator_id=user_id, ) if sent_count == 0: raise ValueError('当前节点未开启任务通知,无法发送催办') # 记录催办日志 log = WorkflowLog( instance_id=str(instance.id), node_id=instance.current_node_id, node_name=instance.current_node_name, action='urge', operator_id=user_id, comment=f'已向 {sent_count} 位处理人发送催办通知', extra_data={'notified_count': sent_count}, ) db.add(log) await db.flush() class WorkflowTaskService: """任务服务""" @staticmethod async def get_pending_tasks( db: AsyncSession, user_id: str, page: int = 1, page_size: int = 20, task_type: str = None, instance_title: str = None, workflow_name: str = None, ) -> Tuple[List[WorkflowTask], int]: """获取待处理任务""" stmt = select(WorkflowTask).where( WorkflowTask.assignee_id == user_id, WorkflowTask.status == 'pending', WorkflowTask.is_deleted == False, ) if task_type: stmt = stmt.where(WorkflowTask.task_type == task_type) if instance_title: stmt = stmt.join(WorkflowInstance, WorkflowTask.instance_id == WorkflowInstance.id) stmt = stmt.where(WorkflowInstance.title.ilike(f"%{instance_title}%")) if workflow_name: if 'WorkflowInstance' not in str(stmt): stmt = stmt.join(WorkflowInstance, WorkflowTask.instance_id == WorkflowInstance.id) stmt = stmt.join(WorkflowDefinition, WorkflowInstance.workflow_id == WorkflowDefinition.id) stmt = stmt.where(WorkflowDefinition.name.ilike(f"%{workflow_name}%")) # 计算总数 count_stmt = select(func.count()).select_from(stmt.subquery()) total_result = await db.execute(count_stmt) total = total_result.scalar() or 0 # 分页 stmt = stmt.order_by(WorkflowTask.sys_create_datetime.desc()) stmt = stmt.offset((page - 1) * page_size).limit(page_size) result = await db.execute(stmt) items = list(result.scalars().all()) return items, total @staticmethod async def get_handled_tasks( db: AsyncSession, user_id: str, page: int = 1, page_size: int = 20, instance_title: str = None, workflow_name: str = None, ) -> Tuple[List[WorkflowTask], int]: """获取已处理任务""" # 只查询用户实际处理过的任务(排除 canceled/waiting/read/pending) handled_statuses = ['approved', 'rejected', 'returned', 'transferred', 'delegated', 'handled'] stmt = select(WorkflowTask).where( WorkflowTask.assignee_id == user_id, WorkflowTask.is_deleted == False, WorkflowTask.status.in_(handled_statuses), ) if instance_title: stmt = stmt.join(WorkflowInstance, WorkflowTask.instance_id == WorkflowInstance.id) stmt = stmt.where(WorkflowInstance.title.ilike(f"%{instance_title}%")) if workflow_name: if 'WorkflowInstance' not in str(stmt): stmt = stmt.join(WorkflowInstance, WorkflowTask.instance_id == WorkflowInstance.id) stmt = stmt.join(WorkflowDefinition, WorkflowInstance.workflow_id == WorkflowDefinition.id) stmt = stmt.where(WorkflowDefinition.name.ilike(f"%{workflow_name}%")) # 计算总数 count_stmt = select(func.count()).select_from(stmt.subquery()) total_result = await db.execute(count_stmt) total = total_result.scalar() or 0 # 分页 stmt = stmt.order_by(WorkflowTask.handled_at.desc()) stmt = stmt.offset((page - 1) * page_size).limit(page_size) result = await db.execute(stmt) items = list(result.scalars().all()) return items, total @staticmethod async def get_instance_pending_tasks(db: AsyncSession, instance_id: str) -> List[WorkflowTask]: """获取流程实例当前待处理的任务""" instance = await WorkflowInstanceService.get_by_id(db, instance_id) if not instance: return [] if instance.status in ['approved', 'rejected', 'canceled']: return [] stmt = select(WorkflowTask).where( WorkflowTask.instance_id == instance_id, WorkflowTask.status.in_(['pending', 'waiting']), WorkflowTask.is_deleted == False, ).order_by(WorkflowTask.sys_create_datetime) result = await db.execute(stmt) return list(result.scalars().all()) @staticmethod async def get_copy_tasks( db: AsyncSession, user_id: str, page: int = 1, page_size: int = 20, instance_title: str = None, workflow_name: str = None, is_read: bool = None, ) -> Tuple[List[WorkflowTask], int]: """获取抄送给我的任务""" stmt = select(WorkflowTask).where( WorkflowTask.assignee_id == user_id, WorkflowTask.task_type == 'copy', WorkflowTask.is_deleted == False, ) if is_read is False: stmt = stmt.where(WorkflowTask.status == 'pending') elif is_read is True: stmt = stmt.where(WorkflowTask.status == 'read') if instance_title: stmt = stmt.join(WorkflowInstance, WorkflowTask.instance_id == WorkflowInstance.id) stmt = stmt.where(WorkflowInstance.title.ilike(f"%{instance_title}%")) if workflow_name: if 'WorkflowInstance' not in str(stmt): stmt = stmt.join(WorkflowInstance, WorkflowTask.instance_id == WorkflowInstance.id) stmt = stmt.join(WorkflowDefinition, WorkflowInstance.workflow_id == WorkflowDefinition.id) stmt = stmt.where(WorkflowDefinition.name.ilike(f"%{workflow_name}%")) # 计算总数 count_stmt = select(func.count()).select_from(stmt.subquery()) total_result = await db.execute(count_stmt) total = total_result.scalar() or 0 # 分页 stmt = stmt.order_by(WorkflowTask.sys_create_datetime.desc()) stmt = stmt.offset((page - 1) * page_size).limit(page_size) result = await db.execute(stmt) items = list(result.scalars().all()) return items, total @staticmethod async def get_by_id(db: AsyncSession, task_id: str) -> Optional[WorkflowTask]: """根据ID获取任务""" stmt = select(WorkflowTask).where( WorkflowTask.id == task_id, WorkflowTask.is_deleted == False, ) result = await db.execute(stmt) return result.scalar_one_or_none() @staticmethod async def _upload_signature(db: AsyncSession, signature_base64: str, task_id: str, user_id: str) -> Optional[str]: """ 上传签名图片到文件系统 Args: db: 数据库会话 signature_base64: base64 格式的签名数据 task_id: 任务ID user_id: 用户ID Returns: 文件ID,如果上传失败返回 None """ import base64 import logging from io import BytesIO from datetime import datetime logger = logging.getLogger(__name__) try: # 解析 base64 数据 if ',' in signature_base64: # 格式: data:image/png;base64,xxxxx signature_base64 = signature_base64.split(',')[1] signature_data = base64.b64decode(signature_base64) # 生成文件名 timestamp = datetime.now().strftime('%Y%m%d%H%M%S') filename = f"signature_{task_id}_{timestamp}.png" # 上传到文件系统 from core.file_manager.service import FileManagerService file_record = await FileManagerService.upload_file( db=db, file=BytesIO(signature_data), filename=filename, content_type="image/png", user_id=user_id, source="workflow", # 自动归类到工作流附件文件夹 is_public=False, # 私有文件 ) logger.info(f"签名上传成功: task_id={task_id}, file_id={file_record.id}") return str(file_record.id) except Exception as e: logger.error(f"签名上传失败: task_id={task_id}, error={e}") return None @staticmethod async def approve( db: AsyncSession, task_id: str, action: str, comment: str, user_id: str, return_to: str = None, form_data: dict = None, signature: str = None, ) -> WorkflowTask: """审批任务""" from online_dev.workflow.engine import WorkflowEngine from online_dev.workflow.engine.base import TaskAction task = await WorkflowTaskService.get_by_id(db, task_id) if not task: raise ValueError("任务不存在") if task.assignee_id != user_id: raise ValueError("无权处理此任务") if task.status != 'pending': raise ValueError("任务已处理") # 处理签名 # signature 可能是文件ID(前端已上传)或 base64 数据(需要后端上传) signature_file_id = None if signature and action == 'approve': # 判断是文件ID还是base64数据 # 文件ID通常是 nanoid 格式(21位字母数字加下划线/横线),base64数据通常以 data: 开头或很长 if signature.startswith('data:') or len(signature) > 100: # base64 数据,需要上传 signature_file_id = await WorkflowTaskService._upload_signature(db, signature, task_id, user_id) else: # 已经是文件ID signature_file_id = signature task.signature_file_id = signature_file_id # 如果审批人修改了表单数据,先更新 if form_data: instance = await WorkflowInstanceService.get_by_id(db, task.instance_id) if instance and instance.form_code and instance.form_data_id: try: from online_dev.form_data_manager.service import FormDataService service = await FormDataService.create_service(db, instance.form_code) await service.update(db, instance.form_data_id, form_data) except Exception as e: import logging logging.getLogger(__name__).warning(f"审批时更新表单数据失败: {e}") # 映射操作类型 action_map = { 'approve': TaskAction.APPROVE, 'reject': TaskAction.REJECT, 'return': TaskAction.RETURN, } task_action = action_map.get(action) if not task_action: raise ValueError(f"无效的操作类型: {action}") # 使用流程引擎处理任务 engine = WorkflowEngine() return await engine.complete_task( db=db, task=task, action=task_action, comment=comment, user_id=user_id, return_to=return_to, ) @staticmethod async def handle(db: AsyncSession, task_id: str, comment: str, user_id: str, form_data: dict = None) -> WorkflowTask: """办理任务""" from online_dev.workflow.engine import WorkflowEngine from online_dev.workflow.engine.base import ExecutionContext task = await WorkflowTaskService.get_by_id(db, task_id) if not task: raise ValueError("任务不存在") if task.assignee_id != user_id: raise ValueError("无权处理此任务") if task.status != 'pending': raise ValueError("任务已处理") if task.task_type != 'handle': raise ValueError("此任务不是办理任务") # 如果办理人修改了表单数据,先更新 if form_data: instance = await WorkflowInstanceService.get_by_id(db, task.instance_id) if instance and instance.form_code and instance.form_data_id: try: from online_dev.form_data_manager.service import FormDataService service = await FormDataService.create_service(db, instance.form_code) await service.update(db, instance.form_data_id, form_data) except Exception as e: import logging logging.getLogger(__name__).warning(f"办理时更新表单数据失败: {e}") # 更新任务状态 task.status = 'handled' task.comment = comment task.handled_at = datetime.now() db.add(task) # 完成该任务对应的钉钉待办 try: from core.message.service import NotifyService await NotifyService.complete_dingtalk_todo(db, "workflow_task", str(task.id)) except Exception as e: import logging logging.getLogger(__name__).warning(f"办理-清理钉钉待办失败 task={task.id}: {e}") # 记录日志 log = WorkflowLog( instance_id=task.instance_id, node_id=task.node_id, node_name=task.node_name, action='handle', operator_id=user_id, comment=comment, ) db.add(log) await db.flush() # 获取实例和流程定义 instance = await WorkflowInstanceService.get_by_id(db, task.instance_id) workflow = await WorkflowDefinitionService.get_by_id(db, instance.workflow_id) # 使用流程引擎处理办理完成后的推进 engine = WorkflowEngine() context = ExecutionContext( instance=instance, form_data=form_data or {}, current_user_id=user_id, flow_definition=workflow.flow_definition, db=db, ) await engine._handle_handle_completion(context, task) return task @staticmethod async def transfer(db: AsyncSession, task_id: str, to_user_id: str, comment: str, user_id: str) -> WorkflowTask: """转交任务""" from online_dev.workflow.engine import WorkflowEngine from core.user.model import User task = await WorkflowTaskService.get_by_id(db, task_id) if not task: raise ValueError("任务不存在") if task.assignee_id != user_id: raise ValueError("无权转交此任务") if task.status != 'pending': raise ValueError("任务已处理") # 获取目标用户 stmt = select(User).where(User.id == to_user_id) result = await db.execute(stmt) to_user = result.scalar_one_or_none() if not to_user: raise ValueError("目标用户不存在") # 使用流程引擎转交任务 engine = WorkflowEngine() return await engine.transfer_task( db=db, task=task, to_user=to_user, comment=comment, user_id=user_id, ) @staticmethod async def get_pending_count(db: AsyncSession, user_id: str) -> int: """获取待处理任务数量""" stmt = select(func.count()).where( WorkflowTask.assignee_id == user_id, WorkflowTask.status == 'pending', WorkflowTask.is_deleted == False, ) result = await db.execute(stmt) return result.scalar() or 0 @staticmethod async def delegate(db: AsyncSession, task_id: str, to_user_id: str, comment: str, user_id: str) -> WorkflowTask: """委派任务""" from core.user.model import User task = await WorkflowTaskService.get_by_id(db, task_id) if not task: raise ValueError("任务不存在") if task.assignee_id != user_id: raise ValueError("无权委派此任务") if task.status != 'pending': raise ValueError("任务已处理") if task.task_type not in ('approval', 'handle'): raise ValueError("此类型任务不支持委派") # 获取被委派人 stmt = select(User).where(User.id == to_user_id) result = await db.execute(stmt) to_user = result.scalar_one_or_none() if not to_user: raise ValueError("被委派人不存在") if str(to_user.id) == user_id: raise ValueError("不能委派给自己") # 更新原任务状态为已委派 task.status = 'delegated' task.comment = comment or f'委派给 {to_user.name or to_user.username}' task.handled_at = datetime.now() 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='delegate', 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 notify_err: import logging logging.getLogger('online_dev.workflow.service').warning( f'委派后发送任务通知失败 task={new_task.id}: {notify_err}' ) # 记录日志 log = WorkflowLog( instance_id=task.instance_id, node_id=task.node_id, node_name=task.node_name, action='delegate', operator_id=user_id, comment=f'委派给 {to_user.name or to_user.username}' + (f':{comment}' if comment else ''), extra_data={ 'to_user_id': str(to_user.id), 'to_user_name': to_user.name or to_user.username, 'original_task_id': str(task.id), }, ) db.add(log) await db.flush() await db.refresh(new_task) return new_task @staticmethod async def mark_read(db: AsyncSession, task_id: str, user_id: str) -> WorkflowTask: """标记抄送任务为已读""" task = await WorkflowTaskService.get_by_id(db, task_id) if not task: raise ValueError("任务不存在") if task.assignee_id != user_id: raise ValueError("无权操作此任务") if task.task_type != 'copy': raise ValueError("只能标记抄送任务为已读") if task.status != 'pending': raise ValueError("任务已处理") task.status = 'read' task.handled_at = datetime.now() db.add(task) await db.flush() await db.refresh(task) return task @staticmethod async def add_sign( db: AsyncSession, task_id: str, sign_type: str, to_user_ids: list, comment: str, user_id: str, ) -> List[WorkflowTask]: """加签任务""" from core.user.model import User if sign_type not in ('before', 'after', 'parallel'): raise ValueError("无效的加签类型") if not to_user_ids: raise ValueError("请选择加签人") task = await WorkflowTaskService.get_by_id(db, task_id) if not task: raise ValueError("任务不存在") if task.assignee_id != user_id: raise ValueError("无权操作此任务") if task.status != 'pending': raise ValueError("任务已处理") if task.task_type not in ('approval', 'handle'): raise ValueError("此类型任务不支持加签") # 获取加签人 stmt = select(User).where(User.id.in_(to_user_ids)) result = await db.execute(stmt) to_users = list(result.scalars().all()) if len(to_users) != len(to_user_ids): raise ValueError("部分加签人不存在") # 检查是否包含自己 if user_id in to_user_ids: raise ValueError("不能加签给自己") created_tasks = [] to_user_names = [u.name or u.username for u in to_users] if sign_type == 'before': # 前加签:当前任务暂停 task.status = 'waiting' task.comment = f'前加签给 {", ".join(to_user_names)}' db.add(task) for to_user in to_users: new_task = WorkflowTask( instance_id=task.instance_id, node_id=task.node_id, node_name=task.node_name, task_type='approval', status='pending', assignee_id=str(to_user.id), parent_task_id=str(task.id), sign_type='before', ) db.add(new_task) created_tasks.append(new_task) elif sign_type == 'after': # 后加签:当前处理人先审批 task.status = 'approved' task.comment = f'审批通过,后加签给 {", ".join(to_user_names)}' task.handled_at = datetime.now() db.add(task) for to_user in to_users: new_task = WorkflowTask( instance_id=task.instance_id, node_id=task.node_id, node_name=task.node_name, task_type='approval', status='pending', assignee_id=str(to_user.id), parent_task_id=str(task.id), sign_type='after', ) db.add(new_task) created_tasks.append(new_task) else: # parallel # 并行加签 for to_user in to_users: new_task = WorkflowTask( instance_id=task.instance_id, node_id=task.node_id, node_name=task.node_name, task_type='approval', status='pending', assignee_id=str(to_user.id), parent_task_id=str(task.id), sign_type='parallel', ) db.add(new_task) created_tasks.append(new_task) # 记录日志 sign_type_labels = { 'before': '前加签', 'after': '后加签', 'parallel': '并行加签', } log = WorkflowLog( instance_id=task.instance_id, node_id=task.node_id, node_name=task.node_name, action='add_sign', operator_id=user_id, comment=f'{sign_type_labels[sign_type]}给 {", ".join(to_user_names)}' + (f':{comment}' if comment else ''), extra_data={ 'sign_type': sign_type, 'to_user_ids': to_user_ids, 'to_user_names': to_user_names, 'original_task_id': str(task.id), }, ) db.add(log) await db.flush() for t in created_tasks: await db.refresh(t) return created_tasks @staticmethod async def revise(db: AsyncSession, task_id: str, form_data: dict, comment: str, user_id: str) -> WorkflowTask: """修改任务(驳回后发起人重新提交)""" from online_dev.workflow.engine import WorkflowEngine task = await WorkflowTaskService.get_by_id(db, task_id) if not task: raise ValueError("任务不存在") if task.assignee_id != user_id: raise ValueError("无权操作此任务") if task.task_type != 'revise': raise ValueError("此任务不是修改任务") if task.status != 'pending': raise ValueError("任务已处理") instance = await WorkflowInstanceService.get_by_id(db, task.instance_id) if not instance: raise ValueError("流程实例不存在") # 完成当前任务 task.status = 'approved' task.comment = comment or '已修改并重新提交' task.handled_at = datetime.now() db.add(task) # 记录日志 log = WorkflowLog( instance_id=str(instance.id), node_id=task.node_id, node_name='修改重提', action='revise', operator_id=user_id, comment=comment or '已修改并重新提交', ) db.add(log) await db.flush() # 更新表单数据(前端传入格式: {main: {...}, sub_tables: {...}}) # 注意: FormDataService.update 内部会 db.commit(),所以放在所有 ORM 操作之后 import logging _logger = logging.getLogger(__name__) _logger.info(f"revise: form_code={instance.form_code}, form_data_id={instance.form_data_id}") _logger.info(f"revise: form_data keys={list(form_data.keys()) if form_data else 'None'}") if form_data and form_data.get('main'): _logger.info(f"revise: main data keys={list(form_data['main'].keys())}") if form_data and instance.form_code and instance.form_data_id: try: from online_dev.form_data_manager.service import FormDataService service = await FormDataService.create_service(db, instance.form_code) _logger.info(f"revise: calling service.update with pk={instance.form_data_id}") await service.update(db, instance.form_data_id, form_data) _logger.info(f"revise: form data update success") except Exception as e: _logger.error(f"更新表单数据失败: {e}", exc_info=True) # 重新刷新 instance(因为 FormDataService.update 内部 commit 可能导致 ORM 对象 detached) instance = await WorkflowInstanceService.get_by_id(db, task.instance_id) # 重新启动流程 engine = WorkflowEngine() await engine.restart_instance(db, instance, user_id) task = await WorkflowTaskService.get_by_id(db, task_id) return task class WorkflowLogService: """日志服务""" @staticmethod async def get_by_instance(db: AsyncSession, instance_id: str) -> List[WorkflowLog]: """获取流程实例的日志""" stmt = select(WorkflowLog).where( WorkflowLog.instance_id == instance_id, WorkflowLog.is_deleted == False, ).order_by(WorkflowLog.sys_create_datetime) result = await db.execute(stmt) return list(result.scalars().all())