Files
base/crates/erp-workflow/src/engine/model.rs
iven 59856ac2fc feat: initialize ERP base platform (extracted from HMS)
- Stripped 11 business crates (health, ai, dialysis, plugins)
- Cleaned AppState, AppConfig, main.rs from business coupling
- Reduced migrations from 169 to 53 (base-only)
- Removed health_provider trait from erp-core
- Removed business integration tests
- Removed gateway rate limiting middleware
- Base capabilities: auth, RBAC, JWT, config, workflow, message, plugin, audit, crypto, RLS, multi-tenant

Cargo check: OK
Cargo test: OK
2026-05-31 20:35:57 +08:00

386 lines
12 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 std::collections::HashMap;
use crate::dto::{EdgeDef, NodeDef, NodeType};
/// 内存中的流程图模型,用于执行引擎。
#[derive(Debug, Clone)]
pub struct FlowGraph {
/// node_id → FlowNode
pub nodes: HashMap<String, FlowNode>,
/// edge_id → FlowEdge
pub edges: HashMap<String, FlowEdge>,
/// node_id → 从该节点出发的边列表
pub outgoing: HashMap<String, Vec<String>>,
/// node_id → 到达该节点的边列表
pub incoming: HashMap<String, Vec<String>>,
/// StartEvent 的 node_id
pub start_node_id: Option<String>,
/// 所有 EndEvent 的 node_id
pub end_node_ids: Vec<String>,
}
/// 内存中的节点模型。
#[derive(Debug, Clone)]
pub struct FlowNode {
pub id: String,
pub node_type: NodeType,
pub name: String,
pub assignee_id: Option<uuid::Uuid>,
pub candidate_groups: Option<Vec<String>>,
pub service_type: Option<String>,
pub service_config: Option<crate::dto::ServiceTaskConfig>,
}
/// 内存中的边模型。
#[derive(Debug, Clone)]
pub struct FlowEdge {
pub id: String,
pub source: String,
pub target: String,
pub condition: Option<String>,
pub label: Option<String>,
}
impl FlowGraph {
/// 从 DTO 节点和边列表构建 FlowGraph。
pub fn build(nodes: &[NodeDef], edges: &[EdgeDef]) -> Self {
let mut graph = FlowGraph {
nodes: HashMap::new(),
edges: HashMap::new(),
outgoing: HashMap::new(),
incoming: HashMap::new(),
start_node_id: None,
end_node_ids: Vec::new(),
};
for n in nodes {
let flow_node = FlowNode {
id: n.id.clone(),
node_type: n.node_type.clone(),
name: n.name.clone(),
assignee_id: n.assignee_id,
candidate_groups: n.candidate_groups.clone(),
service_type: n.service_type.clone(),
service_config: n.service_config.clone(),
};
if n.node_type == NodeType::StartEvent {
graph.start_node_id = Some(n.id.clone());
}
if n.node_type == NodeType::EndEvent {
graph.end_node_ids.push(n.id.clone());
}
graph.nodes.insert(n.id.clone(), flow_node);
graph.outgoing.insert(n.id.clone(), Vec::new());
graph.incoming.insert(n.id.clone(), Vec::new());
}
for e in edges {
graph.edges.insert(
e.id.clone(),
FlowEdge {
id: e.id.clone(),
source: e.source.clone(),
target: e.target.clone(),
condition: e.condition.clone(),
label: e.label.clone(),
},
);
if let Some(out) = graph.outgoing.get_mut(&e.source) {
out.push(e.id.clone());
}
if let Some(inc) = graph.incoming.get_mut(&e.target) {
inc.push(e.id.clone());
}
}
graph
}
/// 获取节点的出边。
pub fn get_outgoing_edges(&self, node_id: &str) -> Vec<&FlowEdge> {
self.outgoing
.get(node_id)
.map(|edge_ids| {
edge_ids
.iter()
.filter_map(|eid| self.edges.get(eid))
.collect()
})
.unwrap_or_default()
}
/// 获取节点的入边。
pub fn get_incoming_edges(&self, node_id: &str) -> Vec<&FlowEdge> {
self.incoming
.get(node_id)
.map(|edge_ids| {
edge_ids
.iter()
.filter_map(|eid| self.edges.get(eid))
.collect()
})
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dto::{EdgeDef, NodeDef, NodeType};
fn make_node(id: &str, node_type: NodeType) -> NodeDef {
NodeDef {
id: id.to_string(),
node_type,
name: id.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,
}
}
// ---- build ----
#[test]
fn build_empty_graph() {
let graph = FlowGraph::build(&[], &[]);
assert!(graph.nodes.is_empty());
assert!(graph.edges.is_empty());
assert!(graph.start_node_id.is_none());
assert!(graph.end_node_ids.is_empty());
}
#[test]
fn build_identifies_start_and_end() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("task1", NodeType::UserTask),
make_node("end", NodeType::EndEvent),
];
let edges = vec![
make_edge("e1", "start", "task1"),
make_edge("e2", "task1", "end"),
];
let graph = FlowGraph::build(&nodes, &edges);
assert_eq!(graph.start_node_id, Some("start".to_string()));
assert_eq!(graph.end_node_ids, vec!["end".to_string()]);
assert_eq!(graph.nodes.len(), 3);
assert_eq!(graph.edges.len(), 2);
}
#[test]
fn build_multiple_end_events() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("gw", NodeType::ExclusiveGateway),
make_node("end1", NodeType::EndEvent),
make_node("end2", NodeType::EndEvent),
];
let edges = vec![
make_edge("e1", "start", "gw"),
make_edge("e2", "gw", "end1"),
make_edge("e3", "gw", "end2"),
];
let graph = FlowGraph::build(&nodes, &edges);
assert_eq!(graph.end_node_ids.len(), 2);
assert!(graph.end_node_ids.contains(&"end1".to_string()));
assert!(graph.end_node_ids.contains(&"end2".to_string()));
}
#[test]
fn build_copies_node_properties() {
let user_id = {
let ts = uuid::Timestamp::now(uuid::NoContext);
uuid::Uuid::new_v7(ts)
};
let nodes = vec![NodeDef {
id: "task".to_string(),
node_type: NodeType::UserTask,
name: "审批".to_string(),
assignee_id: Some(user_id),
candidate_groups: Some(vec!["managers".to_string()]),
service_type: None,
service_config: None,
position: None,
}];
let graph = FlowGraph::build(&nodes, &[]);
let node = graph.nodes.get("task").unwrap();
assert_eq!(node.name, "审批");
assert_eq!(node.assignee_id, Some(user_id));
assert_eq!(node.candidate_groups, Some(vec!["managers".to_string()]));
}
#[test]
fn build_edge_with_condition_and_label() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("end", NodeType::EndEvent),
];
let edges = vec![EdgeDef {
id: "e1".to_string(),
source: "start".to_string(),
target: "end".to_string(),
condition: Some("amount > 1000".to_string()),
label: Some("高额审批".to_string()),
}];
let graph = FlowGraph::build(&nodes, &edges);
let edge = graph.edges.get("e1").unwrap();
assert_eq!(edge.condition, Some("amount > 1000".to_string()));
assert_eq!(edge.label, Some("高额审批".to_string()));
}
#[test]
fn build_edge_to_unknown_node_still_recorded() {
let nodes = vec![make_node("start", NodeType::StartEvent)];
// edge target "missing" 不在 nodes 中,但 edge 仍被记录
let edges = vec![make_edge("e1", "start", "missing")];
let graph = FlowGraph::build(&nodes, &edges);
assert_eq!(graph.edges.len(), 1);
// outgoing 为 start 有 e1但 incoming["missing"] 不存在
assert_eq!(graph.outgoing.get("start").unwrap().len(), 1);
assert!(graph.incoming.get("missing").is_none());
}
// ---- get_outgoing_edges ----
#[test]
fn outgoing_edges_for_known_node() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("end", NodeType::EndEvent),
];
let edges = vec![make_edge("e1", "start", "end")];
let graph = FlowGraph::build(&nodes, &edges);
let out = graph.get_outgoing_edges("start");
assert_eq!(out.len(), 1);
assert_eq!(out[0].target, "end");
}
#[test]
fn outgoing_edges_for_unknown_node_empty() {
let graph = FlowGraph::build(&[], &[]);
assert!(graph.get_outgoing_edges("nonexistent").is_empty());
}
#[test]
fn outgoing_edges_parallel_gateway_fan_out() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("fork", NodeType::ParallelGateway),
make_node("a", NodeType::UserTask),
make_node("b", NodeType::ServiceTask),
make_node("end", NodeType::EndEvent),
];
let edges = vec![
make_edge("e1", "start", "fork"),
make_edge("e2", "fork", "a"),
make_edge("e3", "fork", "b"),
];
let graph = FlowGraph::build(&nodes, &edges);
let out = graph.get_outgoing_edges("fork");
assert_eq!(out.len(), 2);
let targets: Vec<&str> = out.iter().map(|e| e.target.as_str()).collect();
assert!(targets.contains(&"a"));
assert!(targets.contains(&"b"));
}
// ---- get_incoming_edges ----
#[test]
fn incoming_edges_for_known_node() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("end", NodeType::EndEvent),
];
let edges = vec![make_edge("e1", "start", "end")];
let graph = FlowGraph::build(&nodes, &edges);
let inc = graph.get_incoming_edges("end");
assert_eq!(inc.len(), 1);
assert_eq!(inc[0].source, "start");
}
#[test]
fn incoming_edges_for_unknown_node_empty() {
let graph = FlowGraph::build(&[], &[]);
assert!(graph.get_incoming_edges("nonexistent").is_empty());
}
#[test]
fn incoming_edges_join_gateway() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("a", NodeType::UserTask),
make_node("b", NodeType::ServiceTask),
make_node("join", NodeType::ParallelGateway),
make_node("end", NodeType::EndEvent),
];
let edges = vec![
make_edge("e1", "start", "a"),
make_edge("e2", "start", "b"),
make_edge("e3", "a", "join"),
make_edge("e4", "b", "join"),
make_edge("e5", "join", "end"),
];
let graph = FlowGraph::build(&nodes, &edges);
let inc = graph.get_incoming_edges("join");
assert_eq!(inc.len(), 2);
let sources: Vec<&str> = inc.iter().map(|e| e.source.as_str()).collect();
assert!(sources.contains(&"a"));
assert!(sources.contains(&"b"));
}
// ---- start/end 节点无入/出边 ----
#[test]
fn start_node_has_no_incoming() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("end", NodeType::EndEvent),
];
let edges = vec![make_edge("e1", "start", "end")];
let graph = FlowGraph::build(&nodes, &edges);
assert!(graph.get_incoming_edges("start").is_empty());
assert_eq!(graph.get_outgoing_edges("start").len(), 1);
}
#[test]
fn end_node_has_no_outgoing() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("end", NodeType::EndEvent),
];
let edges = vec![make_edge("e1", "start", "end")];
let graph = FlowGraph::build(&nodes, &edges);
assert!(graph.get_outgoing_edges("end").is_empty());
assert_eq!(graph.get_incoming_edges("end").len(), 1);
}
}