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; /** * 报告判定。 *

* 在渲染前把检验项算成 PASS/FAIL,并汇总出报告结论与合格率。 * 判定只发生在这里,组件与模板都不做计算,保证同一份数据在任何模板下判定一致。 *

* 与前端 {@code engine/report-evaluator.ts} 一一对应,两边必须算出同一结果。 */ public final class ReportEvaluator { private ReportEvaluator() { } /** * 计算检验项判定与报告汇总。 *

* 单个检验项的判定优先级:item 作用域规则 → 规格上下限 → 数据自带结果。 * 报告级判定默认取「全部检验项合格」,有 report 作用域规则时以规则为准。 *

* 不修改传入的上下文,判定后的结果放在返回值里。 */ public static EvaluationOutcome evaluate(ReportContext context, List rules) { List errors = new ArrayList<>(); List itemRules = new ArrayList<>(); List reportRules = new ArrayList<>(); for (QualityRuleDefinition rule : rules == null ? List.of() : rules) { if (rule == null || !rule.active()) { continue; } if (rule.scope() == RuleScope.REPORT) { reportRules.add(rule); } else { itemRules.add(rule); } } List source = context.getInspectionItems() == null ? List.of() : context.getInspectionItems(); List 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 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 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(); } }