Files
udmin/backend/src/flow/executors/condition.rs
ayou 75c6974a35 feat(flow): 新增分组执行与异步模式支持
refactor(executors): 将 Rhai 引擎评估逻辑迁移至 script_rhai 模块
docs: 添加 Flow 架构文档与示例 JSON
feat(i18n): 新增前端多语言支持
perf(axios): 优化 token 刷新与 401 处理逻辑
style: 统一代码格式化与简化条件判断
2025-12-03 20:51:22 +08:00

217 lines
8.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// third-party
use anyhow::Result;
use serde_json::Value as V;
use tracing::info;
// 业务函数
pub(crate) fn eval_condition_json(ctx: &serde_json::Value, cond: &serde_json::Value) -> Result<bool> {
// 新增:若 cond 为数组,按 AND 语义评估(全部为 true 才为 true
if let Some(arr) = cond.as_array() {
let mut all_true = true;
for (idx, item) in arr.iter().enumerate() {
let ok = eval_condition_json(ctx, item)?;
info!(target = "udmin.flow", index = idx, result = %ok, "condition group item (AND)");
if !ok { all_true = false; }
}
info!(target = "udmin.flow", count = arr.len(), result = %all_true, "condition group evaluated (AND)");
return Ok(all_true);
}
// 支持前端 Condition 组件导出的: { left:{type, content}, operator, right? }
let left = cond.get("left").ok_or_else(|| anyhow::anyhow!("missing left"))?;
let op_raw = cond.get("operator").and_then(|v| v.as_str()).unwrap_or("");
let right_raw = cond.get("right");
// 解析弱等于标记:当右值 schema.extra.weak 为 true 时,对字符串比较采用忽略大小写与首尾空白的弱等于
let weak_eq = right_raw
.and_then(|r| r.get("schema"))
.and_then(|s| s.get("extra"))
.and_then(|e| e.get("weak"))
.and_then(|b| b.as_bool())
.unwrap_or(false);
let lval = resolve_value(ctx, left)?;
let rval = match right_raw { Some(v) => Some(resolve_value(ctx, v)?), None => None };
// 归一化操作符:忽略大小写,替换下划线为空格
let op = op_raw.trim().to_lowercase().replace('_', " ");
// 工具函数
fn to_f64(v: &V) -> Option<f64> {
match v {
V::Number(n) => n.as_f64(),
V::String(s) => s.parse::<f64>().ok(),
_ => None,
}
}
fn is_empty_val(v: &V) -> bool {
match v {
V::Null => true,
V::String(s) => s.trim().is_empty(),
V::Array(a) => a.is_empty(),
V::Object(m) => m.is_empty(),
_ => false,
}
}
fn norm_str(s: &str) -> String { s.trim().to_lowercase() }
fn json_equal(a: &V, b: &V, weak: bool) -> bool {
match (a, b) {
// 数字:做宽松比较(字符串转数字)
(V::Number(_), V::Number(_)) | (V::Number(_), V::String(_)) | (V::String(_), V::Number(_)) => {
match (to_f64(a), to_f64(b)) { (Some(x), Some(y)) => x == y, _ => a == b }
}
// 字符串:若 weak 则忽略大小写与首尾空白
(V::String(sa), V::String(sb)) if weak => norm_str(sa) == norm_str(sb),
_ => a == b,
}
}
fn contains(left: &V, right: &V, weak: bool) -> bool {
match (left, right) {
(V::String(s), V::String(t)) => {
if weak { norm_str(s).contains(&norm_str(t)) } else { s.contains(t) }
}
(V::Array(arr), r) => arr.iter().any(|x| json_equal(x, r, weak)),
(V::Object(map), V::String(key)) => {
if weak { map.keys().any(|k| norm_str(k) == norm_str(key)) } else { map.contains_key(key) }
}
_ => false,
}
}
fn in_op(left: &V, right: &V, weak: bool) -> bool {
match right {
V::Array(arr) => arr.iter().any(|x| json_equal(left, x, weak)),
V::Object(map) => match left { V::String(k) => {
if weak { map.keys().any(|kk| norm_str(kk) == norm_str(k)) } else { map.contains_key(k) }
}, _ => false },
V::String(hay) => match left { V::String(needle) => {
if weak { norm_str(hay).contains(&norm_str(needle)) } else { hay.contains(needle) }
}, _ => false },
_ => false,
}
}
fn bool_like(v: &V) -> bool {
match v {
V::Bool(b) => *b,
V::Null => false,
V::Number(n) => n.as_f64().map(|x| x != 0.0).unwrap_or(false),
V::String(s) => {
let s_l = s.trim().to_lowercase();
if s_l == "true" { true } else if s_l == "false" { false } else { !s_l.is_empty() }
}
V::Array(a) => !a.is_empty(),
V::Object(m) => !m.is_empty(),
}
}
let res = match (op.as_str(), &lval, &rval) {
// 等于 / 不等于(适配所有 JSON 类型;数字按 f64 比较,其他走深度相等)
("equal" | "equals" | "==" | "eq", l, Some(r)) => json_equal(l, r, weak_eq),
("not equal" | "!=" | "not equals" | "neq", l, Some(r)) => !json_equal(l, r, weak_eq),
// 数字比较
("greater than" | ">" | "gt", l, Some(r)) => match (to_f64(l), to_f64(r)) { (Some(a), Some(b)) => a > b, _ => false },
("greater than or equal" | ">=" | "gte" | "ge", l, Some(r)) => match (to_f64(l), to_f64(r)) { (Some(a), Some(b)) => a >= b, _ => false },
("less than" | "<" | "lt", l, Some(r)) => match (to_f64(l), to_f64(r)) { (Some(a), Some(b)) => a < b, _ => false },
("less than or equal" | "<=" | "lte" | "le", l, Some(r)) => match (to_f64(l), to_f64(r)) { (Some(a), Some(b)) => a <= b, _ => false },
// 包含 / 不包含(字符串、数组、对象(键)
("contains", l, Some(r)) => contains(l, r, weak_eq),
("not contains", l, Some(r)) => !contains(l, r, weak_eq),
// 成员关系left in right / not in
("in", l, Some(r)) => in_op(l, r, weak_eq),
("not in" | "nin", l, Some(r)) => !in_op(l, r, weak_eq),
// 为空 / 非空字符串、数组、对象、null
("is empty" | "empty" | "isempty", l, _) => is_empty_val(l),
("is not empty" | "not empty" | "notempty", l, _) => !is_empty_val(l),
// 布尔判断(对各类型进行布尔化)
("is true" | "is true?" | "istrue", l, _) => bool_like(l),
("is false" | "isfalse", l, _) => !bool_like(l),
_ => false,
};
// 记录调试日志,便于定位条件为何未命中
let l_dbg = match &lval { V::String(s) => format!("\"{}\"", s), _ => format!("{}", lval) };
let r_dbg = match &rval { Some(V::String(s)) => format!("\"{}\"", s), Some(v) => format!("{}", v), None => "<none>".to_string() };
info!(target = "udmin.flow", op=%op, weak=%weak_eq, left=%l_dbg, right=%r_dbg, result=%res, "condition eval");
Ok(res)
}
pub(crate) fn resolve_value(ctx: &serde_json::Value, v: &serde_json::Value) -> Result<serde_json::Value> {
let t = v.get("type").and_then(|v| v.as_str()).unwrap_or("");
match t {
"constant" => Ok(v.get("content").cloned().unwrap_or(V::Null)),
"ref" => {
// content: [nodeId, field]
if let Some(arr) = v.get("content").and_then(|v| v.as_array()) {
if arr.len() >= 2 {
if let (Some(node), Some(field)) = (arr[0].as_str(), arr[1].as_str()) {
let val = ctx
.get("nodes")
.and_then(|n| n.get(node))
.and_then(|m| m.get(field))
.cloned()
.or_else(|| ctx.get(field).cloned())
.unwrap_or(V::Null);
return Ok(val);
}
}
}
Ok(V::Null)
}
"expression" => {
let expr = v.get("content").and_then(|x| x.as_str()).unwrap_or("");
if expr.trim().is_empty() { return Ok(V::Null); }
Ok(crate::flow::executors::script_rhai::eval_rhai_expr_json(expr, ctx).unwrap_or_else(|_| V::Null))
}
_ => Ok(V::Null),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn cond_eq_const(left: serde_json::Value, right: serde_json::Value) -> serde_json::Value {
json!({
"left": {"type": "constant", "content": left},
"operator": "eq",
"right": {"type": "constant", "content": right}
})
}
#[test]
fn and_group_all_true() {
let ctx = json!({});
let group = json!([
cond_eq_const(json!(100), json!(100)),
json!({
"left": {"type": "constant", "content": 100},
"operator": ">",
"right": {"type": "constant", "content": 10}
})
]);
let ok = eval_condition_json(&ctx, &group).unwrap();
assert!(ok);
}
#[test]
fn and_group_has_false() {
let ctx = json!({});
let group = json!([
cond_eq_const(json!(100), json!(10)), // false
json!({
"left": {"type": "constant", "content": 100},
"operator": ">",
"right": {"type": "constant", "content": 10}
})
]);
let ok = eval_condition_json(&ctx, &group).unwrap();
assert!(!ok);
}
}