23 小时以前 35722562e9e13f0504acc15b740d042ecb810199
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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/**
 * 报告判定
 * <p>
 * 在渲染前把检验项算成 PASS/FAIL,并汇总出报告结论与合格率。
 * 判定只发生在这里,组件与模板都不做计算,保证同一份数据在任何模板下判定一致。
 */
import type { QualityResult, ReportContext, ReportContextItem } from './context';
import type { RuleNode } from './rule-engine';
 
import { QUALITY_RESULT, QUALITY_RESULT_TEXT } from './context';
import { toNumber } from './path';
import {
  evaluateConditionNode,
  findUnknownFunction,
  listAllFunctionNames,
  parseRule,
  RuleSyntaxError,
} from './rule-engine';
 
/** 规则作用域:item 逐项判定,report 报告级汇总 */
export type QualityRuleScope = 'item' | 'report';
 
export interface QualityRuleDefinition {
  /** 规则标识,用于出错时定位 */
  id?: string;
  /** 规则名称 */
  name?: string;
  scope: QualityRuleScope;
  /** 判定表达式,求值为真视为合格 */
  expression: string;
  /** 该规则是否参与判定,默认 true */
  enabled?: boolean;
}
 
/** 待判定标记:规则本身出错时用,避免把不确定当成合格 */
const PENDING_TEXT = '待判定';
 
/**
 * 无判定规则标记:既没有规则、也没有规格上下限。
 * <p>
 * 和「待判定」刻意分开:待判定是「本该判、但规则跑挂了」,仍留在合格率分母里;
 * 无判定规则是「这项根本没有判定依据」,不计入合格率分母、也不参与报告结论。
 */
const UNDECIDABLE_TEXT = '无判定规则';
 
/**
 * 校验入参:只用到这三个字段,且都允许缺省。
 * <p>
 * 不复用 {@link QualityRuleDefinition}——那里的 expression 是必填的,
 * 而设计器里正在编辑的规则、以及历史数据里的规则项都可能是空的,
 * 校验函数恰恰要能接住这种「还没写完」的入参。
 */
export interface RuleValidationInput {
  id?: string;
  name?: string;
  expression?: string;
}
 
/**
 * 保存前校验规则文本:语法与函数白名单。
 * <p>
 * 两块都要拦。只拦语法的话,{@code FOO(1)} 这种调用会一路存进库里,
 * 之后每张报告都带一条「无法执行」,而用户在设计器里看不出哪里不对。
 * 返回的问题清单与后端 {@code QualityReportEngine.validateRules} 同口径。
 */
export function validateRule(rule: RuleValidationInput): string[] {
  const errors: string[] = [];
  const label = rule.name?.trim() || rule.id || '未命名规则';
  if (!rule.expression?.trim()) {
    errors.push(`规则「${label}」的表达式为空`);
    return errors;
  }
 
  let node: RuleNode;
  try {
    node = parseRule(rule.expression);
  } catch (error) {
    if (error instanceof RuleSyntaxError) {
      errors.push(`规则「${label}」语法错误:${error.message}(第 ${error.position + 1} 个字符)`);
    } else {
      errors.push(`规则「${label}」无法解析:${(error as Error).message}`);
    }
    return errors;
  }
 
  const unknown = findUnknownFunction(node);
  if (unknown) {
    errors.push(
      `规则「${label}」调用了不支持的函数 ${unknown},可用函数:${listAllFunctionNames().join('、')}`,
    );
  }
  return errors;
}
 
/** 用规格上下限判定单项:实测值不在区间内即不合格 */
function evaluateByLimit(item: ReportContextItem): boolean | undefined {
  const actual = toNumber(item.actualValue);
  if (!Number.isFinite(actual)) {
    return undefined;
  }
  const hasUpper = typeof item.upperLimit === 'number' && Number.isFinite(item.upperLimit);
  const hasLower = typeof item.lowerLimit === 'number' && Number.isFinite(item.lowerLimit);
  if (!hasUpper && !hasLower) {
    return undefined;
  }
  if (hasUpper && actual > item.upperLimit!) {
    return false;
  }
  if (hasLower && actual < item.lowerLimit!) {
    return false;
  }
  return true;
}
 
function normalizeExisting(result: string): QualityResult | undefined {
  const text = result?.trim().toUpperCase();
  if (text === QUALITY_RESULT.PASS) {
    return QUALITY_RESULT.PASS;
  }
  if (text === QUALITY_RESULT.FAIL) {
    return QUALITY_RESULT.FAIL;
  }
  return undefined;
}
 
function applyResult(item: ReportContextItem, result: QualityResult | undefined): void {
  if (!result) {
    item.result = '';
    item.resultText = PENDING_TEXT;
    return;
  }
  item.result = result;
  item.resultText = QUALITY_RESULT_TEXT[result];
}
 
/**
 * 没有判定依据:置为「无判定规则」。
 * <p>
 * 报告里常有「试样质量 m」「称量瓶 m0」这类只供公式取数的过程参数,它们没有规格也就无从判定;
 * 若照常算进分母,一份本来全合格的报告会因为几个过程参数被压成「待判定」。
 */
