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;
|
|
/**
|
* 渲染引擎入口。
|
* <p>
|
* 只做一件事:把落库的模板 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));
|
}
|
|
/**
|
* 模板是否已经有<b>可渲染的</b>画布内容。
|
* <p>
|
* 判据与渲染器读画布的那一行严格一致——{@link HtmlRenderer#render} 取的是
|
* {@code pages[0].frames[0].component},这里就用 {@link Paths#readPath} 取同一个位置,
|
* 而不是退化成「grapes 这个 Map 非空」。
|
* <p>
|
* <b>为什么不能只看非空</b>: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<String, Object> 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<QualityRuleDefinition> rulesOf(ReportTemplateSchema schema) {
|
List<QualityRuleDefinition> rules = new ArrayList<>();
|
if (schema == null || schema.getRules() == null) {
|
return rules;
|
}
|
for (Map<String, Object> raw : schema.getRules()) {
|
QualityRuleDefinition rule = QualityRuleDefinition.from(raw);
|
if (rule != null) {
|
rules.add(rule);
|
}
|
}
|
return rules;
|
}
|
|
/**
|
* 保存前校验模板里的全部判定规则,返回问题清单,空清单表示通过。
|
* <p>
|
* 除了语法,还拦白名单外的函数调用:这类规则一旦存下去,
|
* 每张报告都会带上一条「无法执行」,不如在设计阶段就退回去。
|
* <p>
|
* <b>空表达式的规则也退回去。</b>这条曾经是放行的——当时设计器还没有规则编辑界面,
|
* 报了错用户无处可改,只会卡在保存不了,等于把「一条坏数据」升级成「整份模板不能保存」。
|
* 规则编辑界面做出来之后这个理由不成立了:用户在设计器里删掉这条规则就能继续保存,
|
* 所以改成明确报错。读取仍然宽容({@link #rulesOf} 不会因为一条坏规则让整份模板打不开),
|
* 执行期也照常把它报出来(「第 N 项「X」的规则「Y」无法执行:规则表达式为空」)。
|
*/
|
public static List<String> validateRules(ReportTemplateSchema schema) {
|
List<String> 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;
|
}
|
|
}
|