:生产追溯,条码触发自动记录)
生产追溯条码触发自动记录这是「从零搭建灌装监控系统」系列第10篇。上一篇把 SQLite 和 FreeSql 接通了但数据库不会自动知道一批产品什么时候开始、什么时候结束。这篇从 PLC 轮询到的条码变化入手实现去重、记录快照和异常保护让“看见一个新条码”真正变成一条生产记录。条码读到了为什么数据库里有三条第一次把条码接到轮询循环时代码很直观每次收到设备状态就读取条码只要条码不为空就插入一条记录。测试人员扫入一个条码界面上很快出现了记录。看起来没问题。过了一会儿打开数据库发现同一个条码有几十条。这不是数据库重复提交也不是扫描枪坏了。轮询本来就会持续运行200ms 一次意味着同一个条码在传感器保持期间会被读到很多次。条码是状态入库动作是事件。如果没有把这两个概念分开重复记录只是时间问题。这一篇做的事情不复杂保存上一次处理过的条码只有从“没有条码”到“出现新条码”或从“旧条码”到“新条码”时才创建记录同时把当时的工艺数据冻结下来。先区分状态和事件设备状态可以这样理解当前状态Barcode B20260923001 下一次轮询Barcode B20260923001 再下一次轮询Barcode B20260923001这是同一个状态不是三个生产事件。只有下面的变化才值得触发入库空字符串 - B20260923001 开始记录 B20260923001 - B20260923001 不处理 B20260923001 - B20260923002 新批次记录 B20260923001 - 空字符串 条码离开结束当前状态代码中的_lastBarcode就是一个小型状态机记忆。它不是数据库也不是缓存系统只负责回答一个问题这次看到的条码和上次相比有没有变化是否是否是否轮询读到条码条码为空?清空 _lastBarcode与上次相同?跳过不入库写生产记录写入成功?更新 _lastBarcode保留旧值下次重试最小实现变化时才保存在 DashboardViewModel 或专门的生产追溯服务里可以写出这样的逻辑privatestring_lastBarcode;privatereadonlySemaphoreSlim_recordLocknew(1,1);privateasyncTaskHandleDeviceStateAsync(DeviceStatesstate){varbarcodestate.Barcode?.Trim()??;if(string.IsNullOrWhiteSpace(barcode)){_lastBarcode;return;}if(string.Equals(barcode,_lastBarcode,StringComparison.OrdinalIgnoreCase))return;_lastBarcodebarcode;awaitSaveNewRecordAsync(barcode,state);}这里先Trim避免扫码器在末尾带回车或空格时把同一个条码当成不同字符串。比较时是否忽略大小写要根据业务规则决定。很多工厂条码是大小写敏感的不能机械地统一转小写。示例使用忽略大小写只是为了说明“同一条码”的判断点正式项目应以条码规范为准。保存方法负责组装快照privateasyncTaskSaveNewRecordAsync(stringbarcode,DeviceStatesstate){await_recordLock.WaitAsync();try{varrecordnewProductionRecord{TimeDateTime.Now,BatchNobarcode,Volumestate.CurrentVolume,Temperaturestate.Temperature,CycleTimestate.CurrentCycleTime,OperatorCurrentUser.Name};awaitDataService.SaveProductionRecordAsync(record);LogService.Info($生产记录已保存{barcode});}finally{_recordLock.Release();}}为什么要保存快照而不是只保存条码因为设备状态会继续变化。把当前液位、温度、周期时间等字段写入记录后后续查询看到的是“当时发生了什么”而不是“现在设备是什么状态”。_lastBarcode放在哪里放在 ViewModel 里可以快速完成但它有一个明显限制页面重新创建状态就可能丢失。更稳妥的做法是把去重逻辑放到独立服务中publicsealedclassProductionTraceService{privatestring_lastBarcode;privatereadonlySemaphoreSlim_gatenew(1,1);publicasyncTaskboolTryRecordAsync(DeviceStatesstate,stringoperatorName){varbarcodestate.Barcode?.Trim()??;if(barcode.Length0){_lastBarcode;returnfalse;}await_gate.WaitAsync();try{if(barcode_lastBarcode)returnfalse;varrecordProductionRecord.From(state,operatorName);awaitDataService.SaveProductionRecordAsync(record);_lastBarcodebarcode;returntrue;}finally{_gate.Release();}}}这里把_lastBarcode的更新放在数据库写入成功之后。这样做的好处是如果数据库写失败下一次轮询仍然会再次尝试而不是因为内存状态已经更新导致这条记录永远丢失。不过也要看到它的边界如果数据库写成功、进程在更新_lastBarcode前崩溃重启后可能再次写入。真正需要强一致去重时不能只靠内存变量要给数据库增加唯一约束或记录业务键。用业务键做第二道保险生产记录可以增加一个业务键例如“条码 开始时间”或设备侧的批次号。SQLite 里把业务键设置为唯一重复写入时由数据库拒绝。publicclassProductionRecord{[Column(IsIdentitytrue,IsPrimarytrue)]publiclongId{get;set;}[Column(IsNullablefalse,StringLength64)]publicstringBatchNo{get;set;};publicDateTimeTime{get;set;}publicstringRecordKey{get;set;};}如果一个条码可以重复生产就不能简单地把BatchNo设置成唯一。可以让设备批次号、开始时间窗口或生产流水号参与业务键。关键是先问清楚业务同一条码重复出现代表重复扫描还是代表返工技术方案必须服从这个答案。数据库唯一约束是最后一道防线不是替代业务判断。把每一次重复都交给数据库抛异常会让日志充满“正常重复”的错误看起来像系统经常失败。条码不是永远可靠现场采集还有几种情况需要处理条码分段到达如果条码由多个寄存器组成某一次轮询可能只读到前半段。不能看到非空字符串就马上入库需要确认长度、结束符或连续两次读取一致。privatestring?_candidateBarcode;privateint_stableCount;privateboolIsStable(stringbarcode){if(barcode_candidateBarcode)_stableCount;else{_candidateBarcodebarcode;_stableCount1;}return_stableCount2;}条码包含不可见字符回车、换行和零字节在寄存器转换时很常见。清洗逻辑要集中处理并保留原始数据的诊断日志但日志里不要输出完整的敏感生产编码。通信重连后重复上报断线重连后设备可能把当前条码再次上报。只要_lastBarcode没有被错误清空就不会重复插入。只有收到明确的“条码离开”状态才应该把上一次条码置空。记录失败和 UI 不能互相拖住轮询线程发现新条码后不应该同步等待一个慢数据库操作再继续读取设备。可以把入库交给后台任务但要用队列或锁控制并发不能每次轮询都Task.Run一把privatereadonlyChannelProductionRecord_recordsChannel.CreateUnboundedProductionRecord();privateasyncTaskWriterLoopAsync(CancellationTokentoken){awaitforeach(varrecordin_records.Reader.ReadAllAsync(token)){try{awaitDataService.SaveProductionRecordAsync(record);}catch(Exceptionex){LogService.Error(保存生产记录失败,ex);}}}文章前面的最小实现适合数据量小、链路简单的项目。数据量上来后单独的写入队列更容易做重试、统计和停机排空。无论采用哪种方案都要保证“采集线程不被数据库卡死”和“写入失败不会被静默吞掉”。踩坑记录只判断非空不判断变化这是重复数据的根因。状态持续存在不等于事件持续发生。先更新_lastBarcode再写数据库写库失败后下一次不会再尝试造成静默丢记录。除非使用了可靠队列否则应在写入成功后更新状态。把条码清洗写在多个页面一个页面去空格另一个页面转大小写最后同一条码在不同功能里变成不同值。清洗、校验和去重应该集中在追溯服务。用 UI 集合当数据库UI 集合适合显示最近记录不适合承担持久化职责。页面关闭、导航切换或程序重启都会让集合失效。本篇小结问题做法轮询重复读取怎么办把状态变化转换成事件如何避免重复记录_lastBarcode 数据库业务键保存什么数据条码和当时的设备状态快照数据库写失败怎么办不提前更新状态记录错误并重试页面重建会丢状态吗将追溯逻辑放到独立服务采集和入库如何解耦小项目用锁大项目用写入队列到这里监控系统已经能留下生产记录。下一步要解决的是数据不能只躺在数据库里现场还需要把它交给 Excel、质量系统或客户。下期预告第11篇CSV 流式导出下一篇实现按时间范围查询生产记录、分批写入 CSV、处理中文编码和大数据量导出重点是避免一次性把所有数据塞进内存。