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:
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user