#!/usr/bin/env python # -*- coding: utf-8 -*- """流程定义结构校验(名称长度、发布前结构校验等)""" from typing import Dict, List, Optional from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession MAX_NODE_NAME_LENGTH = 100 _ASSIGNEE_TYPES_NEED_LIST = ('user', 'role', 'department') def validate_flow_definition_names(flow_definition: dict) -> None: """ 校验 flow_definition 中节点与分支名称长度。 Raises: ValueError: 名称超过限制时 """ if not flow_definition or not isinstance(flow_definition, dict): return nodes = flow_definition.get('nodes') if not nodes or not isinstance(nodes, dict): return def check_name(name: str, label: str) -> None: if name and len(name) > MAX_NODE_NAME_LENGTH: raise ValueError( f'{label}名称不能超过 {MAX_NODE_NAME_LENGTH} 个字符' ) def walk(node: dict) -> None: if not node or not isinstance(node, dict): return node_label = node.get('name') or node.get('id') or node.get('type') or '节点' check_name(node.get('name') or '', node_label) for branch in node.get('branches') or []: if not isinstance(branch, dict): continue branch_label = branch.get('name') or branch.get('id') or '分支' check_name(branch.get('name') or '', branch_label) config = branch.get('config') or {} if isinstance(config, dict): config_name = config.get('name') or '' check_name(config_name, branch_label) child = branch.get('children') if child: walk(child) child = node.get('children') if child: walk(child) walk(nodes) def _node_label(node: dict) -> str: return node.get('name') or node.get('id') or node.get('type') or '节点' def _find_nodes_of_type(node: Optional[dict], node_type: str) -> List[dict]: if not node or not isinstance(node, dict): return [] result: List[dict] = [] if node.get('type') == node_type: result.append(node) children = node.get('children') if children: result.extend(_find_nodes_of_type(children, node_type)) for branch in node.get('branches') or []: if isinstance(branch, dict) and branch.get('children'): result.extend(_find_nodes_of_type(branch['children'], node_type)) return result def _assignees_configured(config: dict) -> bool: if not isinstance(config, dict): return False assignee_type = config.get('assigneeType') or 'user' assignees = config.get('assignees') or [] assignee_fields = config.get('assigneeFields') or [] assignee_field = config.get('assigneeField') or '' if assignee_type in _ASSIGNEE_TYPES_NEED_LIST: return len(assignees) > 0 if assignee_type == 'form_field': return len(assignee_fields) > 0 or bool(assignee_field) # superior / manager / initiator 等无需显式配置列表 return True def validate_flow_definition_for_publish(flow_definition: dict) -> None: """ 发布前校验流程定义结构(与设计器 useFlowValidation 核心规则对齐)。 Raises: ValueError: 不满足发布条件时 """ if not flow_definition or not isinstance(flow_definition, dict): raise ValueError('流程定义为空') root = flow_definition.get('nodes') if not root or not isinstance(root, dict): raise ValueError('流程定义缺少有效节点') approval_nodes = _find_nodes_of_type(root, 'approval') if not approval_nodes: raise ValueError('流程必须包含至少一个审批节点') for node in approval_nodes: label = _node_label(node) if not _assignees_configured(node.get('config') or {}): raise ValueError(f'审批节点「{label}」未配置审批人') for node in _find_nodes_of_type(root, 'handle'): label = _node_label(node) if not _assignees_configured(node.get('config') or {}): raise ValueError(f'办理节点「{label}」未配置办理人') for node in _find_nodes_of_type(root, 'condition'): label = _node_label(node) branches = node.get('branches') or [] if not branches: raise ValueError(f'条件节点「{label}」未配置分支') has_default = False for branch in branches: if not isinstance(branch, dict): continue config = branch.get('config') or {} if config.get('isDefault'): has_default = True continue groups = config.get('groups') or [] branch_label = branch.get('name') or branch.get('id') or '分支' if not groups: raise ValueError( f'条件节点「{label}」的分支「{branch_label}」未配置条件' ) if not has_default: raise ValueError(f'条件节点「{label}」缺少默认分支') async def validate_flow_definition_subflows_for_publish( db: AsyncSession, flow_definition: dict, ) -> None: """ 校验子流程节点引用的流程已发布。 Raises: ValueError: 子流程未配置或未发布时 """ from online_dev.workflow.model import WorkflowDefinition root = (flow_definition or {}).get('nodes') if not root: return for node in _find_nodes_of_type(root, 'subflow'): label = _node_label(node) config = node.get('config') or {} subflow_id = (config.get('subflowId') or '').strip() if not subflow_id: raise ValueError(f'子流程节点「{label}」未选择子流程') stmt = select(WorkflowDefinition).where( WorkflowDefinition.id == subflow_id, WorkflowDefinition.is_deleted == False, ) result = await db.execute(stmt) child = result.scalar_one_or_none() if not child: raise ValueError(f'子流程节点「{label}」引用的流程不存在') if child.status != 'published': child_name = child.name or subflow_id raise ValueError( f'子流程节点「{label}」引用的流程「{child_name}」尚未发布' )