多品种小批量的产线,一天换型七八次。温度少输一位、速度记错一个数,整批料报废还是小事,压力参数错了可能直接出安全事故。很多工厂的配方还活在 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-04PE-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操作时刻、登录人、当时角色快照(角色后来变了也能还原)
actionCREATE_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 校验,导入结果生成差异预览,确认后仍需提交审核才能发布;导出文件带版本号与导出时间水印。

八、验收清单

  1. 越权测试:用操作员账号尝试访问编辑接口、用工程师账号审核自己的配方,均被拒绝且有审计记录;
  2. 不可变测试:直接对数据库 PUBLISHED 行做 UPDATE 应失败(触发器/权限控制);同型号发布新版后旧版状态正确转 OBSOLETE 且历史批次仍指向旧版号;
  3. 校验测试:温度输入超出工艺上下限、枚举乱填、小数位超限,界面与下发前校验双重拦截;
  4. 下发故障注入:通信中断、PLC 回读值被人为改偏,验证 DISPATCH_FAIL、设备无法进自动、审计留差异明细;
  5. 追溯演练:随机抽一个月前的批次,5 分钟内查出当时配方版本、全部参数值、下发人、审核人、首件确认记录。

配方管理的本质是把"老师傅脑子里的工艺"变成组织受控的数字资产:元数据拦住错误输入,生命周期拦住随意修改,四重校验拦住错误下发,权限审计拦住责任真空。系统上线后最直观的变化是——换型不再需要师傅在场,而质量经理第一次能回答"这批货当时到底是按什么参数做的"。