10 小时以前 9bad721754fe8bbe2e5f459d0706e0fefac569f3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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;
    }
 
}