package cn.iocoder.yudao.module.qcreport.engine; import cn.iocoder.yudao.module.qcreport.dal.dataobject.version.ReportTemplateSchema; import cn.iocoder.yudao.module.qcreport.engine.context.ReportContext; import cn.iocoder.yudao.module.qcreport.engine.render.HtmlRenderer; import cn.iocoder.yudao.module.qcreport.engine.render.RenderOutcome; import cn.iocoder.yudao.module.qcreport.engine.rule.RuleEngine; import cn.iocoder.yudao.module.qcreport.engine.rule.RuleEvaluator; import cn.iocoder.yudao.module.qcreport.engine.rule.QualityRuleDefinition; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * 渲染引擎入口。 *

* 只做一件事:把落库的模板 Schema 翻译成引擎的内部结构,然后渲染。 * 引擎本身不依赖 Spring、不落库、不做权限——它只吃「Schema + 上下文」吐 HTML, * 这样批量生成、定时任务、其它模块调用都能走同一个入口。 */ public final class QualityReportEngine { private QualityReportEngine() { } /** 渲染报告:判定 → 生成正文 → 组装文档 */ public static RenderOutcome render(ReportTemplateSchema schema, ReportContext context) { return HtmlRenderer.render(grapesOf(schema), pageOf(schema), context, rulesOf(schema)); } /** * 模板是否已经有可渲染的画布内容。 *

* 判据与渲染器读画布的那一行严格一致——{@link HtmlRenderer#render} 取的是 * {@code pages[0].frames[0].component},这里就用 {@link Paths#readPath} 取同一个位置, * 而不是退化成「grapes 这个 Map 非空」。 *

* 为什么不能只看非空:GrapesJS 把「打开过但一个组件都没放」的项目存成 * {@code {"assets":[],"styles":[]}},它非空、却没有 frames;渲染器取不到根组件时 * 直接把 body 渲染成空串——不报错,用户拿到一张白纸 PDF。出件前的这个检查就是为了 * 把这种版本挡在渲染之前,判据必须和渲染器对齐,否则挡不住。 */ public static boolean hasCanvas(ReportTemplateSchema schema) { return Paths.readPath(grapesOf(schema), "pages[0].frames[0].component") instanceof Map; } /** GrapesJS 项目数据 */ public static Map grapesOf(ReportTemplateSchema schema) { return schema == null ? null : schema.getGrapes(); } /** 纸张与页边距,未配置时落到默认 A4 与默认边距 */ public static PageSetting pageOf(ReportTemplateSchema schema) { ReportTemplateSchema.Page page = schema == null ? null : schema.getPage(); if (page == null) { return PageSetting.defaultSetting(); } ReportTemplateSchema.Margin margin = page.getMargin(); PageMargin pageMargin = margin == null ? PageMargin.none() : new PageMargin(margin.getTop(), margin.getRight(), margin.getBottom(), margin.getLeft()); return new PageSetting(page.getSize(), page.getOrientation(), pageMargin); } /** 判定规则定义,Schema 里没配时返回空清单 */ public static List rulesOf(ReportTemplateSchema schema) { List rules = new ArrayList<>(); if (schema == null || schema.getRules() == null) { return rules; } for (Map raw : schema.getRules()) { QualityRuleDefinition rule = QualityRuleDefinition.from(raw); if (rule != null) { rules.add(rule); } } return rules; } /** * 保存前校验模板里的全部判定规则,返回问题清单,空清单表示通过。 *

* 除了语法,还拦白名单外的函数调用:这类规则一旦存下去, * 每张报告都会带上一条「无法执行」,不如在设计阶段就退回去。 *

* 空表达式的规则也退回去。这条曾经是放行的——当时设计器还没有规则编辑界面, * 报了错用户无处可改,只会卡在保存不了,等于把「一条坏数据」升级成「整份模板不能保存」。 * 规则编辑界面做出来之后这个理由不成立了:用户在设计器里删掉这条规则就能继续保存, * 所以改成明确报错。读取仍然宽容({@link #rulesOf} 不会因为一条坏规则让整份模板打不开), * 执行期也照常把它报出来(「第 N 项「X」的规则「Y」无法执行:规则表达式为空」)。 */ public static List validateRules(ReportTemplateSchema schema) { List errors = new ArrayList<>(); for (QualityRuleDefinition rule : rulesOf(schema)) { errors.addAll(RuleEngine.validate(rule)); String expression = rule.expression(); if (expression == null || expression.trim().isEmpty()) { // 空表达式上一步已经报过,也没什么可静态检查的 continue; } try { String unknown = RuleEvaluator.findUnknownFunction(RuleEngine.parse(expression)); if (unknown != null) { errors.add("规则「" + rule.label() + "」调用了不支持的函数 " + unknown + ",可用函数:" + String.join("、", RuleEvaluator.functionNames())); } } catch (RuntimeException ignored) { // 语法错误已经在上一步报过,不重复刷屏 } } return errors; } }