12 小时以前 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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
package cn.iocoder.yudao.module.qcreport.engine.report;
 
import cn.iocoder.yudao.module.qcreport.engine.Paths;
import cn.iocoder.yudao.module.qcreport.engine.QualityResult;
import cn.iocoder.yudao.module.qcreport.engine.context.InspectionItem;
import cn.iocoder.yudao.module.qcreport.engine.context.ReportContext;
import cn.iocoder.yudao.module.qcreport.engine.context.ReportFields;
import cn.iocoder.yudao.module.qcreport.engine.rule.QualityRuleDefinition;
import cn.iocoder.yudao.module.qcreport.engine.rule.RuleEngine;
import cn.iocoder.yudao.module.qcreport.engine.rule.RuleScope;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
 
/**
 * 报告判定。
 * <p>
 * 在渲染前把检验项算成 PASS/FAIL,并汇总出报告结论与合格率。
 * 判定只发生在这里,组件与模板都不做计算,保证同一份数据在任何模板下判定一致。
 * <p>
 * 与前端 {@code engine/report-evaluator.ts} 一一对应,两边必须算出同一结果。
 */
public final class ReportEvaluator {
 
    private ReportEvaluator() {
    }
 
    /**
     * 计算检验项判定与报告汇总。
     * <p>
     * 单个检验项的判定优先级:item 作用域规则 → 规格上下限 → 数据自带结果。
     * 报告级判定默认取「全部检验项合格」,有 report 作用域规则时以规则为准。
     * <p>
     * 不修改传入的上下文,判定后的结果放在返回值里。
     */
    public static EvaluationOutcome evaluate(ReportContext context, List<QualityRuleDefinition> rules) {
        List<String> errors = new ArrayList<>();
        List<QualityRuleDefinition> itemRules = new ArrayList<>();
        List<QualityRuleDefinition> reportRules = new ArrayList<>();
        for (QualityRuleDefinition rule : rules == null ? List.<QualityRuleDefinition>of() : rules) {
            if (rule == null || !rule.active()) {
                continue;
            }
            if (rule.scope() == RuleScope.REPORT) {
                reportRules.add(rule);
            } else {
                itemRules.add(rule);
            }
        }
 
        List<InspectionItem> source = context.getInspectionItems() == null ? List.of() : context.getInspectionItems();
        List<InspectionItem> items = new ArrayList<>(source.size());
        for (InspectionItem item : source) {
            items.add(item.copy());
        }
 
        // 无判定依据的项数,只用来把这类项从合格率分母与报告结论里摘出去
        int undecidableCount = 0;
 
        for (int index = 0; index < items.size(); index++) {
            InspectionItem item = items.get(index);
            if (!itemRules.isEmpty()) {
                // 逐项规则在「单项上下文」里求值:item 指向当前行,index 从 1 开始与报告序号一致
                Map<String, Object> scoped = context.toScope();
                scoped.put("item", ReportContext.itemMap(item));
                scoped.put("index", index + 1);
                boolean passed = true;
                boolean failed = false;
                for (QualityRuleDefinition rule : itemRules) {
                    try {
                        if (!RuleEngine.test(rule.expression(), scoped)) {
                            passed = false;
                        }
                    } catch (RuntimeException error) {
                        failed = true;
                        errors.add("第 " + (index + 1) + " 项「" + orEmpty(item.getItemName()) + "」的规则「"
                                + rule.rawLabel() + "」无法执行:" + error.getMessage());
                    }
                }
                item.applyResult(failed ? null : (passed ? QualityResult.PASS : QualityResult.FAIL));
                continue;
            }
 
            QualityResult byLimit = evaluateByLimit(item);
            if (byLimit != null) {
                item.applyResult(byLimit);
                continue;
            }
 
            QualityResult existing = QualityResult.of(item.getResult());
            if (existing == null) {
                // 既没有规则、也没有判定依据:如实标出来,不要默认合格
                item.applyUndecidable();
                undecidableCount++;
                errors.add("第 " + (index + 1) + " 项「"
                        + (isBlank(item.getItemName()) ? "未命名" : item.getItemName())
                        + "」没有规格上下限也没有判定规则,无法判定");
                continue;
            }
            item.applyResult(existing);
        }
 
        int passCount = 0;
        int failCount = 0;
        for (InspectionItem item : items) {
            if (QualityResult.PASS.name().equals(item.getResult())) {
                passCount++;
            } else if (QualityResult.FAIL.name().equals(item.getResult())) {
                failCount++;
            }
        }
        int total = items.size();
        // 合格率的分母只算「能判定」的项,无判定规则的项既不加分也不减分
        int decidableCount = total - undecidableCount;
 
        QualityResult verdict = null;
        if (!reportRules.isEmpty()) {
            Map<String, Object> scoped = context.scope(items);
            scoped.put("index", 0);
            boolean passed = true;
            boolean failed = false;
            for (QualityRuleDefinition rule : reportRules) {
                try {
                    if (!RuleEngine.test(rule.expression(), scoped)) {
                        passed = false;
                    }
                } catch (RuntimeException error) {
                    failed = true;
                    errors.add("报告级规则「" + rule.rawLabel() + "」无法执行:" + error.getMessage());
                }
            }
            verdict = failed ? null : (passed ? QualityResult.PASS : QualityResult.FAIL);
        } else if (decidableCount > 0) {
            // 只有「全部可判定项都合格」才给合格结论。无判定规则的项不参与,它们没有对错可言。
            // 存在待判定项(规则跑挂)时不给结论:没验过不能说合格,没证据也不能说不合格,
            // 否则会出现「结论合格、合格率 0%」这种自相矛盾的报告。
            if (failCount > 0) {
                verdict = QualityResult.FAIL;
            } else if (passCount == decidableCount) {
                verdict = QualityResult.PASS;
            }
        }
 
        ReportFields report = context.getReport().copy()
                .setTotal(total)
                .setPassCount(passCount)
                .setFailCount(failCount)
                .setPassRate(formatPassRate(passCount, decidableCount))
                .applyResult(verdict);
        return new EvaluationOutcome(new ReportContext().setReport(report).setInspectionItems(items), errors);
    }
 
    /** 用规格上下限判定单项:实测值不在区间内即不合格 */
    private static QualityResult evaluateByLimit(InspectionItem item) {
        double actual = Paths.toNumber(item.getActualValue());
        if (!Double.isFinite(actual)) {
            return null;
        }
        boolean hasUpper = item.getUpperLimit() != null && Double.isFinite(item.getUpperLimit());
        boolean hasLower = item.getLowerLimit() != null && Double.isFinite(item.getLowerLimit());
        if (!hasUpper && !hasLower) {
            return null;
        }
        if (hasUpper && actual > item.getUpperLimit()) {
            return QualityResult.FAIL;
        }
        if (hasLower && actual < item.getLowerLimit()) {
            return QualityResult.FAIL;
        }
        return QualityResult.PASS;
    }
 
    private static String formatPassRate(int passCount, int total) {
        if (total == 0) {
            return "";
        }
        return String.format(Locale.ROOT, "%.2f%%", (double) passCount / total * 100);
    }
 
    private static String orEmpty(String value) {
        return value == null ? "" : value;
    }
 
    private static boolean isBlank(String value) {
        return value == null || value.isEmpty();
    }
 
}