feat: initialize Nuanji (Warm Notes) project

- Base platform from base.git (ERP base: auth, core, config, message, workflow, plugin)
- Created erp-diary module skeleton (lib.rs, dto.rs, error.rs, event.rs, state.rs)
- Integrated erp-diary into workspace and erp-server
- Added DiaryModule registration in main.rs
- Added DiaryState FromRef in state.rs
- Diary routes mounted (empty routes, ready for implementation)
- Product design spec v1.2 preserved in docs/
- Implementation plan preserved in plans/

Cargo check: OK
Cargo test: OK (78+ base tests passing)
This commit is contained in:
iven
2026-05-31 20:52:19 +08:00
commit c539e6fd83
285 changed files with 59156 additions and 0 deletions

View File

@@ -0,0 +1,704 @@
use std::collections::HashMap;
use chrono::Utc;
use sea_orm::{
ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryFilter, Set,
};
use uuid::Uuid;
use crate::dto::NodeType;
use crate::engine::expression::ExpressionEvaluator;
use crate::engine::model::FlowGraph;
use crate::entity::{process_instance, task, token};
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.version = Set(active.version.take().unwrap_or(0) + 1);
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
}
NodeType::ServiceTask => {
// ServiceTask 自动执行 HTTP 调用
let now = Utc::now();
let system_user = uuid::Uuid::nil();
let auto_token_id = Uuid::now_v7();
let token_model = token::ActiveModel {
id: Set(auto_token_id),
tenant_id: Set(tenant_id),
instance_id: Set(instance_id),
node_id: Set(node_id.to_string()),
status: Set("consumed".to_string()),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(system_user),
updated_by: Set(system_user),
deleted_at: Set(None),
version: Set(1),
consumed_at: Set(Some(now)),
};
token_model
.insert(txn)
.await
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
// 执行 HTTP 调用(如果配置了 service_config
let var_name = format!("service_task_{node_id}_result");
let result_value = Self::execute_service_task(node, variables).await;
// 将结果存储为流程变量
Self::set_process_variable(
instance_id,
tenant_id,
&var_name,
&result_value,
txn,
)
.await?;
// 沿出边继续推进
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)
}
_ => {
// UserTask / 网关(分支)等:创建活跃 token
let new_token_id = Uuid::now_v7();
let now = Utc::now();
let system_user = uuid::Uuid::nil();
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),
updated_at: Set(now),
created_by: Set(system_user),
updated_by: Set(system_user),
deleted_at: Set(None),
version: Set(1),
consumed_at: Set(None),
};
token_model
.insert(txn)
.await
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
// UserTask: 同时创建 task 记录
if node.node_type == NodeType::UserTask {
let task_model = task::ActiveModel {
id: Set(Uuid::now_v7()),
tenant_id: Set(tenant_id),
instance_id: Set(instance_id),
token_id: Set(new_token_id),
node_id: Set(node_id.to_string()),
node_name: Set(Some(node.name.clone())),
assignee_id: Set(node.assignee_id),
candidate_groups: Set(node
.candidate_groups
.as_ref()
.map(|g| serde_json::to_value(g).unwrap_or_default())),
status: Set("pending".to_string()),
outcome: Set(None),
form_data: Set(None),
due_date: Set(None),
completed_at: Set(None),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(Uuid::nil()),
updated_by: Set(Uuid::nil()),
deleted_at: Set(None),
version: Set(1),
};
task_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![]);
}
}
// 所有分支都完成了,先将 consumed tokens 标记为 completed 防止并发重复触发
for edge in &incoming {
let consumed_tokens = token::Entity::find()
.filter(token::Column::InstanceId.eq(instance_id))
.filter(token::Column::NodeId.eq(&edge.source))
.filter(token::Column::Status.eq("consumed"))
.all(txn)
.await
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
for t in consumed_tokens {
let ver = t.version;
let mut active: token::ActiveModel = t.into();
active.status = Set("completed".to_string());
active.version = Set(ver + 1);
active.updated_at = Set(chrono::Utc::now());
active
.update(txn)
.await
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
}
}
// 沿出边继续创建新 token
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)
}
/// 执行 ServiceTask HTTP 调用。
///
/// 根据 `service_config` 中的 url/method/body 发起 HTTP 请求。
/// 如果没有配置 `service_config` 或调用失败,返回错误信息 JSON 而不是阻塞流程。
async fn execute_service_task(
node: &crate::engine::model::FlowNode,
variables: &HashMap<String, serde_json::Value>,
) -> serde_json::Value {
let config = match &node.service_config {
Some(c) => c,
None => {
tracing::warn!(
node_id = &node.id,
node_name = %node.name,
"ServiceTask 没有 service_config 配置,跳过 HTTP 调用"
);
return serde_json::json!({
"status": "skipped",
"reason": "未配置 service_config"
});
}
};
let method = config.method.to_uppercase();
let url = &config.url;
tracing::info!(
node_id = &node.id,
node_name = %node.name,
method = %method,
url = %url,
"ServiceTask 开始 HTTP 调用"
);
let client = reqwest::Client::new();
let result = match method.as_str() {
"POST" => {
let body = config.body.as_ref().map(|b| {
// 简单变量替换:${var_name} → variables 中的值
let mut body_str = b.to_string();
for (key, val) in variables {
let placeholder = format!("${{{key}}}");
body_str = body_str.replace(&placeholder, &val.to_string());
}
body_str
});
client.post(url).body(body.unwrap_or_default()).send().await
}
_ => {
// 默认 GET
client.get(url).send().await
}
};
match result {
Ok(resp) => {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
tracing::info!(
node_id = &node.id,
status = status,
"ServiceTask HTTP 调用完成"
);
serde_json::json!({
"status": "success",
"http_status": status,
"body": body,
})
}
Err(e) => {
tracing::warn!(
node_id = &node.id,
error = %e,
"ServiceTask HTTP 调用失败(流程继续推进)"
);
serde_json::json!({
"status": "error",
"error": e.to_string(),
})
}
}
}
/// 将流程变量写入 process_variables 表。
async fn set_process_variable(
instance_id: Uuid,
tenant_id: Uuid,
name: &str,
value: &serde_json::Value,
txn: &impl ConnectionTrait,
) -> WorkflowResult<()> {
use crate::entity::process_variable;
let now = Utc::now();
let system_user = Uuid::nil();
let var_model = process_variable::ActiveModel {
id: Set(Uuid::now_v7()),
tenant_id: Set(tenant_id),
instance_id: Set(instance_id),
name: Set(name.to_string()),
var_type: Set("json".to_string()),
value_string: Set(Some(value.to_string())),
value_number: Set(None),
value_boolean: Set(None),
value_date: Set(None),
created_at: Set(now),
updated_at: Set(now),
created_by: Set(system_user),
updated_by: Set(system_user),
deleted_at: Set(None),
version: Set(1),
};
var_model
.insert(txn)
.await
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
Ok(())
}
/// 检查实例是否所有 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.version = Set(active.version.take().unwrap_or(0) + 1);
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()))?;
// 写入完成事件到 outbox由 relay 广播
let now = Utc::now();
let outbox_event = erp_core::entity::domain_event::ActiveModel {
id: Set(Uuid::now_v7()),
tenant_id: Set(tenant_id),
event_type: Set("process_instance.completed".to_string()),
payload: Set(Some(serde_json::json!({ "instance_id": instance_id }))),
correlation_id: Set(Some(Uuid::now_v7())),
status: Set("pending".to_string()),
attempts: Set(0),
last_error: Set(None),
created_at: Set(now),
published_at: Set(None),
};
outbox_event
.insert(txn)
.await
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
}
Ok(())
}
}
#[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,
}
}
#[test]
fn test_is_join_gateway_with_multiple_incoming() {
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);
assert!(FlowExecutor::is_join_gateway("join", &graph));
}
#[test]
fn test_is_not_join_gateway_single_incoming() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("fork", NodeType::ParallelGateway),
make_node("end", NodeType::EndEvent),
];
let edges = vec![
make_edge("e1", "start", "fork"),
make_edge("e2", "fork", "end"),
];
let graph = FlowGraph::build(&nodes, &edges);
assert!(!FlowExecutor::is_join_gateway("fork", &graph));
}
#[test]
fn test_is_not_join_gateway_for_nonexistent_node() {
let graph = FlowGraph::build(&[], &[]);
assert!(!FlowExecutor::is_join_gateway("nonexistent", &graph));
}
}

