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();
|
}
|
|
}
|