多品种小批量的产线,一天换型七八次。温度少输一位、速度记错一个数,整批料报废还是小事,压力参数错了可能直接出安全事故。很多工厂的配方还活在 Excel、纸质工艺卡和老师傅的脑子里——谁改过、什么时候改的、现在 PLC 里跑的是哪一版,没人说得清。这篇做一套真正能过质量审计的 C# 配方管理:参数有模型、版本不可变、下发有校验、操作有留痕。
一、先立规矩:配方不是"一张参数表",而是受控文件
| 野生做法 | 风险 | 受控做法 |
|---|---|---|
| Excel/纸卡,换型照着敲 | 敲错无校验,版本散落各台电脑 | 系统统一存储,选择即下发,免手抄 |
| 谁都能在 HMI 上改参数 | 误改无痕,问题无法追溯 | 编辑/审核/下发三权分离 |
| 改完直接覆盖旧值 | 旧批次出问题时对不上当时参数 | 发布版本不可变,修改生成新版本 |
| 参数范围靠人记 | 85.5 输成 855 不报警 | 每个参数带类型、上下限、单位、枚举 |
| 下发成功与否不确认 | 通信失败一半参数没写进去,设备照样开 | 三拍握手 + 回读逐点比对 |
二、参数模型:把工艺约束写进数据结构
配方参数不能用"参数名 + 字符串值"的松散表,那样所有校验都得写在业务代码里、改一次工艺改一次程序。正确做法是每个参数带完整的元数据,界面、校验、下发全部由元数据驱动:
public class ParamSpec { public string Key { get; set; } // 参数键,与PLC通信DB偏移映射表对应 public string Name { get; set; } // "封口温度" public ParamType Type { get; set; } // Int/Real/Bool/Enum/String public string Unit { get; set; } // "℃",界面跟着参数走 public double? Min { get; set; } public double? Max { get; set; } public int Decimals { get; set; } // 小数位,输入控件直接限定位数 public string[] Allowed { get; set; } // 枚举可选值,下拉选择不让手输 public string PlcAddr { get; set; } // 如 DB2.DBD4,下发时用 } public class RecipeParam { public string Key; public string Value; // 统一字符串存储,下发前按Spec解析转换 } public class Recipe { public string Code; // 产品型号 A12-BLUE-500 public string Version; // 3.2,发布后冻结 public RecipeStatus Status; public string[] ApplicableLines; // 适用产线,跨线下发的硬边界 public List<RecipeParam> Params = new(); public string Remark; }
上下限按"工艺安全"定,不按设备量程定。比如封口温度设备能到 300℃,但这款膜过 200℃ 就熔穿,Max 应填 190 而不是 300。限值是质量工程师对工艺负责的签字项,不是程序员拍的。枚举型参数(材质、档位)一律下拉,从源头消灭"手滑输入"。
三、版本生命周期:发布即冻结,修改即新版
草稿 DRAFT工艺员可反复改
不允许下发
不允许下发
待审核 REVIEW提交后锁定编辑
审核人可驳回
审核人可驳回
已发布 PUBLISHED版本冻结可下发
任何修改派生新版
任何修改派生新版
已停用 OBSOLETE保留只读可查
禁止再下发
禁止再下发
- 版本号语义化:Major.Minor(如 3.2)。参数实质调整(温度/压力/速度)升 Minor;换产线适用范围、安全限值变化升 Major。草稿不带版本号,审核通过时才分配;
- 发布行不可变:PUBLISHED 版本的参数值、校验元数据在数据库层拒绝 UPDATE,只能"基于此版新建草稿"。这样三个月后追溯某批次时,看到的配方永远是当时那一版;
- 同一型号只允许一个"当前发布版",旧发布版自动转 OBSOLETE 但记录保留;历史批次记录里存的是
配方Code+Version,不是外键到可变表; - 驳回有理由:审核驳回必须填写原因,退回草稿人,全流转过程进审计表。
public class RecipeService { public void Submit(string draftId, string user) { var r = _repo.GetDraft(draftId); RequireRole(user, Role.Engineer); ValidateAll(r); // 提交前全量校验,不让带病配方进审核 r.Status = RecipeStatus.Review; _repo.Save(r); _audit.Log(user, "RECIPE_SUBMIT", draftId); } public void Approve(string draftId, string user, bool majorBump) { var r = _repo.GetDraft(draftId); RequireRole(user, Role.QaManager); // 审核人≠编辑人,数据库层也校验角色 if (r.CreatedBy == user) throw new InvalidOperationException("编辑人与审核人不能为同一人"); var frozen = r.FreezeAsPublished(NextVersion(r.Code, majorBump)); _repo.InsertPublished(frozen); // INSERT新行,历史发布版原封不动 _repo.ObsoleteOthers(r.Code); // 同型号旧发布版转停用(保留数据) _audit.Log(user, "RECIPE_APPROVE", $"{r.Code}@{frozen.Version}"); } }
四、版本差异对比:审核人和工艺员都需要它
审核环节不能要求人拿两版参数表逐行肉眼对。提供参数级 diff:审核界面按 Key 对齐两版,标出新增、删除、变化的值,变化项显示旧→新与差值百分比。代码核心就是按 Key 做全外连接:
public List<ParamDiff> Diff(Recipe a, Recipe b) { var mapA = a.Params.ToDictionary(p => p.Key); var mapB = b.Params.ToDictionary(p => p.Key); var result = new List<ParamDiff>(); foreach (var key in mapA.Keys.Union(mapB.Keys).OrderBy(k => k)) { mapA.TryGetValue(key, out var pa); mapB.TryGetValue(key, out var pb); if (pa == null) result.Add(new ParamDiff(key, null, pb.Value, DiffKind.Added)); else if (pb == null) result.Add(new ParamDiff(key, pa.Value, null, DiffKind.Removed)); else if (pa.Value != pb.Value) result.Add(new ParamDiff(key, pa.Value, pb.Value, DiffKind.Changed)); } return result; }
| 参数 | 3.1 旧版 | 3.2 新版 | 变化 |
|---|---|---|---|
| 封口温度 | 178.0 ℃ | 182.0 ℃ | +4.0(+2.2%) |
| 灌装速度 | 1200 瓶/时 | 1350 瓶/时 | +150(+12.5%) |
| 膜材质 | PE-04 | PE-04 | — |
| 氮气压力 | — | 0.25 MPa | 新增 |
五、下发:选择到生效之间的四重校验
下发不是"把值写进 PLC"那么简单,一条配方从被选择到设备真正跑起来,要过四道关,任何一关失败都中止并给出明确原因:
| 关卡 | 校验内容 | 失败处置 |
|---|---|---|
| ① 状态与权限 | 配方必须 PUBLISHED;用户具备下发权限;设备在手动/待机态(运行中禁止换型) | 直接拒绝,记录尝试 |
| ② 适用性 | 配方适用产线包含当前设备;产品型号与工单一致(MES 工单校验) | 拒绝,提示型号/工单不符 |
| ③ 参数值 | 按每个 ParamSpec 再做一次范围/枚举/类型校验(防历史脏数据和手工导库) | 列出全部不合格参数,禁止下发 |
| ④ 回读比对 | 写完全部参数后逐点读回,按容差与下发值比对,全部一致才算成功 | 列出不一致点,配方标记下发失败,设备不许启动 |
public async Task<DispatchResult> DispatchAsync(string code, string version, string line, string user) { var recipe = _repo.GetPublished(code, version) ?? throw new BusinessEx("配方不存在或未发布"); // ①② 状态/权限/适用性 RequireRole(user, Role.Operator); if (!recipe.ApplicableLines.Contains(line)) throw new BusinessEx("该配方不适用于此产线"); if (!await _plc.IsIdleAsync(line)) throw new BusinessEx("设备运行中,禁止换型"); if (!await _mes.MatchWorkOrder(code)) throw new BusinessEx("与当前工单型号不一致"); // ③ 参数校验(Spec随配方模板走) var errors = ValidateAgainstSpecs(recipe); if (errors.Count > 0) return DispatchResult.Fail(errors); // ④ 三拍握手整块下发(见S7一篇),成功后回读逐点比对 await _plc.WriteParamsWithHandshakeAsync(line, recipe); var readback = await _plc.ReadParamsAsync(line, recipe.Specs); var mismatch = CompareWithTolerance(recipe, readback); if (mismatch.Count > 0) { _audit.Log(user, "RECIPE_DISPATCH_FAIL", $"{code}@{version}", mismatch.ToJson()); return DispatchResult.Fail(mismatch); // 联动:PLC不收到确认不允许进自动 } // 成功:当前批次冻结引用"code@version+下发人+时间",这是追溯链入口 _runState.SetActiveRecipe(line, code, version, user); _audit.Log(user, "RECIPE_DISPATCH_OK", $"{code}@{version} -> {line}"); return DispatchResult.Ok(); }
回读必须读 PLC 实际生效区,不能读回显。有些程序写完参数把自己界面上的值再"比对"一遍,等于没验。要从 PLC 的参数 DB 重新读回,Real 类型按工程容差比对(如温度 ±0.2℃),Bool/枚举要求完全一致。比对未全过,上位机不给 PLC 发"允许自动"确认位。
六、权限分离与审计:四角色 + 全留痕
工艺工程师新建/编辑草稿、提交审核;不能审核、不能直接发布
质量主管审核批准/驳回;不能自己编配方,关键变更双人复核
操作员只能选择已发布配方并下发;不可改值
系统管理员管账号和产线配置;不碰工艺参数值
| 审计字段 | 说明 |
|---|---|
| time / user / role | 操作时刻、登录人、当时角色快照(角色后来变了也能还原) |
| action | CREATE_DRAFT / EDIT / SUBMIT / APPROVE / REJECT / DISPATCH_OK / DISPATCH_FAIL / ROLLBACK |
| target | 配方 Code@Version、产线、工单 |
| before / after | 编辑和下发类操作记录变更前后值(JSON),只追加不修改不删除 |
| machine / ip | 哪台工控机、什么 IP,便于查共用账号 |
有客户审核(尤其医药、食品、汽车零部件)时,这套表就是 21 CFR Part 11 / IATF 16949 思路的现场证据:谁在什么时候、基于哪份工单、把哪一版配方的哪些参数下发到了哪条线,全部可回放。紧急改参数也要走流程:班长权限做"临时覆盖",参数生效但该批次自动标记"非标准参数生产",24 小时内必须补审核,逾期报警。
七、换型防错与多机管理
- 物料扫码联动:配方里声明所需物料清单与条码规则,换型时逐桶扫料,系统比对 BOM,料不对齐直接红灯拦截,杜绝"A 配方用了 B 料";
- 首件确认闭环:换型后首批产品必须经质检录入合格,设备才解除"首件锁定"进入正常节拍——把参数问题挡在第一批;
- 多产线统一下发:配方库集中部署(车间服务器),各线上位机拉取已发布版本,本地缓存最近使用的版本供断网时使用;不允许本地私自新建,缓存版本带签名,防止离线被篡改;
- 导入导出要校验:Excel/JSON 导入走与手工录入完全相同的 Spec 校验,导入结果生成差异预览,确认后仍需提交审核才能发布;导出文件带版本号与导出时间水印。
八、验收清单
- 越权测试:用操作员账号尝试访问编辑接口、用工程师账号审核自己的配方,均被拒绝且有审计记录;
- 不可变测试:直接对数据库 PUBLISHED 行做 UPDATE 应失败(触发器/权限控制);同型号发布新版后旧版状态正确转 OBSOLETE 且历史批次仍指向旧版号;
- 校验测试:温度输入超出工艺上下限、枚举乱填、小数位超限,界面与下发前校验双重拦截;
- 下发故障注入:通信中断、PLC 回读值被人为改偏,验证 DISPATCH_FAIL、设备无法进自动、审计留差异明细;
- 追溯演练:随机抽一个月前的批次,5 分钟内查出当时配方版本、全部参数值、下发人、审核人、首件确认记录。
配方管理的本质是把"老师傅脑子里的工艺"变成组织受控的数字资产:元数据拦住错误输入,生命周期拦住随意修改,四重校验拦住错误下发,权限审计拦住责任真空。系统上线后最直观的变化是——换型不再需要师傅在场,而质量经理第一次能回答"这批货当时到底是按什么参数做的"。