View File

@@ -0,0 +1,431 @@
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>()
&& 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());
}
// ---- 扩展边界测试 ----
#[test]
fn test_less_or_equal() {
let vars = make_vars();
assert!(ExpressionEvaluator::eval("amount <= 1500", &vars).unwrap());
assert!(ExpressionEvaluator::eval("amount <= 2000", &vars).unwrap());
assert!(!ExpressionEvaluator::eval("amount <= 1000", &vars).unwrap());
}
#[test]
fn test_boolean_equality() {
let vars = make_vars();
assert!(ExpressionEvaluator::eval("active == true", &vars).unwrap());
assert!(!ExpressionEvaluator::eval("active == false", &vars).unwrap());
}
#[test]
fn test_string_literal_with_single_quotes() {
let vars = make_vars();
assert!(ExpressionEvaluator::eval("status == 'approved'", &vars).unwrap());
}
#[test]
fn test_float_comparison() {
let mut vars = HashMap::new();
vars.insert("temperature".to_string(), json!(36.5));
assert!(ExpressionEvaluator::eval("temperature >= 36.0", &vars).unwrap());
assert!(ExpressionEvaluator::eval("temperature < 37.0", &vars).unwrap());
assert!(!ExpressionEvaluator::eval("temperature > 37.5", &vars).unwrap());
}
#[test]
fn test_integer_float_cross_comparison() {
let mut vars = HashMap::new();
vars.insert("val".to_string(), json!(10));
assert!(ExpressionEvaluator::eval("val == 10.0", &vars).unwrap());
assert!(ExpressionEvaluator::eval("val >= 9.5", &vars).unwrap());
}
#[test]
fn test_string_comparison_lexicographic() {
let vars = make_vars();
// name = "Alice"
assert!(!ExpressionEvaluator::eval("name > \"Alice\"", &vars).unwrap());
assert!(ExpressionEvaluator::eval("name >= \"Alice\"", &vars).unwrap());
assert!(ExpressionEvaluator::eval("name < \"Bob\"", &vars).unwrap());
assert!(ExpressionEvaluator::eval("name == \"Alice\"", &vars).unwrap());
}
#[test]
fn test_compound_and_or() {
let vars = make_vars();
// amount > 1000 (true) && score > 80 (true) || amount > 3000 (false)
// OR 先绑定,所以是 amount>1000 && score>80 (true) || amount>3000 (false)
// 注意:当前实现从左到右先匹配 ||,所以实际解析为:
// amount > 1000 && score > 80 || amount > 3000
// find_logical_op 先找 || → split at ||
// left = "amount > 1000 && score > 80", right = "amount > 3000"
// left = true, right = false → true || false = true
assert!(
ExpressionEvaluator::eval("amount > 1000 && score > 80 || amount > 3000", &vars)
.unwrap()
);
}
#[test]
fn test_compound_all_false() {
let vars = make_vars();
assert!(!ExpressionEvaluator::eval("amount > 2000 && score > 90", &vars).unwrap());
}
#[test]
fn test_number_not_equals() {
let vars = make_vars();
assert!(ExpressionEvaluator::eval("amount != 1000", &vars).unwrap());
assert!(!ExpressionEvaluator::eval("amount != 1500", &vars).unwrap());
}
#[test]
fn test_expression_with_extra_whitespace() {
let vars = make_vars();
assert!(ExpressionEvaluator::eval(" amount > 1000 ", &vars).unwrap());
}
#[test]
fn test_unresolvable_variable_returns_error() {
let vars = HashMap::new();
let result = ExpressionEvaluator::eval("missing_var > 10", &vars);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("未知的变量"));
}
#[test]
fn test_empty_expression_returns_error() {
let vars = HashMap::new();
let result = ExpressionEvaluator::eval("", &vars);
assert!(result.is_err());
}
}

