192 lines
6.3 KiB
Python
192 lines
6.3 KiB
Python
#!/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}」尚未发布'
|
|
)
|