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
This commit is contained in:
385
crates/erp-workflow/src/engine/model.rs
Normal file
385
crates/erp-workflow/src/engine/model.rs
Normal file
@@ -0,0 +1,385 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user