View File

@@ -0,0 +1,5 @@
pub mod executor;
pub mod expression;
pub mod model;
pub mod parser;
pub mod timeout;

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

View File

@@ -0,0 +1,491 @@
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());
}
}

View File

@@ -0,0 +1,77 @@
// 超时检查框架
//
// TimeoutChecker 定期扫描 tasks 表中已超时但仍处于 pending 状态的任务,
// 发布 task.timeout 事件用于升级通知。
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())
}
/// 查询所有租户中已超时但未完成的任务列表。
///
/// 返回 due_date < now 且 status = 'pending' 的任务 ID。
/// 用于后台定时任务的全量扫描。
pub async fn find_all_overdue_tasks(
db: &sea_orm::DatabaseConnection,
) -> WorkflowResult<Vec<Uuid>> {
let now = Utc::now();
let overdue = task::Entity::find()
.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())
}
/// 查询所有租户中已超时的任务(含详细信息)。
///
/// 返回 (task_id, tenant_id, instance_id, assignee_id) 元组,
/// 用于发布 task.timeout 事件。
pub async fn find_all_overdue_tasks_with_details(
db: &sea_orm::DatabaseConnection,
) -> WorkflowResult<Vec<(Uuid, Uuid, Uuid, Option<Uuid>)>> {
let now = Utc::now();
let overdue = task::Entity::find()
.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, t.tenant_id, t.instance_id, t.assignee_id))
.collect())
}
}