package cn.iocoder.yudao.module.qcreport.engine;
|
|
import java.math.BigDecimal;
|
import java.time.temporal.TemporalAccessor;
|
import java.time.format.DateTimeFormatter;
|
import java.time.ZoneId;
|
import java.util.ArrayList;
|
import java.util.Collection;
|
import java.util.Date;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Set;
|
import java.util.regex.Matcher;
|
import java.util.regex.Pattern;
|
|
/**
|
* 上下文取值。
|
* <p>
|
* 只做白名单式的属性导航,不解析、不执行任何代码:
|
* 支持 a.b.c 与 a.b[0].c 两种写法,路径在数组上继续取属性时按元素逐个取值(pluck)。
|
* <p>
|
* 与前端 {@code src/components/quality/engine/path.ts} 一一对应,两边必须同语义。
|
*/
|
public final class Paths {
|
|
/** 路径片段:普通属性名或数组下标 */
|
private static final Pattern TOKEN_PATTERN = Pattern.compile("[^.\\[\\]]+");
|
|
/** 禁止访问的属性名,避免顺着原型链读到构造器 */
|
private static final Set<String> BLOCKED_KEYS = Set.of("__proto__", "constructor", "prototype");
|
|
private static final Pattern INTEGER_TEXT = Pattern.compile("^\\d+$");
|
|
private static final DateTimeFormatter DATE_TIME_FORMAT =
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
private Paths() {
|
}
|
|
/**
|
* 把路径拆成片段:inspectionItems[0].actualValue → [inspectionItems, 0, actualValue]
|
*/
|
public static List<String> parsePath(String path) {
|
List<String> tokens = new ArrayList<>();
|
if (path == null) {
|
return tokens;
|
}
|
Matcher matcher = TOKEN_PATTERN.matcher(path);
|
while (matcher.find()) {
|
tokens.add(matcher.group());
|
}
|
return tokens;
|
}
|
|
/**
|
* 按路径取值,取不到返回 null。
|
*/
|
public static Object readPath(Object source, String path) {
|
return readTokens(source, parsePath(path), 0);
|
}
|
|
private static Object readTokens(Object source, List<String> tokens, int start) {
|
Object current = source;
|
for (int index = start; index < tokens.size(); index++) {
|
String token = tokens.get(index);
|
if (current == null) {
|
return null;
|
}
|
if (current instanceof List<?> list) {
|
Integer arrayIndex = toIndex(token);
|
if (arrayIndex == null) {
|
// 数组上继续取属性:逐元素取值,得到同长度的数组
|
List<Object> plucked = new ArrayList<>(list.size());
|
for (Object element : list) {
|
plucked.add(readTokens(element, tokens, index));
|
}
|
return plucked;
|
}
|
current = arrayIndex < list.size() ? list.get(arrayIndex) : null;
|
continue;
|
}
|
if (current instanceof Map<?, ?> map) {
|
if (BLOCKED_KEYS.contains(token)) {
|
return null;
|
}
|
current = map.get(token);
|
continue;
|
}
|
// 标量后面还有路径片段,说明路径写错了
|
return null;
|
}
|
return current;
|
}
|
|
private static Integer toIndex(String token) {
|
return INTEGER_TEXT.matcher(token).matches() ? Integer.valueOf(token) : null;
|
}
|
|
/**
|
* 展示用格式化:空值渲染为空字符串,布尔值转中文,日期只保留到秒。
|
* <p>
|
* 整数值的双精度数(如 60.0)按整数输出,与 JS 的 {@code String(60)} 保持一致,
|
* 否则报告上会出现「标准值 60.0」这种不该有的尾数。
|
*/
|
public static String formatValue(Object value) {
|
if (value == null) {
|
return "";
|
}
|
if (value instanceof Boolean bool) {
|
return bool ? "是" : "否";
|
}
|
if (value instanceof Date date) {
|
return DATE_TIME_FORMAT.format(date.toInstant().atZone(ZoneId.systemDefault()));
|
}
|
if (value instanceof TemporalAccessor temporal) {
|
return DATE_TIME_FORMAT.format(temporal);
|
}
|
if (value instanceof Collection<?> collection) {
|
List<String> parts = new ArrayList<>(collection.size());
|
for (Object element : collection) {
|
parts.add(formatValue(element));
|
}
|
return String.join("、", parts);
|
}
|
return formatNumberAware(value);
|
}
|
|
private static String formatNumberAware(Object value) {
|
if (value instanceof Double || value instanceof Float || value instanceof BigDecimal) {
|
return Numbers.toString(((Number) value).doubleValue());
|
}
|
return String.valueOf(value);
|
}
|
|
/**
|
* 数值化:非数值返回 NaN,由调用方决定如何处理。
|
*/
|
public static double toNumber(Object value) {
|
if (value instanceof Number number) {
|
return number.doubleValue();
|
}
|
if (value instanceof Boolean bool) {
|
return bool ? 1D : 0D;
|
}
|
if (value instanceof String text && !text.isBlank()) {
|
try {
|
return Double.parseDouble(text.trim());
|
} catch (NumberFormatException ignored) {
|
return Double.NaN;
|
}
|
}
|
return Double.NaN;
|
}
|
|
/**
|
* 把任意取值收敛成数值数组,供统计函数使用。
|
*/
|
public static List<Double> toNumberArray(Object value) {
|
List<Double> numbers = new ArrayList<>();
|
if (value instanceof Collection<?> collection) {
|
flattenInto(collection, numbers);
|
return numbers;
|
}
|
double single = toNumber(value);
|
if (Double.isFinite(single)) {
|
numbers.add(single);
|
}
|
return numbers;
|
}
|
|
private static void flattenInto(Object value, List<Double> target) {
|
if (value instanceof Collection<?> collection) {
|
for (Object element : collection) {
|
flattenInto(element, target);
|
}
|
return;
|
}
|
double numeric = toNumber(value);
|
if (Double.isFinite(numeric)) {
|
target.add(numeric);
|
}
|
}
|
|
/**
|
* 把任意取值按一层一层摊平成列表,保留原始元素(不做数值转换),供 PASS_RATE 这类函数使用。
|
*/
|
public static List<Object> flatten(Object value) {
|
List<Object> result = new ArrayList<>();
|
if (value instanceof Collection<?> collection) {
|
for (Object element : collection) {
|
result.addAll(flatten(element));
|
}
|
return result;
|
}
|
result.add(value);
|
return result;
|
}
|
|
}
|