Phase 6 功能补全: - P1-3: 消息 SSE 实时推送端点 + 前端 EventSource 连接 - P1-6: ServiceTask HTTP 调用能力 (reqwest GET/POST) - P1-7: user.deleted 事件处理 — 终止相关流程实例 - P1-8: 任务认领 (claim) 端点 + handler - P1-9: 超时检查器发布 task.timeout 事件 - P1-15: 组织/部门名称唯一性校验 (create + update) - P1-18: 消息群发 fan-out (role/department/all 批量投递) Phase 7 P3-P4 收尾: - PluginAdmin purge 按钮状态修复 - ChangePassword 最小 8 字符 + 新旧密码不同验证 - AuditLogViewer 用户名缓存 + 扩展资源类型 - InstanceMonitor 通过 definition 缓存解析 node_name - NotificationPreferences DND 时间范围校验
270 lines
8.7 KiB
Rust
270 lines
8.7 KiB
Rust
use crate::dto::{EdgeDef, NodeDef, NodeType};
|
||
use crate::engine::model::FlowGraph;
|
||
use crate::error::{WorkflowError, WorkflowResult};
|
||
|
||
/// 解析节点和边列表为 FlowGraph 并验证合法性。
|
||
pub fn parse_and_validate(nodes: &[NodeDef], edges: &[EdgeDef]) -> WorkflowResult<FlowGraph> {
|
||
// 基本检查:至少有一个节点
|
||
if nodes.is_empty() {
|
||
return Err(WorkflowError::InvalidDiagram("流程图不能为空".to_string()));
|
||
}
|
||
|
||
// 检查恰好 1 个 StartEvent
|
||
let start_count = nodes
|
||
.iter()
|
||
.filter(|n| n.node_type == NodeType::StartEvent)
|
||
.count();
|
||
if start_count == 0 {
|
||
return Err(WorkflowError::InvalidDiagram(
|
||
"流程图必须包含一个开始事件".to_string(),
|
||
));
|
||
}
|
||
if start_count > 1 {
|
||
return Err(WorkflowError::InvalidDiagram(
|
||
"流程图只能包含一个开始事件".to_string(),
|
||
));
|
||
}
|
||
|
||
// 检查至少 1 个 EndEvent
|
||
let end_count = nodes
|
||
.iter()
|
||
.filter(|n| n.node_type == NodeType::EndEvent)
|
||
.count();
|
||
if end_count == 0 {
|
||
return Err(WorkflowError::InvalidDiagram(
|
||
"流程图必须包含至少一个结束事件".to_string(),
|
||
));
|
||
}
|
||
|
||
// 检查节点 ID 唯一性
|
||
let node_ids: std::collections::HashSet<&str> = nodes.iter().map(|n| n.id.as_str()).collect();
|
||
if node_ids.len() != nodes.len() {
|
||
return Err(WorkflowError::InvalidDiagram(
|
||
"节点 ID 不能重复".to_string(),
|
||
));
|
||
}
|
||
|
||
// 检查边引用的节点存在
|
||
for e in edges {
|
||
if !node_ids.contains(e.source.as_str()) {
|
||
return Err(WorkflowError::InvalidDiagram(format!(
|
||
"连线 {} 的源节点 {} 不存在",
|
||
e.id, e.source
|
||
)));
|
||
}
|
||
if !node_ids.contains(e.target.as_str()) {
|
||
return Err(WorkflowError::InvalidDiagram(format!(
|
||
"连线 {} 的目标节点 {} 不存在",
|
||
e.id, e.target
|
||
)));
|
||
}
|
||
}
|
||
|
||
// 构建图
|
||
let graph = FlowGraph::build(nodes, edges);
|
||
|
||
// 检查 StartEvent 没有入边
|
||
if let Some(start_id) = &graph.start_node_id {
|
||
if !graph.get_incoming_edges(start_id).is_empty() {
|
||
return Err(WorkflowError::InvalidDiagram(
|
||
"开始事件不能有入边".to_string(),
|
||
));
|
||
}
|
||
if graph.get_outgoing_edges(start_id).is_empty() {
|
||
return Err(WorkflowError::InvalidDiagram(
|
||
"开始事件必须有出边".to_string(),
|
||
));
|
||
}
|
||
}
|
||
|
||
// 检查 EndEvent 没有出边
|
||
for end_id in &graph.end_node_ids {
|
||
if !graph.get_outgoing_edges(end_id).is_empty() {
|
||
return Err(WorkflowError::InvalidDiagram(
|
||
"结束事件不能有出边".to_string(),
|
||
));
|
||
}
|
||
}
|
||
|
||
// 检查网关至少有一个入边和一个出边(排除 start/end)
|
||
for node in nodes {
|
||
match &node.node_type {
|
||
NodeType::ExclusiveGateway | NodeType::ParallelGateway => {
|
||
let inc = graph.get_incoming_edges(&node.id);
|
||
let out = graph.get_outgoing_edges(&node.id);
|
||
if inc.is_empty() {
|
||
return Err(WorkflowError::InvalidDiagram(format!(
|
||
"网关 '{}' 必须有至少一条入边",
|
||
node.name
|
||
)));
|
||
}
|
||
if out.is_empty() {
|
||
return Err(WorkflowError::InvalidDiagram(format!(
|
||
"网关 '{}' 必须有至少一条出边",
|
||
node.name
|
||
)));
|
||
}
|
||
// 排他网关的出边应该有条件(第一条可以无条件作为默认分支)
|
||
if node.node_type == NodeType::ExclusiveGateway && out.len() > 1 {
|
||
let with_condition: Vec<_> =
|
||
out.iter().filter(|e| e.condition.is_some()).collect();
|
||
if with_condition.is_empty() {
|
||
return Err(WorkflowError::InvalidDiagram(format!(
|
||
"排他网关 '{}' 有多条出边但没有条件表达式",
|
||
node.name
|
||
)));
|
||
}
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
Ok(graph)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::dto::NodePosition;
|
||
|
||
fn make_start() -> NodeDef {
|
||
NodeDef {
|
||
id: "start".to_string(),
|
||
node_type: NodeType::StartEvent,
|
||
name: "开始".to_string(),
|
||
assignee_id: None,
|
||
candidate_groups: None,
|
||
service_type: None,
|
||
service_config: None,
|
||
position: Some(NodePosition { x: 100.0, y: 100.0 }),
|
||
}
|
||
}
|
||
|
||
fn make_end() -> NodeDef {
|
||
NodeDef {
|
||
id: "end".to_string(),
|
||
node_type: NodeType::EndEvent,
|
||
name: "结束".to_string(),
|
||
assignee_id: None,
|
||
candidate_groups: None,
|
||
service_type: None,
|
||
service_config: None,
|
||
position: Some(NodePosition { x: 100.0, y: 300.0 }),
|
||
}
|
||
}
|
||
|
||
fn make_user_task(id: &str, name: &str) -> NodeDef {
|
||
NodeDef {
|
||
id: id.to_string(),
|
||
node_type: NodeType::UserTask,
|
||
name: name.to_string(),
|
||
assignee_id: None,
|
||
candidate_groups: None,
|
||
service_type: None,
|
||
service_config: None,
|
||
position: None,
|
||
}
|
||
}
|
||
|
||
fn make_edge(id: &str, source: &str, target: &str) -> EdgeDef {
|
||
EdgeDef {
|
||
id: id.to_string(),
|
||
source: source.to_string(),
|
||
target: target.to_string(),
|
||
condition: None,
|
||
label: None,
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_valid_linear_flow() {
|
||
let nodes = vec![make_start(), make_user_task("task1", "审批"), make_end()];
|
||
let edges = vec![
|
||
make_edge("e1", "start", "task1"),
|
||
make_edge("e2", "task1", "end"),
|
||
];
|
||
let result = parse_and_validate(&nodes, &edges);
|
||
assert!(result.is_ok());
|
||
let graph = result.unwrap();
|
||
assert_eq!(graph.start_node_id, Some("start".to_string()));
|
||
assert_eq!(graph.end_node_ids, vec!["end".to_string()]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_no_start_event() {
|
||
let nodes = vec![make_user_task("task1", "审批"), make_end()];
|
||
let edges = vec![make_edge("e1", "task1", "end")];
|
||
let result = parse_and_validate(&nodes, &edges);
|
||
assert!(result.is_err());
|
||
let msg = result.unwrap_err().to_string();
|
||
assert!(msg.contains("开始事件"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_no_end_event() {
|
||
let nodes = vec![make_start(), make_user_task("task1", "审批")];
|
||
let edges = vec![make_edge("e1", "start", "task1")];
|
||
let result = parse_and_validate(&nodes, &edges);
|
||
assert!(result.is_err());
|
||
let msg = result.unwrap_err().to_string();
|
||
assert!(msg.contains("结束事件"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_duplicate_node_id() {
|
||
let nodes = vec![
|
||
make_start(),
|
||
NodeDef {
|
||
id: "start".to_string(), // 重复 ID
|
||
node_type: NodeType::EndEvent,
|
||
name: "结束".to_string(),
|
||
assignee_id: None,
|
||
candidate_groups: None,
|
||
service_type: None,
|
||
service_config: None,
|
||
position: None,
|
||
},
|
||
];
|
||
let edges = vec![];
|
||
let result = parse_and_validate(&nodes, &edges);
|
||
assert!(result.is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_end_event_with_outgoing() {
|
||
let nodes = vec![make_start(), make_end()];
|
||
let edges = vec![
|
||
make_edge("e1", "start", "end"),
|
||
make_edge("e2", "end", "start"), // 结束事件有出边
|
||
];
|
||
let result = parse_and_validate(&nodes, &edges);
|
||
assert!(result.is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_exclusive_gateway_without_conditions() {
|
||
let nodes = vec![
|
||
make_start(),
|
||
NodeDef {
|
||
id: "gw1".to_string(),
|
||
node_type: NodeType::ExclusiveGateway,
|
||
name: "判断".to_string(),
|
||
assignee_id: None,
|
||
candidate_groups: None,
|
||
service_type: None,
|
||
service_config: None,
|
||
position: None,
|
||
},
|
||
make_end(),
|
||
];
|
||
let edges = vec![
|
||
make_edge("e1", "start", "gw1"),
|
||
make_edge("e2", "gw1", "end"),
|
||
make_edge("e3", "gw1", "end"), // 两条出边无条件
|
||
];
|
||
let result = parse_and_validate(&nodes, &edges);
|
||
assert!(result.is_err());
|
||
}
|
||
}
|