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:
iven
2026-04-11 09:54:02 +08:00
parent 0cbd08eb78
commit 91ecaa3ed7
51 changed files with 4826 additions and 12 deletions

View 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(())
}
}