ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用C#开发MES系统:订单管理与生产概况的落地实现

用C#开发MES系统:订单管理与生产概况的落地实现 简介一套基于 C# 与 WPF 技术栈开发的 MES 制造执行系统源码面向制造业信息化开发人员及 WPF 桌面应用学习者。系统围绕订单管理和生产概况两大核心模块展开订单管理覆盖加工、检测、拧螺丝、轴承压装四类工单每类订单对应不同流程操作支持作业计划下达与生产派工操作人员可在人工操作台完成立库上下料其中轴承托盘 C 和螺钉托盘 D 上料需指定数量参数生产概况则集中展示立库、AGV、生产区/检测区/装配区机器人及人工上下料台的运行状态AGV 位置和立库操作均细分多种情形。压缩包内共 313 个文件体积约 11.94MB以 88 个 cs 源文件、62 个 dll 库、45 个 xml 配置及 12 个 exe 为主同时包含 mdf/ldf 数据库文件和 sln 工程文件便于直接编译运行与二次开发。已有 4163 人浏览学习。源码结构清晰对理解 MES 与自动化产线AGV、立库、机器人之间的信息交互具有实际参考价值。1. 用C#写MES订单管理只是入口生产概况才是刚需车间里最常见的一幕是PMC拿着Excel排产车间主任盯着白板问“今天能出多少”设备一停就是半小时没人知道。这时候老板说“上MES”第一个被推上台的往往是写C#上位机或企业信息系统的工程师。你会发现订单管理听着像常规CRUD生产概况听着像几个图表真正跑起来才明白卡住你的不是页面而是数据从哪来、怎么聚、怎么刷。这篇文章就顺着“C#写MES系统实现订单管理和生产概况”这条线把一套能做出来的方案讲透订单状态怎么流转、扫码枪怎么把条码变成数据、生产概况为什么不能只靠定时器刷页面以及怎么解决C#上位机场景里最常见的“循环采集导致UI卡顿”问题。适合正在做或准备做车间信息化、MES、设备数据采集的.NET工程师也适合带团队做项目的人用来对方案边界。全文不依赖第三方商业框架拿ASP.NET Core Web API加WPF或Web前端就能把核心链路跑通。2. 先想清楚订单和生产概况的关系再写第一行代码2.1 订单管理不只是增删改查状态流转才是灵魂MES里的订单和ERP订单最大的区别是ERP管“要不要做、做多少”MES管“现在做到哪一步”。所以MES里的订单一定带着状态机常见的状态是Created已创建、Released已下达、InProgress生产中、Completed已完工、Closed已关闭另外还要考虑OnHold挂起和Cancelled取消。很多初版MES把订单状态做成一个字符串字段页面随意改结果就是“订单明明在生产车间主任却点了完工”数据全乱了。正确做法是把状态迁移规则集中管理比如InProgress状态不允许直接跳到Closed必须先CompletedCancelled状态是终态不能恢复。状态机的好处不仅是防错还让后续的权限、通知、报表全部有了挂载点。比如订单进入InProgress时自动给班组长推送消息这个逻辑写在状态迁移的同一处代码里而不是散落在按钮点击事件中。2.2 生产概况不是查出来的是聚出来的生产概况页面上那些数字——今日计划数、完成数、良率、设备OEE、在制品数量——看起来像SQL聚合查询实际上背后是一连串业务事件在支撑。事件源头有三个第一是报工。操作工每做完一批活在机台或扫码枪上录入数量报工记录表插一行。生产概况里的“完成数”就是报工记录的汇总。第二是设备采集。PLC、扫码枪、传感器通过Modbus、OPC UA或TCP Socket把数据送到MES比如当前产量、设备状态、报警信息。第三是质量检验。每个批次抽检结果回写良率和合格数才有出处。换句话说如果车间里的数据采集没做好生产概况页面做得再漂亮也只是“演示版”。有不少MES项目死在这里业务方要求看实时产出但机台根本没有数据采集最后只能靠人工录入Excel再导入等于把MES做成了报表工具。2.3 最小可落地的表结构设计直接给一套够用的表结构SQL Server或PostgreSQL都适用CREATE TABLE WorkOrder ( Id BIGINT IDENTITY PRIMARY KEY, OrderNo NVARCHAR(50) NOT NULL UNIQUE, ProductCode NVARCHAR(50) NOT NULL, PlannedQty INT NOT NULL, CompletedQty INT NOT NULL DEFAULT 0, Status NVARCHAR(20) NOT NULL DEFAULT Created, PlanStartTime DATETIME NULL, PlanEndTime DATETIME NULL, CreatedTime DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE WorkReport ( Id BIGINT IDENTITY PRIMARY KEY, WorkOrderId BIGINT NOT NULL REFERENCES WorkOrder(Id), OperatorCode NVARCHAR(50) NOT NULL, ProcessCode NVARCHAR(50) NOT NULL, ReportQty INT NOT NULL, GoodQty INT NOT NULL, ReportTime DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE EquipmentRealtime ( Id BIGINT IDENTITY PRIMARY KEY, EquipmentCode NVARCHAR(50) NOT NULL, CurrentQty INT NOT NULL DEFAULT 0, Status NVARCHAR(20) NOT NULL, -- Running/Idle/Alarm LastUpdateTime DATETIME NOT NULL DEFAULT GETDATE() );这三个表是核心骨架WorkOrder存订单主数据WorkReport存报工明细EquipmentRealtime存设备实时数据。生产概况页面的“今日完成数”就是查WorkReport按日期聚合“设备状态”就是查EquipmentRealtime表的最新值。表结构设计上有一个关键取舍设备实时数据是高频写入的如果和订单数据放在同一个表里频繁更新会带来锁竞争。常见的做法是把实时数据单独放一张表并且用“覆盖写”的方式只保留最新值历史数据另写采集明细表。2.4 为什么这个场景特别适合C#和.NET选C#不只是因为标题写了C#。MES系统处在企业信息系统和车间设备之间的夹层一边要对接ERP的接口通常是Web API或数据库视图一边要对接PLC、扫码枪、称重仪这些硬件设备。C#在这两端的生态都很成熟对接ERP用ASP.NET Core Web API对接设备有Modbus库、OPC UA库、串口通信库WPF做上位机界面也是老本行。再加上车间里常见的扫码枪、打印机、LED看板的SDK大多数提供C#的示例代码用C#写MES能把“设备接入”这件事的沟通成本压到最低。Java体系在这块也能做但遇到硬件设备的二次开发时C#能直接调厂商提供的DLL省去一层封装。3. 订单管理模块怎么实现状态机、扫码枪和一个可续的看板3.1 用状态机类把订单流转管起来别到处写if直接写一个泛型状态机可能过度设计但一个订单专用的状态迁移类非常有必要。下面这个类是实际项目中够用的版本public class WorkOrderStateMachine { private static readonly DictionaryOrderStatus, ListOrderStatus Transitions new DictionaryOrderStatus, ListOrderStatus { [OrderStatus.Created] new ListOrderStatus { OrderStatus.Released, OrderStatus.Cancelled }, [OrderStatus.Released] new ListOrderStatus { OrderStatus.InProgress, OrderStatus.OnHold, OrderStatus.Cancelled }, [OrderStatus.InProgress] new ListOrderStatus { OrderStatus.Completed, OrderStatus.OnHold }, [OrderStatus.OnHold] new ListOrderStatus { OrderStatus.InProgress, OrderStatus.Cancelled }, [OrderStatus.Completed] new ListOrderStatus { OrderStatus.Closed }, [OrderStatus.Closed] new ListOrderStatus(), // 终态 [OrderStatus.Cancelled] new ListOrderStatus() // 终态 }; public static bool CanTransition(OrderStatus current, OrderStatus target) { return Transitions.ContainsKey(current) Transitions[current].Contains(target); } }代码的逻辑是状态迁移规则集中在一张字典表里CanTransition只做一件事——判断当前状态能否迁移到目标状态。在Web API的Service层调用状态下单接口时public async Task ChangeOrderStatusAsync(long orderId, OrderStatus target) { var order await _db.WorkOrders.FindAsync(orderId); if (!WorkOrderStateMachine.CanTransition(order.Status, target)) { throw new InvalidOperationException( $订单状态不能从{order.Status}变更为{target}); } // 记录状态变更日志 order.Status target; await _db.SaveChangesAsync(); }参数说明OrderStatus是枚举类型对应订单在系统中的全部合法状态Transitions字典的key是当前状态value是允许迁移到的目标状态集合。这样做的直接好处是新增状态或修改迁移规则时只改这一个类不用全局搜索按钮事件里的if判断。对于MES这种多人协作、需求会演进的系统状态迁移集中管理几乎是刚需。3.2 扫码枪触发事件把条码变成订单数据的入口MES里最常见的硬件交互就是扫码枪。市面上的USB扫码枪多数模拟键盘输入也就是扫码后像敲键盘一样把字符串送进焦点所在的输入框。在这个前提下WPF里触发扫码事件的标准做法是全局键盘钩子监听避免焦点问题产生的漏码。private void Window_PreviewKeyDown(object sender, KeyEventArgs e) { // 回车代表一次扫码结束 if (e.Key Key.Enter _barcodeBuffer.Length 0) { string barcode _barcodeBuffer.ToString(); _barcodeBuffer.Clear(); e.Handled true; // 把条码交给订单处理逻辑 await ProcessBarcodeAsync(barcode); return; } // 过滤Shift等控制键只保留可打印字符 if (e.Key Key.A e.Key Key.Z || e.Key Key.D0 e.Key Key.D9) { _barcodeBuffer.Append(GetCharFromKey(e.Key, Keyboard.Modifiers)); e.Handled true; } }代码逻辑扫码枪每扫一个码会快速输入一串字符并以回车结尾。_barcodeBuffer是StringBuilder用于暂存字符直到收到回车然后整体交给ProcessBarcodeAsync处理。e.Handled true是为了防止字符漏进其他控件的文本里。关于触发事件有两点实战经验一是不要使用KeyDown而要用PreviewKeyDown隧道事件能避免焦点控件把按键消费掉二是要处理输入法问题如果在中文输入法下扫码偶发丢字符常见的做法是扫码输入框禁用输入法InputMethod.IsInputMethodEnabledFalse或者用KeyDown配合Keyboard.Modifiers进行一次“按键—字符”映射而不是依赖TextInput事件。条码拿到后是一个解析流程比如工单号工序号员工号的组合编码则拆分字符串并按编码规则校验然后查订单信息确认当前状态允许报工写入报工记录。这一连串动作都在ProcessBarcodeAsync里完成超时或条码格式错误时给出声音和界面双重提示。3.3 生产概况看板接口一次查询返回所有聚合指标订单管理和生产概况在前端页面是两个视图但后台可以共用一个聚合接口。下面这个接口返回今日生产概况所需的核心数据[HttpGet(api/dashboard/today)] public async TaskIActionResult GetTodayOverview() { var today DateTime.Today; var start today; var end today.AddDays(1); var reportSummary await _db.WorkReports .Where(r r.ReportTime start r.ReportTime end) .GroupBy(r 1) .Select(g new { TotalReportQty g.Sum(r r.ReportQty), TotalGoodQty g.Sum(r r.GoodQty), ReportCount g.Count() }) .FirstOrDefaultAsync(); var orderSummary await _db.WorkOrders .Where(o o.Status OrderStatus.InProgress || o.Status OrderStatus.Released) .GroupBy(o 1) .Select(g new { ActiveOrderCount g.Count(), PlannedQtySum g.Sum(o o.PlannedQty), CompletedQtySum g.Sum(o o.CompletedQty) }) .FirstOrDefaultAsync(); return Ok(new { TotalPlanQty orderSummary?.PlannedQtySum ?? 0, TotalCompletedQty orderSummary?.CompletedQtySum ?? 0, TodayReportQty reportSummary?.TotalReportQty ?? 0, TodayGoodQty reportSummary?.TotalGoodQty ?? 0, TodayYieldRate reportSummary?.TotalReportQty 0 ? Math.Round((double)reportSummary.TotalGoodQty / reportSummary.TotalReportQty * 100, 2) : 0, ActiveOrderCount orderSummary?.ActiveOrderCount ?? 0 }); }这段代码的作用是一次接口调用同时返回计划总量、累计完工量、今日报工量、今日良品量、今日良率、执行中订单数前端拿到后直接渲染。GroupBy(r 1)是个小技巧如果不想引入额外的聚合字段就把整个表聚成一行。参数说明TodayYieldRate的百分比保留两位小数Math.Round的第二个参数是小数位数良率的计算口径是“良品数÷报工数”这个口径要和车间确认清楚有的工厂把“合格数÷投料数”也算良率两种算法结果差异很大。4. 生产概况界面解决“循环数据采集导致UI卡顿”这个老大难4.1 为什么后台采集数据不能直接更新界面用WPF写生产概况看板最容易踩的坑是开一个后台循环读设备数据然后在循环里直接改TextBox.Text或ProgressBar.Value结果界面假死。原因是WPF的UI控件只能由UI线程访问后台线程直接操作会抛异常或导致无法预料的刷新行为。真正的问题不是“访问UI控件”这个动作本身而是频繁的跨线程调度。即使你用了Dispatcher.Invoke如果采集周期是200毫秒一次每次都同步调度到UI线程UI线程被刷屏鼠标拖动窗口都会卡。所以正确的思路不是“怎么安全更新UI”而是“怎么降低更新UI的频率”。4.2 高频采集低频刷新的实现模式推荐一个实践过的组合后台线程只负责采集和计算把结果写入一个共享缓冲区UI定时器每隔1~2秒从缓冲区取最新值并刷新控件。缓冲区用Volatile.Read或简单的锁保护起来。public class EquipmentMonitor { private readonly object _lock new object(); private EquipmentRealtime _latestData; public void UpdateRealtimeData(EquipmentRealtime data) { lock (_lock) { _latestData data; } } public EquipmentRealtime GetLatestData() { lock (_lock) { return _latestData; } } }采集线程每300毫秒读一次PLC或扫码数据调用UpdateRealtimeDataUI线程的DispatcherTimer每1500毫秒调用GetLatestData并刷新界面。逻辑说明锁保护了共享对象DispatcherTimer是WPF的UI线程定时器它的回调直接运行在UI线程上不需要再Invoke。这样做的收益是采集频率保持300毫秒级别数据新鲜度够用UI刷新频率降到1.5秒一次CPU占用明显下降界面操作流畅度大幅提升。对于生产概况大屏这种场景1到2秒的延迟完全可接受。4.3 生产概况的图表展示C#后端推送加前端ECharts渲染WPF做看板没问题但越来越多的MES生产概况页面采用Web方案因为车间里的电视看板、办公室大屏、手机端都要看同一份数据。C#后端用SignalR或者WebSocket推送前端用ECharts渲染图表这个配合在MES项目里很常见。后端推送的关键代码public class DashboardHub : Hub { public async Task SendRealtimeOverview(object data) { await Clients.All.SendAsync(ReceiveOverview, data); } }生产概况主循环里每2秒从聚合接口拉一次数据然后通过DashboardHub推给所有已连接的浏览器端。前端订阅的JavaScript代码const connection new signalR.HubConnectionBuilder() .withUrl(/dashboardHub) .build(); connection.on(ReceiveOverview, function (data) { document.getElementById(totalQty).innerText data.TotalCompletedQty; yieldChart.setOption({ series: [{ data: [data.TodayYieldRate] }] }); });参数说明withUrl里的地址对应后端MapHubDashboardHub的路由ReceiveOverview是方法名前后端必须完全一致。setOption是ECharts的增量更新接口只需要传入变化的部分。需要留意的是ECharts图表如果每2秒全量setOption会有轻微闪烁推荐开启animation: false或只更新series.data。5. 订单管理和生产概况的联动细节从“做完”到“做对”5.1 订单进度条反映的是报工汇总而非设备计数生产概况页面上订单完成率是一个进度条。这个进度条的数据来源容易做错有些项目直接用设备累计产量除以计划数量问题是设备计数包含了调试件、试产件、报废件和真正的合格完工数量对不上。正确口径是订单完成率 该订单所有报工记录里良品数之和 ÷ 计划数量。用SQL表示就是SELECT wo.OrderNo, SUM(wr.GoodQty) AS TotalGoodQty, wo.PlannedQty, CAST(SUM(wr.GoodQty) AS DECIMAL(10,2)) / NULLIF(wo.PlannedQty, 0) * 100 AS ProgressPercent FROM WorkOrder wo LEFT JOIN WorkReport wr ON wr.WorkOrderId wo.Id WHERE wo.OrderNo OrderNo GROUP BY wo.OrderNo, wo.PlannedQty;这段SQL的重点是NULLIF(wo.PlannedQty, 0)防止计划数量为零时出现除零错误。CAST转成DECIMAL是为了保留小数点精度避免整数相除得到0。实际项目里这个查询我会加一个索引WorkReport(WorkOrderId, ReportTime)因为生产概况页面要按时间范围过滤报工记录联合索引能避免全表扫描。5.2 生产概况大屏“漂移”的3个排查步骤大屏上的数据和车间实际对不上是MES上线后最多的投诉。排查看板漂移问题按下面三步走第一步查报工链路。看扫码枪上报工是否成功ERP导入的订单有没有重复同类单据有没有冲销逻辑。第二步对比数据库实时数据。页面显示今日产量500用SQL直接查SELECT SUM(GoodQty) FROM WorkReport WHERE ReportTime 2024-01-01 AND ReportTime 2024-01-02;第三步检查设备采集链路。如果设备计数比报工数大很多大概率是设备计入了非生产件比如调试、换料、测试件。定位到具体原因后分别处理报工漏记的补报工流程设备多计的在采集脚本里加过滤条件。5.3 订单管理和生产概况的联动场景工单暂停时看板自动反映一个实用的联动逻辑是订单状态变为OnHold时生产概况的“执行中订单数”要减1同时对应的设备看板要亮黄灯提示“当前工单暂停”。这个联动不需要单独写逻辑状态机的变更事件会触发一次聚合查询的刷新private async Task OnOrderStatusChangedAsync(WorkOrder order) { var data await GetTodayOverviewAsync(); await _hub.Clients.All.SendAsync(ReceiveOverview, data); }订单状态一变聚合数据重新计算并推送到所有大屏。这个模式的好处是前端完全不需要关心业务逻辑只负责渲染。当车间规模变大、看板数量增多后这个模式也能支撑因为数据推送是广播式的。整个订单管理加生产概况的最小闭环到这里就通了扫码报工报工数据进库聚合接口算出概况SignalR推送大屏大屏反映订单进度。剩下的工作就是把单个车间扩展到多个车间把日维度扩展到小时粒度把报表从生产概况拆出OEE、人员绩效、质量追溯等专项模块。框架不变数据链路不变变的是上游的设备接入数量和下游的展示维度。这不是一条从零到一的“大项目”路径而是一条从最小闭环出发、持续叠加的MES落地方式。本文还有配套的精品资源点击获取
返回列表