4. 批记录必填项与逻辑校验规则配置

校验规则的分层设计

批记录提交前的校验分为两层:必填校验负责“有没有填”,逻辑校验负责“填得对不对”。两层必须串联执行——先过必填,再算逻辑,否则空值参与计算会产生误判。本例以某产品A的配料工序批记录为对象,配置投料确认、中间控制结果、清场确认三类字段的规则。

批记录提交校验链:必填层 → 逻辑层 → 拦截/放行 批记录输入字段 投料确认 物料/实际量/签名 中间控制结果 pH/温度/含量 清场确认 清场人/复核人/时间 配方理论量 来自生效版本模板 提交动作 触发校验 第一层:必填校验 投料确认.实际量 ≠ 空 投料确认.操作员签名 ≠ 空 中间控制.pH/温度 ≠ 空 清场确认.清场人 ≠ 空 清场确认.复核人 ≠ 空 清场确认.时间 ≠ 空 任一为空 → 拦截 第二层:逻辑校验 偏差% = |实际−理论|/理论×100 偏差 ≤ 2.0% → 通过 2.0% < 偏差 ≤ 5.0% → 预警 偏差 > 5.0% → 超限拦截 pH 6.5–7.5,温度 20–25℃ 清场时间 ≥ 上批结束时间 超限/越界 → 拦截 放行 可提交 拦截 禁止提交 空值直接拦截 示例:物料M-01 理论量 500.0 kg 实际 508.0 kg → 偏差 1.6% → 通过 实际 515.0 kg → 偏差 3.0% → 预警(需复核说明) 实际 530.0 kg → 偏差 6.0% → 超限拦截 阈值 2.0% / 5.0% 为示例配置,实际以工艺规程为准

规则配置表

把规则写成可配置的字段级定义,便于在批记录系统中维护,也便于测试时逐条对照。下表为产品A配料工序的配置样例:

字段校验类型规则表达式不通过时动作
投料确认.实际量必填value != null && value > 0拦截,提示“实际量未填写”
投料确认.操作员签名必填signature != null拦截,提示“缺少操作员签名”
中间控制.pH必填+逻辑6.5 ≤ pH ≤ 7.5越界拦截,提示“pH超出工艺范围”
中间控制.温度必填+逻辑20 ≤ T ≤ 25越界拦截,提示“温度超出工艺范围”
投料偏差逻辑|实际−理论|/理论×100 ≤ 2.0>2.0 预警;>5.0 拦截
清场确认.清场人/复核人必填两者均非空且不相同拦截,提示“清场人与复核人不可相同”
清场确认.时间逻辑清场时间 ≥ 上批结束时间拦截,提示“清场时间早于上批结束”

规则引擎实现(关键代码)

以下为校验引擎的核心实现,采用“规则列表 + 逐条执行 + 收集错误”的方式,便于扩展和测试。偏差阈值与工艺范围作为参数传入,不硬编码在函数内。

// 校验规则引擎:先必填,后逻辑
function validateBatchRecord(record, config) {
  const errors = [];
  const warnings = [];

  // 第一层:必填校验
  const requiredFields = [
    ['投料确认.实际量', record.feeding?.actualQty],
    ['投料确认.操作员签名', record.feeding?.operatorSign],
    ['中间控制.pH', record.ipc?.ph],
    ['中间控制.温度', record.ipc?.temp],
    ['清场确认.清场人', record.cleaning?.cleaner],
    ['清场确认.复核人', record.cleaning?.reviewer],
    ['清场确认.时间', record.cleaning?.time],
  ];
  for (const [name, val] of requiredFields) {
    if (val === null || val === undefined || val === '') {
      errors.push(`必填项缺失:${name}`);
    }
  }
  if (errors.length > 0) return { ok: false, errors, warnings };

  // 第二层:逻辑校验
  const { phMin, phMax, tempMin, tempMax, warnPct, blockPct } = config;

  if (record.ipc.ph < phMin || record.ipc.ph > phMax) {
    errors.push(`pH ${record.ipc.ph} 超出范围 [${phMin}, ${phMax}]`);
  }
  if (record.ipc.temp < tempMin || record.ipc.temp > tempMax) {
    errors.push(`温度 ${record.ipc.temp}℃ 超出范围 [${tempMin}, ${tempMax}]`);
  }

  // 投料偏差
  const theory = record.feeding.theoryQty;
  const actual = record.feeding.actualQty;
  const devPct = Math.abs(actual - theory) / theory * 100;
  if (devPct > blockPct) {
    errors.push(`投料偏差 ${devPct.toFixed(2)}% 超过拦截阈值 ${blockPct}%`);
  } else if (devPct > warnPct) {
    warnings.push(`投料偏差 ${devPct.toFixed(2)}% 超过预警阈值 ${warnPct}%`);
  }

  // 清场人与复核人不可相同
  if (record.cleaning.cleaner === record.cleaning.reviewer) {
    errors.push('清场人与复核人不可为同一人');
  }
  // 清场时间不得早于上批结束时间
  if (record.cleaning.time < record.lastBatchEndTime) {
    errors.push('清场时间早于上批结束时间');
  }

  return { ok: errors.length === 0, errors, warnings };
}

