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;
|
|
/**
|
* 规则求值。
|
* <p>
|
* 只认 {@link RuleNode} 与白名单函数,规则文本里的任何内容都不会变成可执行代码。
|
* 与前端 {@code engine/rule-engine.ts} 的求值段一一对应,两边结果必须一致。
|
*/
|
public final class RuleEvaluator {
|
|
/** 判定结果类取值统一成 PASS / FAIL:兼容中文、布尔与数字 1/0 */
|
private static final Set<String> PASS_TOKENS = Set.of("PASS", "TRUE", "合格", "OK", "1", "是");
|
private static final Set<String> FAIL_TOKENS = Set.of("FAIL", "FALSE", "不合格", "NG", "0", "否");
|
|
private RuleEvaluator() {
|
}
|
|
/* ------------------------------ 基础判定 ------------------------------ */
|
|
/**
|
* JS {@code String(value)} 的等价实现。
|
* <p>
|
* 规则里的文本比较与拼接都要走这里,否则两端会算出不同结果。
|
*/
|
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<String, RuleFunction> FUNCTIONS = buildFunctions();
|
|
private static Map<String, RuleFunction> buildFunctions() {
|
Map<String, RuleFunction> 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<Double> 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<Double> 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。
|
* <p>
|
* 少了这个兜底,{@code SUM()} 这种写错的规则会直接抛 IndexOutOfBoundsException 穿透到接口层,
|
* 用户看到的是「系统异常」而不是「这条规则写错了」。
|
*/
|
private static Object arg(List<Object> args, int index) {
|
return index < args.size() ? args.get(index) : null;
|
}
|
|
/** 白名单函数名,顺序稳定,用于报错时列出可用函数 */
|
public static List<String> functionNames() {
|
return List.copyOf(FUNCTIONS.keySet());
|
}
|
|
/** 一次性求值全部合格/不合格占比,口径与前端一致 */
|
private static double rate(Object value, String expected) {
|
List<Object> 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<Double> 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<Double> values) {
|
double sum = 0;
|
for (Double value : values) {
|
sum += value;
|
}
|
return sum / values.size();
|
}
|
|
/** 样本标准差(n-1),SPC 计算 CP/CPK 用这个口径 */
|
private static double standardDeviation(List<Double> 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<Double> requireNumbers(String name, Object value) {
|
List<Double> 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<Object> 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<String> listFunctionNames(RuleNode node) {
|
List<String> names = new ArrayList<>();
|
collectFunctionNames(node, names);
|
return names;
|
}
|
|
private static void collectFunctionNames(RuleNode node, List<String> 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;
|
}
|
|
/**
|
* 解析后的路径依赖,渲染前可据此检查数据是否齐备。
|
* <p>
|
* 统计函数作用在数组路径上,这里去掉末端字段只保留数组本身。
|
*/
|
public static List<String> listPaths(RuleNode node) {
|
Set<String> paths = new LinkedHashSet<>();
|
collectPaths(node, paths);
|
Set<String> 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<String> 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<Object> args);
|
|
}
|
|
}
|