4. 批记录必填项与逻辑校验规则配置
校验规则的分层设计
批记录提交前的校验分为两层:必填校验负责“有没有填”,逻辑校验负责“填得对不对”。两层必须串联执行——先过必填,再算逻辑,否则空值参与计算会产生误判。本例以某产品A的配料工序批记录为对象,配置投料确认、中间控制结果、清场确认三类字段的规则。
规则配置表
把规则写成可配置的字段级定义,便于在批记录系统中维护,也便于测试时逐条对照。下表为产品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%”这类误导性错误,掩盖真正的缺失问题。
预警与拦截要区分处理:预警允许提交但要求复核说明,拦截则禁止提交。若把预警也做成硬拦截,操作员会倾向于修改数据绕过校验,反而削弱数据完整性。
结果判读与配置要点
- 拦截提示要定位到字段:错误信息应包含字段名与当前值,便于操作员直接修正,而不是笼统提示“校验失败”。
- 阈值来自生效版本模板:偏差阈值和工艺范围应随批记录模板版本走,避免不同版本共用一套硬编码阈值。
- 测试覆盖三类异常:空值、越界值、逻辑冲突(如清场人与复核人相同),每类至少一条用例,确认拦截生效且提示准确。
- 校验结果留痕:拦截与预警事件应写入审计追踪,记录触发规则、输入值和操作人,便于后续追溯。