// 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 { // 新增:若 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 { match v { V::Number(n) => n.as_f64(), V::String(s) => s.parse::().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 => "".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 { 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); } }