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