feat(workflow): add workflow engine module (Phase 4)
Implement complete workflow engine with BPMN subset support: Backend (erp-workflow crate): - Token-driven execution engine with exclusive/parallel gateway support - BPMN parser with flow graph validation - Expression evaluator for conditional branching - Process definition CRUD with draft/publish lifecycle - Process instance management (start, suspend, terminate) - Task service (pending, complete, delegate) - PostgreSQL advisory locks for concurrent safety - 5 database tables: process_definitions, process_instances, tokens, tasks, process_variables - 13 API endpoints with RBAC protection - Timeout checker framework (placeholder) Frontend: - Workflow page with 4 tabs (definitions, pending, completed, monitor) - React Flow visual process designer (@xyflow/react) - Process viewer with active node highlighting - 3 API client modules for workflow endpoints - Sidebar menu integration
This commit is contained in:
371
crates/erp-workflow/src/engine/executor.rs
Normal file
371
crates/erp-workflow/src/engine/executor.rs
Normal file
@@ -0,0 +1,371 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::Utc;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, ConnectionTrait,
|
||||
PaginatorTrait,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::dto::NodeType;
|
||||
use crate::engine::expression::ExpressionEvaluator;
|
||||
use crate::engine::model::FlowGraph;
|
||||
use crate::entity::{token, process_instance};
|
||||
use crate::error::{WorkflowError, WorkflowResult};
|
||||
|
||||
/// Token 驱动的流程执行引擎。
|
||||
///
|
||||
/// 核心职责:
|
||||
/// - 在流程启动时,于 StartEvent 创建第一个 token
|
||||
/// - 在任务完成时推进 token 到下一个节点
|
||||
/// - 处理网关分支/汇合逻辑
|
||||
/// - 在 EndEvent 完成实例
|
||||
pub struct FlowExecutor;
|
||||
|
||||
impl FlowExecutor {
|
||||
/// 启动流程:在 StartEvent 的后继节点创建 token。
|
||||
///
|
||||
/// 返回创建的 token ID 列表。
|
||||
pub async fn start(
|
||||
instance_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
graph: &FlowGraph,
|
||||
variables: &HashMap<String, serde_json::Value>,
|
||||
txn: &impl ConnectionTrait,
|
||||
) -> WorkflowResult<Vec<Uuid>> {
|
||||
let start_id = graph
|
||||
.start_node_id
|
||||
.as_ref()
|
||||
.ok_or_else(|| WorkflowError::InvalidDiagram("流程图没有开始事件".to_string()))?;
|
||||
|
||||
// 获取 StartEvent 的出边,推进到后继节点
|
||||
let outgoing = graph.get_outgoing_edges(start_id);
|
||||
if outgoing.is_empty() {
|
||||
return Err(WorkflowError::InvalidDiagram(
|
||||
"开始事件没有出边".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// StartEvent 只有一条出边
|
||||
let first_edge = &outgoing[0];
|
||||
let target_node_id = &first_edge.target;
|
||||
|
||||
Self::create_token_at_node(
|
||||
instance_id,
|
||||
tenant_id,
|
||||
target_node_id,
|
||||
graph,
|
||||
variables,
|
||||
txn,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 推进 token:消费当前 token,在下一节点创建新 token。
|
||||
///
|
||||
/// 返回新创建的 token ID 列表。
|
||||
pub async fn advance(
|
||||
token_id: Uuid,
|
||||
instance_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
graph: &FlowGraph,
|
||||
variables: &HashMap<String, serde_json::Value>,
|
||||
txn: &impl ConnectionTrait,
|
||||
) -> WorkflowResult<Vec<Uuid>> {
|
||||
// 读取当前 token
|
||||
let current_token = token::Entity::find_by_id(token_id)
|
||||
.one(txn)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?
|
||||
.ok_or_else(|| WorkflowError::NotFound(format!("Token 不存在: {token_id}")))?;
|
||||
|
||||
if current_token.status != "active" {
|
||||
return Err(WorkflowError::InvalidState(format!(
|
||||
"Token 状态不是 active: {}",
|
||||
current_token.status
|
||||
)));
|
||||
}
|
||||
|
||||
let node_id = current_token.node_id.clone();
|
||||
|
||||
// 消费当前 token
|
||||
let mut active: token::ActiveModel = current_token.into();
|
||||
active.status = Set("consumed".to_string());
|
||||
active.consumed_at = Set(Some(Utc::now()));
|
||||
active.update(txn).await.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||||
|
||||
// 获取当前节点的出边
|
||||
let outgoing = graph.get_outgoing_edges(&node_id);
|
||||
let current_node = graph.nodes.get(&node_id)
|
||||
.ok_or_else(|| WorkflowError::InvalidDiagram(format!("节点不存在: {node_id}")))?;
|
||||
|
||||
match current_node.node_type {
|
||||
NodeType::ExclusiveGateway => {
|
||||
// 排他网关:求值条件,选择一条分支
|
||||
Self::advance_exclusive_gateway(
|
||||
instance_id,
|
||||
tenant_id,
|
||||
&outgoing,
|
||||
graph,
|
||||
variables,
|
||||
txn,
|
||||
)
|
||||
.await
|
||||
}
|
||||
NodeType::ParallelGateway => {
|
||||
// 并行网关:为每条出边创建 token
|
||||
Self::advance_parallel_gateway(
|
||||
instance_id,
|
||||
tenant_id,
|
||||
&outgoing,
|
||||
graph,
|
||||
variables,
|
||||
txn,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
// 普通节点:沿出边前进
|
||||
if outgoing.is_empty() {
|
||||
// 没有出边(理论上只有 EndEvent 会到这里)
|
||||
Ok(vec![])
|
||||
} else {
|
||||
let mut new_tokens = Vec::new();
|
||||
for edge in &outgoing {
|
||||
let tokens = Self::create_token_at_node(
|
||||
instance_id,
|
||||
tenant_id,
|
||||
&edge.target,
|
||||
graph,
|
||||
variables,
|
||||
txn,
|
||||
)
|
||||
.await?;
|
||||
new_tokens.extend(tokens);
|
||||
}
|
||||
Ok(new_tokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 排他网关分支:求值条件,选择第一个满足条件的分支。
|
||||
async fn advance_exclusive_gateway(
|
||||
instance_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
outgoing: &[&crate::engine::model::FlowEdge],
|
||||
graph: &FlowGraph,
|
||||
variables: &HashMap<String, serde_json::Value>,
|
||||
txn: &impl ConnectionTrait,
|
||||
) -> WorkflowResult<Vec<Uuid>> {
|
||||
let mut default_target: Option<&str> = None;
|
||||
let mut matched_target: Option<&str> = None;
|
||||
|
||||
for edge in outgoing {
|
||||
if let Some(condition) = &edge.condition {
|
||||
match ExpressionEvaluator::eval(condition, variables) {
|
||||
Ok(true) => {
|
||||
matched_target = Some(&edge.target);
|
||||
break;
|
||||
}
|
||||
Ok(false) => continue,
|
||||
Err(_) => continue, // 条件求值失败,跳过
|
||||
}
|
||||
} else {
|
||||
// 无条件的边作为默认分支
|
||||
default_target = Some(&edge.target);
|
||||
}
|
||||
}
|
||||
|
||||
let target = matched_target
|
||||
.or(default_target)
|
||||
.ok_or_else(|| WorkflowError::ExpressionError(
|
||||
"排他网关没有匹配的条件分支".to_string(),
|
||||
))?;
|
||||
|
||||
Self::create_token_at_node(instance_id, tenant_id, target, graph, variables, txn).await
|
||||
}
|
||||
|
||||
/// 并行网关分支:为每条出边创建 token。
|
||||
async fn advance_parallel_gateway(
|
||||
instance_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
outgoing: &[&crate::engine::model::FlowEdge],
|
||||
graph: &FlowGraph,
|
||||
variables: &HashMap<String, serde_json::Value>,
|
||||
txn: &impl ConnectionTrait,
|
||||
) -> WorkflowResult<Vec<Uuid>> {
|
||||
let mut new_tokens = Vec::new();
|
||||
for edge in outgoing {
|
||||
let tokens = Self::create_token_at_node(
|
||||
instance_id,
|
||||
tenant_id,
|
||||
&edge.target,
|
||||
graph,
|
||||
variables,
|
||||
txn,
|
||||
)
|
||||
.await?;
|
||||
new_tokens.extend(tokens);
|
||||
}
|
||||
Ok(new_tokens)
|
||||
}
|
||||
|
||||
/// 在指定节点创建 token,并根据节点类型执行相应逻辑。
|
||||
fn create_token_at_node<'a>(
|
||||
instance_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
node_id: &'a str,
|
||||
graph: &'a FlowGraph,
|
||||
variables: &'a HashMap<String, serde_json::Value>,
|
||||
txn: &'a impl ConnectionTrait,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = WorkflowResult<Vec<Uuid>>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
let node = graph.nodes.get(node_id)
|
||||
.ok_or_else(|| WorkflowError::InvalidDiagram(format!("节点不存在: {node_id}")))?;
|
||||
|
||||
match node.node_type {
|
||||
NodeType::EndEvent => {
|
||||
// 到达 EndEvent,不创建新 token
|
||||
// 检查实例是否所有 token 都完成
|
||||
Self::check_instance_completion(instance_id, tenant_id, txn).await?;
|
||||
Ok(vec![])
|
||||
}
|
||||
NodeType::ParallelGateway
|
||||
if Self::is_join_gateway(node_id, graph) =>
|
||||
{
|
||||
// 并行网关汇合:等待所有入边 token 到达
|
||||
Self::handle_join_gateway(
|
||||
instance_id,
|
||||
tenant_id,
|
||||
node_id,
|
||||
graph,
|
||||
variables,
|
||||
txn,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
// UserTask / ServiceTask / 网关(分支)等:创建活跃 token
|
||||
let new_token_id = Uuid::now_v7();
|
||||
let now = Utc::now();
|
||||
|
||||
let token_model = token::ActiveModel {
|
||||
id: Set(new_token_id),
|
||||
tenant_id: Set(tenant_id),
|
||||
instance_id: Set(instance_id),
|
||||
node_id: Set(node_id.to_string()),
|
||||
status: Set("active".to_string()),
|
||||
created_at: Set(now),
|
||||
consumed_at: Set(None),
|
||||
};
|
||||
token_model
|
||||
.insert(txn)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||||
|
||||
Ok(vec![new_token_id])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 判断并行网关是否是汇合模式(入边数 > 出边数,或者入边数 > 1)。
|
||||
fn is_join_gateway(node_id: &str, graph: &FlowGraph) -> bool {
|
||||
let incoming = graph.get_incoming_edges(node_id);
|
||||
incoming.len() > 1
|
||||
}
|
||||
|
||||
/// 处理并行网关汇合逻辑。
|
||||
///
|
||||
/// 当所有入边的源节点都有已消费的 token 时,创建新 token 推进到后继。
|
||||
async fn handle_join_gateway(
|
||||
instance_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
node_id: &str,
|
||||
graph: &FlowGraph,
|
||||
variables: &HashMap<String, serde_json::Value>,
|
||||
txn: &impl ConnectionTrait,
|
||||
) -> WorkflowResult<Vec<Uuid>> {
|
||||
let incoming = graph.get_incoming_edges(node_id);
|
||||
|
||||
// 检查所有入边的源节点是否都有已消费/已完成的 token
|
||||
for edge in &incoming {
|
||||
let has_consumed = token::Entity::find()
|
||||
.filter(token::Column::InstanceId.eq(instance_id))
|
||||
.filter(token::Column::NodeId.eq(&edge.source))
|
||||
.filter(token::Column::Status.is_in(["consumed", "active"]))
|
||||
.one(txn)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||||
|
||||
if has_consumed.is_none() {
|
||||
// 还有分支没有到达,等待
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
// 检查是否还有活跃的 token(来自其他分支)
|
||||
let has_active = token::Entity::find()
|
||||
.filter(token::Column::InstanceId.eq(instance_id))
|
||||
.filter(token::Column::NodeId.eq(&edge.source))
|
||||
.filter(token::Column::Status.eq("active"))
|
||||
.one(txn)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||||
|
||||
if has_active.is_some() {
|
||||
// 还有分支在执行中,等待
|
||||
return Ok(vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
// 所有分支都完成了,沿出边继续
|
||||
let outgoing = graph.get_outgoing_edges(node_id);
|
||||
let mut new_tokens = Vec::new();
|
||||
for edge in &outgoing {
|
||||
let tokens = Self::create_token_at_node(
|
||||
instance_id,
|
||||
tenant_id,
|
||||
&edge.target,
|
||||
graph,
|
||||
variables,
|
||||
txn,
|
||||
)
|
||||
.await?;
|
||||
new_tokens.extend(tokens);
|
||||
}
|
||||
Ok(new_tokens)
|
||||
}
|
||||
|
||||
/// 检查实例是否所有 token 都已完成,如果是则完成实例。
|
||||
async fn check_instance_completion(
|
||||
instance_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
txn: &impl ConnectionTrait,
|
||||
) -> WorkflowResult<()> {
|
||||
let active_count = token::Entity::find()
|
||||
.filter(token::Column::InstanceId.eq(instance_id))
|
||||
.filter(token::Column::Status.eq("active"))
|
||||
.count(txn)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||||
|
||||
if active_count == 0 {
|
||||
// 所有 token 都完成,标记实例完成
|
||||
let instance = process_instance::Entity::find_by_id(instance_id)
|
||||
.one(txn)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?
|
||||
.filter(|i| i.tenant_id == tenant_id && i.deleted_at.is_none())
|
||||
.ok_or_else(|| WorkflowError::NotFound(format!("流程实例不存在: {instance_id}")))?;
|
||||
|
||||
let mut active: process_instance::ActiveModel = instance.into();
|
||||
active.status = Set("completed".to_string());
|
||||
active.completed_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.update(txn).await.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
325
crates/erp-workflow/src/engine/expression.rs
Normal file
325
crates/erp-workflow/src/engine/expression.rs
Normal file
@@ -0,0 +1,325 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::error::{WorkflowError, WorkflowResult};
|
||||
|
||||
/// 简单表达式求值器。
|
||||
///
|
||||
/// 支持的比较运算符:>, >=, <, <=, ==, !=
|
||||
/// 支持 && 和 || 逻辑运算。
|
||||
/// 操作数可以是变量名(从 variables map 查找)或字面量(数字、字符串)。
|
||||
///
|
||||
/// 示例:
|
||||
/// - `amount > 1000`
|
||||
/// - `status == "approved"`
|
||||
/// - `score >= 60 && attendance > 80`
|
||||
pub struct ExpressionEvaluator;
|
||||
|
||||
impl ExpressionEvaluator {
|
||||
/// 求值单个条件表达式。
|
||||
///
|
||||
/// 表达式格式: `{left} {op} {right}` 或复合表达式 `{expr1} && {expr2}`
|
||||
pub fn eval(expr: &str, variables: &HashMap<String, serde_json::Value>) -> WorkflowResult<bool> {
|
||||
let expr = expr.trim();
|
||||
|
||||
// 处理逻辑 OR
|
||||
if let Some(idx) = Self::find_logical_op(expr, "||") {
|
||||
let left = &expr[..idx];
|
||||
let right = &expr[idx + 2..];
|
||||
return Ok(Self::eval(left, variables)? || Self::eval(right, variables)?);
|
||||
}
|
||||
|
||||
// 处理逻辑 AND
|
||||
if let Some(idx) = Self::find_logical_op(expr, "&&") {
|
||||
let left = &expr[..idx];
|
||||
let right = &expr[idx + 2..];
|
||||
return Ok(Self::eval(left, variables)? && Self::eval(right, variables)?);
|
||||
}
|
||||
|
||||
// 处理单个比较表达式
|
||||
Self::eval_comparison(expr, variables)
|
||||
}
|
||||
|
||||
/// 查找逻辑运算符位置,跳过引号内的内容。
|
||||
fn find_logical_op(expr: &str, op: &str) -> Option<usize> {
|
||||
let mut in_string = false;
|
||||
let mut string_char = ' ';
|
||||
let chars: Vec<char> = expr.chars().collect();
|
||||
let op_chars: Vec<char> = op.chars().collect();
|
||||
let op_len = op_chars.len();
|
||||
|
||||
for i in 0..chars.len().saturating_sub(op_len - 1) {
|
||||
let c = chars[i];
|
||||
|
||||
if !in_string && (c == '"' || c == '\'') {
|
||||
in_string = true;
|
||||
string_char = c;
|
||||
continue;
|
||||
}
|
||||
if in_string && c == string_char {
|
||||
in_string = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if in_string {
|
||||
continue;
|
||||
}
|
||||
|
||||
if chars[i..].starts_with(&op_chars) {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 求值单个比较表达式。
|
||||
fn eval_comparison(expr: &str, variables: &HashMap<String, serde_json::Value>) -> WorkflowResult<bool> {
|
||||
let operators = [">=", "<=", "!=", "==", ">", "<"];
|
||||
|
||||
for op in &operators {
|
||||
if let Some(idx) = Self::find_comparison_op(expr, op) {
|
||||
let left = expr[..idx].trim();
|
||||
let right = expr[idx + op.len()..].trim();
|
||||
|
||||
let left_val = Self::resolve_value(left, variables)?;
|
||||
let right_val = Self::resolve_value(right, variables)?;
|
||||
|
||||
return Self::compare(&left_val, &right_val, op);
|
||||
}
|
||||
}
|
||||
|
||||
Err(WorkflowError::ExpressionError(format!(
|
||||
"无法解析表达式: '{}'",
|
||||
expr
|
||||
)))
|
||||
}
|
||||
|
||||
/// 查找比较运算符位置,跳过引号内的内容。
|
||||
fn find_comparison_op(expr: &str, op: &str) -> Option<usize> {
|
||||
let mut in_string = false;
|
||||
let mut string_char = ' ';
|
||||
let bytes = expr.as_bytes();
|
||||
let op_bytes = op.as_bytes();
|
||||
let op_len = op_bytes.len();
|
||||
|
||||
for i in 0..bytes.len().saturating_sub(op_len - 1) {
|
||||
let c = bytes[i] as char;
|
||||
|
||||
if !in_string && (c == '"' || c == '\'') {
|
||||
in_string = true;
|
||||
string_char = c;
|
||||
continue;
|
||||
}
|
||||
if in_string && c == string_char {
|
||||
in_string = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if in_string {
|
||||
continue;
|
||||
}
|
||||
|
||||
if bytes[i..].starts_with(op_bytes) {
|
||||
// 确保不是被嵌在其他运算符里(如 != 中的 =)
|
||||
// 对于 > 和 < 检查后面不是 = 或 >
|
||||
if op == ">" || op == "<" {
|
||||
if i + op_len < bytes.len() {
|
||||
let next = bytes[i + op_len] as char;
|
||||
if next == '=' || (op == ">" && next == '>') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// 也检查前面不是 ! 或 = 或 < 或 >
|
||||
if i > 0 {
|
||||
let prev = bytes[i - 1] as char;
|
||||
if prev == '!' || prev == '=' || prev == '<' || prev == '>' {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 对于 ==, >=, <=, != 确保前面不是 ! 或 = (避免匹配到 == 中的第二个 =)
|
||||
// 这已经通过从长到短匹配处理了
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 解析值:字符串字面量、数字字面量或变量引用。
|
||||
fn resolve_value(
|
||||
token: &str,
|
||||
variables: &HashMap<String, serde_json::Value>,
|
||||
) -> WorkflowResult<serde_json::Value> {
|
||||
let token = token.trim();
|
||||
|
||||
// 字符串字面量
|
||||
if (token.starts_with('"') && token.ends_with('"'))
|
||||
|| (token.starts_with('\'') && token.ends_with('\''))
|
||||
{
|
||||
return Ok(serde_json::Value::String(
|
||||
token[1..token.len() - 1].to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// 数字字面量
|
||||
if let Ok(n) = token.parse::<i64>() {
|
||||
return Ok(serde_json::Value::Number(n.into()));
|
||||
}
|
||||
if let Ok(f) = token.parse::<f64>() {
|
||||
if let Some(n) = serde_json::Number::from_f64(f) {
|
||||
return Ok(serde_json::Value::Number(n));
|
||||
}
|
||||
}
|
||||
|
||||
// 布尔字面量
|
||||
if token == "true" {
|
||||
return Ok(serde_json::Value::Bool(true));
|
||||
}
|
||||
if token == "false" {
|
||||
return Ok(serde_json::Value::Bool(false));
|
||||
}
|
||||
|
||||
// 变量引用
|
||||
if let Some(val) = variables.get(token) {
|
||||
return Ok(val.clone());
|
||||
}
|
||||
|
||||
Err(WorkflowError::ExpressionError(format!(
|
||||
"未知的变量或值: '{}'",
|
||||
token
|
||||
)))
|
||||
}
|
||||
|
||||
/// 比较两个 JSON 值。
|
||||
fn compare(
|
||||
left: &serde_json::Value,
|
||||
right: &serde_json::Value,
|
||||
op: &str,
|
||||
) -> WorkflowResult<bool> {
|
||||
match op {
|
||||
"==" => Ok(Self::values_equal(left, right)),
|
||||
"!=" => Ok(!Self::values_equal(left, right)),
|
||||
">" => Ok(Self::values_compare(left, right)? == std::cmp::Ordering::Greater),
|
||||
">=" => Ok(Self::values_compare(left, right)? != std::cmp::Ordering::Less),
|
||||
"<" => Ok(Self::values_compare(left, right)? == std::cmp::Ordering::Less),
|
||||
"<=" => Ok(Self::values_compare(left, right)? != std::cmp::Ordering::Greater),
|
||||
_ => Err(WorkflowError::ExpressionError(format!(
|
||||
"不支持的比较运算符: '{}'",
|
||||
op
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn values_equal(left: &serde_json::Value, right: &serde_json::Value) -> bool {
|
||||
// 数值比较:允许整数和浮点数互比
|
||||
if left.is_number() && right.is_number() {
|
||||
return left.as_f64() == right.as_f64();
|
||||
}
|
||||
left == right
|
||||
}
|
||||
|
||||
fn values_compare(
|
||||
left: &serde_json::Value,
|
||||
right: &serde_json::Value,
|
||||
) -> WorkflowResult<std::cmp::Ordering> {
|
||||
if left.is_number() && right.is_number() {
|
||||
let l = left.as_f64().unwrap_or(0.0);
|
||||
let r = right.as_f64().unwrap_or(0.0);
|
||||
return Ok(l.partial_cmp(&r).unwrap_or(std::cmp::Ordering::Equal));
|
||||
}
|
||||
|
||||
if let (Some(l), Some(r)) = (left.as_str(), right.as_str()) {
|
||||
return Ok(l.cmp(r));
|
||||
}
|
||||
|
||||
Err(WorkflowError::ExpressionError(format!(
|
||||
"无法比较 {:?} 和 {:?}",
|
||||
left, right
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn make_vars() -> HashMap<String, serde_json::Value> {
|
||||
let mut m = HashMap::new();
|
||||
m.insert("amount".to_string(), json!(1500));
|
||||
m.insert("status".to_string(), json!("approved"));
|
||||
m.insert("score".to_string(), json!(85));
|
||||
m.insert("name".to_string(), json!("Alice"));
|
||||
m.insert("active".to_string(), json!(true));
|
||||
m
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_greater_than() {
|
||||
let vars = make_vars();
|
||||
assert!(ExpressionEvaluator::eval("amount > 1000", &vars).unwrap());
|
||||
assert!(!ExpressionEvaluator::eval("amount > 2000", &vars).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_less_than() {
|
||||
let vars = make_vars();
|
||||
assert!(ExpressionEvaluator::eval("amount < 2000", &vars).unwrap());
|
||||
assert!(!ExpressionEvaluator::eval("amount < 1000", &vars).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_equals() {
|
||||
let vars = make_vars();
|
||||
assert!(ExpressionEvaluator::eval("amount == 1500", &vars).unwrap());
|
||||
assert!(!ExpressionEvaluator::eval("amount == 1000", &vars).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_equals() {
|
||||
let vars = make_vars();
|
||||
assert!(ExpressionEvaluator::eval("status == \"approved\"", &vars).unwrap());
|
||||
assert!(!ExpressionEvaluator::eval("status == \"rejected\"", &vars).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_not_equals() {
|
||||
let vars = make_vars();
|
||||
assert!(ExpressionEvaluator::eval("status != \"rejected\"", &vars).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_greater_or_equal() {
|
||||
let vars = make_vars();
|
||||
assert!(ExpressionEvaluator::eval("amount >= 1500", &vars).unwrap());
|
||||
assert!(ExpressionEvaluator::eval("amount >= 1000", &vars).unwrap());
|
||||
assert!(!ExpressionEvaluator::eval("amount >= 2000", &vars).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logical_and() {
|
||||
let vars = make_vars();
|
||||
assert!(ExpressionEvaluator::eval("amount > 1000 && score > 80", &vars).unwrap());
|
||||
assert!(!ExpressionEvaluator::eval("amount > 2000 && score > 80", &vars).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logical_or() {
|
||||
let vars = make_vars();
|
||||
assert!(ExpressionEvaluator::eval("amount > 2000 || score > 80", &vars).unwrap());
|
||||
assert!(!ExpressionEvaluator::eval("amount > 2000 || score > 90", &vars).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_variable() {
|
||||
let vars = make_vars();
|
||||
let result = ExpressionEvaluator::eval("unknown > 0", &vars);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_expression() {
|
||||
let vars = make_vars();
|
||||
let result = ExpressionEvaluator::eval("justavariable", &vars);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
5
crates/erp-workflow/src/engine/mod.rs
Normal file
5
crates/erp-workflow/src/engine/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod expression;
|
||||
pub mod executor;
|
||||
pub mod model;
|
||||
pub mod parser;
|
||||
pub mod timeout;
|
||||
122
crates/erp-workflow/src/engine/model.rs
Normal file
122
crates/erp-workflow/src/engine/model.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
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>,
|
||||
}
|
||||
|
||||
/// 内存中的边模型。
|
||||
#[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(),
|
||||
};
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
258
crates/erp-workflow/src/engine/parser.rs
Normal file
258
crates/erp-workflow/src/engine/parser.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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());
|
||||
}
|
||||
}
|
||||
36
crates/erp-workflow/src/engine/timeout.rs
Normal file
36
crates/erp-workflow/src/engine/timeout.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
// 超时检查框架 — 占位实现
|
||||
//
|
||||
// 当前版本仅提供接口定义,实际超时检查逻辑将在后续迭代中实现。
|
||||
// Task 表的 due_date 字段已支持设置超时时间。
|
||||
|
||||
use chrono::Utc;
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::entity::task;
|
||||
use crate::error::WorkflowResult;
|
||||
|
||||
/// 超时检查服务(占位)。
|
||||
pub struct TimeoutChecker;
|
||||
|
||||
impl TimeoutChecker {
|
||||
/// 查询已超时但未完成的任务列表。
|
||||
///
|
||||
/// 返回 due_date < now 且 status = 'pending' 的任务 ID。
|
||||
pub async fn find_overdue_tasks(
|
||||
tenant_id: Uuid,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> WorkflowResult<Vec<Uuid>> {
|
||||
let now = Utc::now();
|
||||
let overdue = task::Entity::find()
|
||||
.filter(task::Column::TenantId.eq(tenant_id))
|
||||
.filter(task::Column::Status.eq("pending"))
|
||||
.filter(task::Column::DueDate.lt(now))
|
||||
.filter(task::Column::DeletedAt.is_null())
|
||||
.all(db)
|
||||
.await
|
||||
.map_err(|e| crate::error::WorkflowError::Validation(e.to_string()))?;
|
||||
|
||||
Ok(overdue.iter().map(|t| t.id).collect())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user