package cn.iocoder.yudao.module.qcreport.engine; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 数据绑定:模板文本里的 {@code {{path}}}。 *

* 只做取值与替换,不做任何表达式求值;需要计算的场景交给规则引擎。 * 取不到的路径渲染为空字符串——报告上不该出现用户看不懂的 {@code {{}}}。 *

* 与前端 {@code engine/binding.ts} 一一对应,两边必须同语义。 */ public final class Bindings { /** 绑定表达式:{{ report.reportNo }} */ private static final Pattern BINDING_PATTERN = Pattern.compile("\\{\\{\\s*([^{}]+?)\\s*\\}\\}"); /** 整段就是一个绑定表达式 */ private static final Pattern SINGLE_BINDING_PATTERN = Pattern.compile("^\\{\\{\\s*([^{}]+?)\\s*\\}\\}$"); private Bindings() { } /** 提取文本里出现的所有绑定路径(去重、保持出现顺序),用于渲染前的数据依赖检查 */ public static List listPaths(String template) { Set paths = new LinkedHashSet<>(); if (template == null) { return List.of(); } Matcher matcher = BINDING_PATTERN.matcher(template); while (matcher.find()) { String path = matcher.group(1).trim(); if (!path.isEmpty()) { paths.add(path); } } return List.copyOf(paths); } /** 解析单个绑定路径,取不到返回 null */ public static Object resolve(String path, Object context) { return path == null ? null : Paths.readPath(context, path.trim()); } /** * 替换文本里的全部绑定表达式,并回报取不到值的路径。 *

* 绑定不上通常意味着数据缺口,静默渲染成空会让报告看起来「正常但缺内容」, * 所以把缺口交给调用方,由渲染引擎汇总成可见的问题清单。 */ public static String resolveText(String template, Object context, Consumer onMissing) { if (template == null) { return ""; } Matcher matcher = BINDING_PATTERN.matcher(template); StringBuilder result = new StringBuilder(); while (matcher.find()) { String path = matcher.group(1).trim(); Object value = Paths.readPath(context, path); if (value == null && onMissing != null) { onMissing.accept(path); } matcher.appendReplacement(result, Matcher.quoteReplacement(Paths.formatValue(value))); } matcher.appendTail(result); return result.toString(); } /** 替换文本里的全部绑定表达式;未绑定的路径渲染为空 */ public static String resolveText(String template, Object context) { return resolveText(template, context, null); } /** 判断一段文本是否是纯绑定表达式({{path}} 单占一段),是则返回路径,否则返回 null */ public static String asSingleBinding(String template) { if (template == null) { return null; } Matcher matcher = SINGLE_BINDING_PATTERN.matcher(template.trim()); return matcher.matches() ? matcher.group(1).trim() : null; } }