package cn.iocoder.yudao.module.qcreport.engine.rule; import cn.iocoder.yudao.module.qcreport.engine.JsValues; import cn.iocoder.yudao.module.qcreport.engine.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; /** * 规则求值。 *

* 只认 {@link RuleNode} 与白名单函数,规则文本里的任何内容都不会变成可执行代码。 * 与前端 {@code engine/rule-engine.ts} 的求值段一一对应,两边结果必须一致。 */ public final class RuleEvaluator { /** 判定结果类取值统一成 PASS / FAIL:兼容中文、布尔与数字 1/0 */ private static final Set PASS_TOKENS = Set.of("PASS", "TRUE", "合格", "OK", "1", "是"); private static final Set FAIL_TOKENS = Set.of("FAIL", "FALSE", "不合格", "NG", "0", "否"); private RuleEvaluator() { } /* ------------------------------ 基础判定 ------------------------------ */ /** * JS {@code String(value)} 的等价实现。 *

* 规则里的文本比较与拼接都要走这里,否则两端会算出不同结果。 */ public static String asText(Object value) { return JsValues.asText(value); } /** 空值:null 或空串(空数组不算空) */ public static boolean isBlank(Object value) { return value == null || "".equals(value); } /** 真值判定,与 JS 版 truthy 同语义 */ public static boolean truthy(Object value) { if (value instanceof List list) { return !list.isEmpty(); } if (isBlank(value)) { return false; } if (value instanceof Boolean bool) { return bool; } double numeric = Paths.toNumber(value); if (Double.isFinite(numeric)) { return numeric != 0; } return true; } /** 两侧都能当数字时按数值比较,否则按字符串比较 */ public static int compare(Object left, Object right) { double leftNumber = Paths.toNumber(left); double rightNumber = Paths.toNumber(right); if (Double.isFinite(leftNumber) && Double.isFinite(rightNumber)) { return Double.compare(leftNumber, rightNumber); } return asText(left).compareTo(asText(right)); } /** 判定结果类取值统一成 PASS / FAIL,识别不出返回空串 */ public static String normalizeResultToken(Object value) { if (value instanceof Boolean bool) { return bool ? "PASS" : "FAIL"; } String text = asText(value).trim().toUpperCase(Locale.ROOT); if (PASS_TOKENS.contains(text)) { return "PASS"; } return FAIL_TOKENS.contains(text) ? "FAIL" : ""; } /* ------------------------------ 白名单函数 ------------------------------ */ /** 白名单函数表,规则文本只能调用这里面的函数 */ private static final Map FUNCTIONS = buildFunctions(); private static Map buildFunctions() { Map functions = new LinkedHashMap<>(); functions.put("ABS", args -> Math.abs(requireNumber("ABS", arg(args, 0)))); functions.put("AVG", args -> mean(requireNumbers("AVG", arg(args, 0)))); functions.put("COUNT", args -> arg(args, 0) instanceof List list ? list.size() : Paths.toNumberArray(arg(args, 0)).size()); // 过程能力指数 CP=(USL-LSL)/(6σ) functions.put("CP", args -> { List values = requireNumbers("CP", arg(args, 0)); double deviation = standardDeviation(values); if (!Double.isFinite(deviation) || deviation == 0) { return Double.NaN; } return (requireNumber("CP", arg(args, 1)) - requireNumber("CP", arg(args, 2))) / (6 * deviation); }); // 过程能力指数 CPK=min(USL-μ, μ-LSL)/(3σ) functions.put("CPK", args -> { List values = requireNumbers("CPK", arg(args, 0)); double deviation = standardDeviation(values); if (!Double.isFinite(deviation) || deviation == 0) { return Double.NaN; } double upper = requireNumber("CPK", arg(args, 1)); double lower = requireNumber("CPK", arg(args, 2)); double average = mean(values); return Math.min(upper - average, average - lower) / (3 * deviation); }); // 不合格率(百分比),口径与 PASS_RATE 一致 functions.put("FAIL_RATE", args -> rate(arg(args, 0), "FAIL")); functions.put("MAX", args -> extreme(requireNumbers("MAX", arg(args, 0)), true)); functions.put("MIN", args -> extreme(requireNumbers("MIN", arg(args, 0)), false)); // 合格率(百分比,保留两位) functions.put("PASS_RATE", args -> rate(arg(args, 0), "PASS")); functions.put("ROUND", args -> { double digits = args.size() > 1 ? requireNumber("ROUND", arg(args, 1)) : 0; double factor = Math.pow(10, digits); return Math.round(requireNumber("ROUND", arg(args, 0)) * factor) / factor; }); functions.put("STDDEV", args -> standardDeviation(requireNumbers("STDDEV", arg(args, 0)))); functions.put("SUM", args -> { double sum = 0; for (Double value : requireNumbers("SUM", arg(args, 0))) { sum += value; } return sum; }); // 不能换成 Map.copyOf:它不保证遍历顺序,报错里列出的可用函数会随机排 return Collections.unmodifiableMap(functions); } /** * 取第 index 个实参,缺参返回 null。 *

* 少了这个兜底,{@code SUM()} 这种写错的规则会直接抛 IndexOutOfBoundsException 穿透到接口层, * 用户看到的是「系统异常」而不是「这条规则写错了」。 */ private static Object arg(List args, int index) { return index < args.size() ? args.get(index) : null; } /** 白名单函数名,顺序稳定,用于报错时列出可用函数 */ public static List functionNames() { return List.copyOf(FUNCTIONS.keySet()); } /** 一次性求值全部合格/不合格占比,口径与前端一致 */ private static double rate(Object value, String expected) { List values = Paths.flatten(value); if (values.isEmpty()) { return Double.NaN; } int hit = 0; for (Object element : values) { if (expected.equals(normalizeResultToken(element))) { hit++; } } return toFixed2((double) hit / values.size() * 100); } /** JS toFixed(2) 后再取回数值:保留两位的百分比 */ private static double toFixed2(double value) { return Math.round(value * 100) / 100.0; } private static double extreme(List values, boolean max) { double result = values.get(0); for (Double value : values) { result = max ? Math.max(result, value) : Math.min(result, value); } return result; } private static double mean(List values) { double sum = 0; for (Double value : values) { sum += value; } return sum / values.size(); } /** 样本标准差(n-1),SPC 计算 CP/CPK 用这个口径 */ private static double standardDeviation(List values) { if (values.size() < 2) { return Double.NaN; } double average = mean(values); double variance = 0; for (Double value : values) { variance += Math.pow(value - average, 2); } return Math.sqrt(variance / (values.size() - 1)); } private static List requireNumbers(String name, Object value) { List values = Paths.toNumberArray(value); if (values.isEmpty()) { throw new RuleRuntimeException("函数 " + name + " 需要数值型参数,实际取到「" + asText(value) + "」"); } return values; } private static double requireNumber(String name, Object value) { double numeric = Paths.toNumber(value); if (!Double.isFinite(numeric)) { throw new RuleRuntimeException("函数 " + name + " 需要数字参数,实际取到「" + asText(value) + "」"); } return numeric; } /* ------------------------------ 求值 ------------------------------ */ /** 求值,返回原始结果(可能是数字、字符串、布尔) */ public static Object evaluateNode(RuleNode node, Object context) { return switch (node) { case RuleNode.Literal literal -> literal.value(); case RuleNode.Path path -> Paths.readPath(context, path.path()); case RuleNode.Call call -> evaluateCall(call, context); case RuleNode.Unary unary -> evaluateUnary(unary, context); case RuleNode.Between between -> evaluateBetween(between, context); case RuleNode.In inNode -> evaluateIn(inNode, context); case RuleNode.Binary binary -> evaluateBinary(binary, context); }; } /** 求值并转成布尔判定,用于「是否合格」这类条件 */ public static boolean evaluateCondition(RuleNode node, Object context) { return truthy(evaluateNode(node, context)); } private static Object evaluateCall(RuleNode.Call call, Object context) { RuleFunction handler = FUNCTIONS.get(call.name()); if (handler == null) { throw new RuleRuntimeException( "不支持函数 " + call.name() + ",可用函数:" + String.join("、", FUNCTIONS.keySet())); } List args = new ArrayList<>(call.args().size()); for (RuleNode arg : call.args()) { args.add(evaluateNode(arg, context)); } return handler.apply(args); } private static Object evaluateUnary(RuleNode.Unary unary, Object context) { Object value = evaluateNode(unary.operand(), context); return "!".equals(unary.operator()) ? !truthy(value) : -Paths.toNumber(value); } private static Object evaluateBetween(RuleNode.Between between, Object context) { Object value = evaluateNode(between.value(), context); boolean hit = compare(value, evaluateNode(between.lower(), context)) >= 0 && compare(value, evaluateNode(between.upper(), context)) <= 0; return between.negated() != hit; } private static Object evaluateIn(RuleNode.In inNode, Object context) { Object value = evaluateNode(inNode.value(), context); boolean hit = false; for (RuleNode item : inNode.items()) { if (compare(value, evaluateNode(item, context)) == 0) { hit = true; break; } } return inNode.negated() != hit; } private static Object evaluateBinary(RuleNode.Binary binary, Object context) { if ("AND".equals(binary.operator())) { return truthy(evaluateNode(binary.left(), context)) && truthy(evaluateNode(binary.right(), context)); } if ("OR".equals(binary.operator())) { return truthy(evaluateNode(binary.left(), context)) || truthy(evaluateNode(binary.right(), context)); } Object left = evaluateNode(binary.left(), context); Object right = evaluateNode(binary.right(), context); return switch (binary.operator()) { case "=" -> compare(left, right) == 0; case "!=" -> compare(left, right) != 0; case ">" -> compare(left, right) > 0; case ">=" -> compare(left, right) >= 0; case "<" -> compare(left, right) < 0; case "<=" -> compare(left, right) <= 0; case "+" -> add(left, right); case "-" -> Paths.toNumber(left) - Paths.toNumber(right); case "*" -> Paths.toNumber(left) * Paths.toNumber(right); case "/" -> divide(left, right); default -> throw new RuleRuntimeException("不支持的运算符 " + binary.operator()); }; } /** 两侧都是数值才做加法,否则按文本拼接(编号类字段常用) */ private static Object add(Object left, Object right) { double leftNumber = Paths.toNumber(left); double rightNumber = Paths.toNumber(right); if (Double.isFinite(leftNumber) && Double.isFinite(rightNumber)) { return leftNumber + rightNumber; } return asText(left) + asText(right); } private static Object divide(Object left, Object right) { double divisor = Paths.toNumber(right); if (divisor == 0) { throw new RuleRuntimeException("规则里出现了除以 0"); } return Paths.toNumber(left) / divisor; } /* ------------------------------ 静态分析 ------------------------------ */ /** 表达式里调用的函数名(含嵌套),用于保存前校验白名单 */ public static List listFunctionNames(RuleNode node) { List names = new ArrayList<>(); collectFunctionNames(node, names); return names; } private static void collectFunctionNames(RuleNode node, List names) { switch (node) { case RuleNode.Call call -> { names.add(call.name()); call.args().forEach(arg -> collectFunctionNames(arg, names)); } case RuleNode.Unary unary -> collectFunctionNames(unary.operand(), names); case RuleNode.Binary binary -> { collectFunctionNames(binary.left(), names); collectFunctionNames(binary.right(), names); } case RuleNode.Between between -> { collectFunctionNames(between.value(), names); collectFunctionNames(between.lower(), names); collectFunctionNames(between.upper(), names); } case RuleNode.In inNode -> { collectFunctionNames(inNode.value(), names); inNode.items().forEach(item -> collectFunctionNames(item, names)); } default -> { } } } /** 未知函数名返回 null,供调用方给出可读报错 */ public static String findUnknownFunction(RuleNode node) { for (String name : listFunctionNames(node)) { if (!FUNCTIONS.containsKey(name)) { return name; } } return null; } /** * 解析后的路径依赖,渲染前可据此检查数据是否齐备。 *

* 统计函数作用在数组路径上,这里去掉末端字段只保留数组本身。 */ public static List listPaths(RuleNode node) { Set paths = new LinkedHashSet<>(); collectPaths(node, paths); Set normalized = new LinkedHashSet<>(); for (String path : paths) { normalized.add(String.join(".", Paths.parsePath(path))); } return List.copyOf(normalized); } private static void collectPaths(RuleNode node, Set paths) { switch (node) { case RuleNode.Path path -> { paths.add(path.path()); } case RuleNode.Call call -> call.args().forEach(arg -> collectPaths(arg, paths)); case RuleNode.Unary unary -> collectPaths(unary.operand(), paths); case RuleNode.Binary binary -> { collectPaths(binary.left(), paths); collectPaths(binary.right(), paths); } case RuleNode.Between between -> { collectPaths(between.value(), paths); collectPaths(between.lower(), paths); collectPaths(between.upper(), paths); } case RuleNode.In inNode -> { collectPaths(inNode.value(), paths); inNode.items().forEach(item -> collectPaths(item, paths)); } default -> { } } } /** 白名单函数 */ @FunctionalInterface public interface RuleFunction { Object apply(List args); } }