异常输入拦截测试

用一组构造的输入验证拦截效果。测试数据均为教学示例,阈值取 pH 6.5–7.5、温度 20–25℃、预警 2.0%、拦截 5.0%。

const config = { phMin: 6.5, phMax: 7.5, tempMin: 20, tempMax: 25, warnPct: 2.0, blockPct: 5.0 };

const base = {
  feeding: { theoryQty: 500, actualQty: 508, operatorSign: 'OP-001' },
  ipc: { ph: 7.0, temp: 22 },
  cleaning: { cleaner: 'C-01', reviewer: 'R-01', time: '2025-03-10T14:00' },
  lastBatchEndTime: '2025-03-10T12:00'
};

// 用例1:正常输入 → 放行
console.log(validateBatchRecord(base, config));
// { ok: true, errors: [], warnings: [] }

// 用例2:实际量缺失 → 必填拦截
const t2 = structuredClone(base); t2.feeding.actualQty = null;
console.log(validateBatchRecord(t2, config));
// { ok: false, errors: ['必填项缺失:投料确认.实际量'], warnings: [] }

// 用例3:投料偏差 6% → 超限拦截
const t3 = structuredClone(base); t3.feeding.actualQty = 530;
console.log(validateBatchRecord(t3, config));
// { ok: false, errors: ['投料偏差 6.00% 超过拦截阈值 5%'], warnings: [] }

// 用例4:投料偏差 3% → 预警但可提交
const t4 = structuredClone(base); t4.feeding.actualQty = 515;
console.log(validateBatchRecord(t4, config));
// { ok: true, errors: [], warnings: ['投料偏差 3.00% 超过预警阈值 2%'] }

// 用例5:pH 越界 → 拦截
const t5 = structuredClone(base); t5.ipc.ph = 8.2;
console.log(validateBatchRecord(t5, config));
// { ok: false, errors: ['pH 8.2 超出范围 [6.5, 7.5]'], warnings: [] }

// 用例6:清场人与复核人相同 → 拦截
const t6 = structuredClone(base); t6.cleaning.reviewer = 'C-01';
console.log(validateBatchRecord(t6, config));
// { ok: false, errors: ['清场人与复核人不可为同一人'], warnings: [] }
必填校验必须先于逻辑校验执行。若先算偏差,空值会被当作 0 参与计算,产生“偏差 100%”这类误导性错误,掩盖真正的缺失问题。
预警与拦截要区分处理:预警允许提交但要求复核说明,拦截则禁止提交。若把预警也做成硬拦截,操作员会倾向于修改数据绕过校验,反而削弱数据完整性。

结果判读与配置要点

  • 拦截提示要定位到字段:错误信息应包含字段名与当前值,便于操作员直接修正,而不是笼统提示“校验失败”。
  • 阈值来自生效版本模板:偏差阈值和工艺范围应随批记录模板版本走,避免不同版本共用一套硬编码阈值。
  • 测试覆盖三类异常:空值、越界值、逻辑冲突(如清场人与复核人相同),每类至少一条用例,确认拦截生效且提示准确。
  • 校验结果留痕:拦截与预警事件应写入审计追踪,记录触发规则、输入值和操作人,便于后续追溯。