Files
hms/crates/erp-workflow/src/engine/parser.rs
iven 6d5a711d2c
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
功能修复:
1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查
2. 仪表盘统计容错:单个查询失败返回零值而非 500
3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致
4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径
5. 积分端点权限码:health.health-data.list → health.points.list
6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage
7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档

Clippy 全 workspace 清零(14→0 errors):
- erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处
- erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处
- erp-ai: 修复 dead_code、unused import 等 11 处
- erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处
- erp-server-migration: 修复 enum_variant_names 5 处
- erp-auth/config/workflow/message: 各 1-3 处

工程改进:
- lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy)
- cargo fmt 统一格式化
2026-05-07 23:43:14 +08:00

492 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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());
}
// ---- 边界测试扩展 ----
#[test]
fn test_empty_nodes_rejected() {
let result = parse_and_validate(&[], &[]);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("不能为空"));
}
#[test]
fn test_multiple_start_events_rejected() {
let nodes = vec![
make_start(),
NodeDef {
id: "start2".to_string(),
node_type: NodeType::StartEvent,
name: "开始2".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", "end"),
make_edge("e2", "start2", "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_edge_references_nonexistent_source() {
let nodes = vec![make_start(), make_end()];
let edges = vec![make_edge("e1", "ghost", "end")];
let result = parse_and_validate(&nodes, &edges);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("源节点") && msg.contains("不存在"));
}
#[test]
fn test_edge_references_nonexistent_target() {
let nodes = vec![make_start(), make_end()];
let edges = vec![make_edge("e1", "start", "ghost")];
let result = parse_and_validate(&nodes, &edges);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("目标节点") && msg.contains("不存在"));
}
#[test]
fn test_start_event_with_incoming_edge_rejected() {
let nodes = vec![make_start(), make_end()];
let edges = vec![
make_edge("e1", "start", "end"),
make_edge("e2", "end", "start"), // start 有入边
];
let result = parse_and_validate(&nodes, &edges);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("入边"));
}
#[test]
fn test_start_event_without_outgoing_edge_rejected() {
let nodes = vec![make_start(), make_end()];
let edges = vec![]; // start 没有出边
let result = parse_and_validate(&nodes, &edges);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("出边"));
}
#[test]
fn test_exclusive_gateway_single_outgoing_ok() {
// 单条出边的排他网关不需要条件
let nodes = vec![
make_start(),
NodeDef {
id: "gw".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", "gw"), make_edge("e2", "gw", "end")];
assert!(parse_and_validate(&nodes, &edges).is_ok());
}
#[test]
fn test_exclusive_gateway_with_conditions_ok() {
let nodes = vec![
make_start(),
NodeDef {
id: "gw".to_string(),
node_type: NodeType::ExclusiveGateway,
name: "金额判断".to_string(),
assignee_id: None,
candidate_groups: None,
service_type: None,
service_config: None,
position: None,
},
make_user_task("task_a", "小额审批"),
make_user_task("task_b", "大额审批"),
make_end(),
];
let edges = vec![
make_edge("e1", "start", "gw"),
EdgeDef {
id: "e2".to_string(),
source: "gw".to_string(),
target: "task_a".to_string(),
condition: Some("amount <= 1000".to_string()),
label: None,
},
EdgeDef {
id: "e3".to_string(),
source: "gw".to_string(),
target: "task_b".to_string(),
condition: Some("amount > 1000".to_string()),
label: None,
},
make_edge("e4", "task_a", "end"),
make_edge("e5", "task_b", "end"),
];
assert!(parse_and_validate(&nodes, &edges).is_ok());
}
#[test]
fn test_gateway_without_incoming_rejected() {
let nodes = vec![
make_start(),
NodeDef {
id: "gw".to_string(),
node_type: NodeType::ParallelGateway,
name: "并行".to_string(),
assignee_id: None,
candidate_groups: None,
service_type: None,
service_config: None,
position: None,
},
make_end(),
];
// gw 没有入边
let edges = vec![
make_edge("e1", "start", "end"),
make_edge("e2", "gw", "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_gateway_without_outgoing_rejected() {
let nodes = vec![
make_start(),
NodeDef {
id: "gw".to_string(),
node_type: NodeType::ParallelGateway,
name: "并行".to_string(),
assignee_id: None,
candidate_groups: None,
service_type: None,
service_config: None,
position: None,
},
make_end(),
];
// gw 没有出边
let edges = vec![
make_edge("e1", "start", "gw"),
make_edge("e2", "start", "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_parallel_gateway_valid() {
let nodes = vec![
make_start(),
NodeDef {
id: "fork".to_string(),
node_type: NodeType::ParallelGateway,
name: "拆分".to_string(),
assignee_id: None,
candidate_groups: None,
service_type: None,
service_config: None,
position: None,
},
make_user_task("a", "任务A"),
make_user_task("b", "任务B"),
make_end(),
];
let edges = vec![
make_edge("e1", "start", "fork"),
make_edge("e2", "fork", "a"),
make_edge("e3", "fork", "b"),
make_edge("e4", "a", "end"),
make_edge("e5", "b", "end"),
];
assert!(parse_and_validate(&nodes, &edges).is_ok());
}
}