function applyUndecidable(item: ReportContextItem): void {
  item.result = '';
  item.resultText = UNDECIDABLE_TEXT;
}
 
function formatPassRate(passCount: number, total: number): string {
  if (total === 0) {
    return '';
  }
  return `${((passCount / total) * 100).toFixed(2)}%`;
}
 
export interface EvaluationOutcome {
  context: ReportContext;
  /** 规则的问题清单,渲染时应在报告上显式暴露,不能悄悄吞掉 */
  errors: string[];
}
 
/**
 * 计算检验项判定与报告汇总。
 * <p>
 * 单个检验项的判定优先级:item 作用域规则 → 规格上下限 → 数据自带结果。
 * 报告级判定默认取「全部检验项合格」,有 report 作用域规则时以规则为准。
 */
export function evaluateReport(
  context: ReportContext,
  rules: QualityRuleDefinition[] = [],
): EvaluationOutcome {
  const errors: string[] = [];
  const itemRules = rules.filter((rule) => rule.scope === 'item' && rule.enabled !== false);
  const reportRules = rules.filter((rule) => rule.scope === 'report' && rule.enabled !== false);
 
  const items = context.inspectionItems.map((item) => ({ ...item }));
  /** 无判定依据的项数,只用来把这类项从合格率分母与报告结论里摘出去 */
  let undecidableCount = 0;
 
  items.forEach((item, index) => {
    if (itemRules.length > 0) {
      let passed = true;
      let failed = false;
      itemRules.forEach((rule) => {
        // 逐项规则在「单项上下文」里求值,item 指向当前行
        const scoped = { ...context, report: context.report, item, index: index + 1 };
        try {
          if (!evaluateConditionNode(compiledRule(rule.expression), scoped)) {
            passed = false;
          }
        } catch (error) {
          failed = true;
          errors.push(
            `第 ${index + 1} 项「${item.itemName}」的规则「${rule.name ?? rule.id ?? ''}」无法执行:${(error as Error).message}`,
          );
        }
      });
      applyResult(item, failed ? undefined : passed ? QUALITY_RESULT.PASS : QUALITY_RESULT.FAIL);
      return;
    }
 
    const byLimit = evaluateByLimit(item);
    if (byLimit !== undefined) {
      applyResult(item, byLimit ? QUALITY_RESULT.PASS : QUALITY_RESULT.FAIL);
      return;
    }
 
    const existing = normalizeExisting(item.result);
    if (!existing) {
      // 既没有规则、也没有判定依据:如实标出来,不要默认合格
      applyUndecidable(item);
      undecidableCount++;
      errors.push(
        `第 ${index + 1} 项「${item.itemName || '未命名'}」没有规格上下限也没有判定规则,无法判定`,
      );
      return;
    }
    applyResult(item, existing);
  });
 
  const passCount = items.filter((item) => item.result === QUALITY_RESULT.PASS).length;
  const failCount = items.filter((item) => item.result === QUALITY_RESULT.FAIL).length;
  const total = items.length;
  /** 合格率的分母只算「能判定」的项,无判定规则的项既不加分也不减分 */
  const decidableCount = total - undecidableCount;
 
  let reportResult: QualityResult | undefined;
  if (reportRules.length > 0) {
    let passed = true;
    let failed = false;
    reportRules.forEach((rule) => {
      const scoped = { ...context, inspectionItems: items, index: 0 };
      try {
        if (!evaluateConditionNode(compiledRule(rule.expression), scoped)) {
          passed = false;
        }
      } catch (error) {
        failed = true;
        errors.push(`报告级规则「${rule.name ?? rule.id ?? ''}」无法执行:${(error as Error).message}`);
      }
    });
    reportResult = failed ? undefined : passed ? QUALITY_RESULT.PASS : QUALITY_RESULT.FAIL;
  } else if (decidableCount > 0) {
    // 只有「全部可判定项都合格」才给合格结论。无判定规则的项不参与,它们没有对错可言。
    // 存在待判定项(规则跑挂)时不给结论:没验过不能说合格,没证据也不能说不合格,
    // 否则会出现「结论合格、合格率 0%」这种自相矛盾的报告。
    if (failCount > 0) {
      reportResult = QUALITY_RESULT.FAIL;
    } else if (passCount === decidableCount) {
      reportResult = QUALITY_RESULT.PASS;
    }
  }
 
  const report = {
    ...context.report,
    total,
    passCount,
    failCount,
    passRate: formatPassRate(passCount, decidableCount),
    result: reportResult ?? '',
    resultText: reportResult ? QUALITY_RESULT_TEXT[reportResult] : PENDING_TEXT,
    conclusion: reportResult ? QUALITY_RESULT_TEXT[reportResult] : PENDING_TEXT,
  };
 
  return { context: { report, inspectionItems: items }, errors };
}
 
/** 校验过的规则只解析一次,多行复用时直接对语法树求值 */
const ruleCache = new Map<string, RuleNode>();
 
function compiledRule(expression: string): RuleNode {
  const cached = ruleCache.get(expression);
  if (cached) {
    return cached;
  }
  const node = parseRule(expression);
  ruleCache.set(expression, node);
  return node;
}