新闻详情

新闻详情

首页 / 资讯中心 / 详情

C#开发MES系统:订单管理与生产概况模块实现方案

发布时间:2026/9/11 18:57:39来源:尧图网络
C#开发MES系统:订单管理与生产概况模块实现方案
简介面向制造企业的C#版MES系统源码基于WPF技术实现覆盖订单管理与生产概况两大核心模块。订单管理含作业计划和生产派工作业计划支持加工、检测、拧螺丝、轴承压装四类订单的下达与执行生产派工则实现人工操作台与立库间的上料/下料并对轴承托盘、螺钉托盘设置数量参数。生产概况实时呈现立库、AGV、多个区域机器人及人工上下料台的通信与操作状态有助于理解MES底层交互逻辑。资源共313个文件以cs源码、dll库、xml配置、xaml界面及baml编译视图为主整体约11.94MB适合学习WPF桌面应用与MES业务流程的初级开发者。已有4163人学习适合参考其模块划分、状态机设计与界面布局快速搭建自身MES原型。1. C#写MES最先落地的两个模块订单管理与生产概况车间推进数字化最常听到的问题就两个这批订单现在到哪了产线上正在做什么、做了多少。C#写的MES系统里订单管理和生产概况几乎总是最先被要求实现的模块——一个管计划一个管执行合起来就是生产主管早会要看的核心数据。很多C#开发者是从上位机转过来的对接PLC、扫码枪、Socket是顺手的事而MES恰好是上位机之上多出来的一层业务系统技术栈高度重合。如果你正在评估MES是什么、需要哪些模块或者被安排做一个车间级的订单跟踪与生产大屏下面这套方案按数据模型、服务层、生产概况大屏、性能验证的顺序展开每一段都能直接落地。2. 订单管理的数据模型MES订单表、工单表与工序计划怎么设计2.1 从订单到工单再到工序三层模型解决计划与执行脱节接手MES时最常见的返工是把所有业务塞进一张订单表。客户需求、生产批次、产线指派、每道工序的报工数量全挤在同一行字段越加越多状态却理不清。车间里一张订单可能跨多条产线、分多个批次、走不同工艺路线单表模型第一个月还能撑住等要统计“哪条产线完成率最低”时SQL已经写不动了。我一般会拆成三张核心表生产订单表负责客户口径的数量与交期工单表负责执行口径的产线与批次拆分工序计划表负责工艺路线和逐工序报工。三张表的粒度关系是 ProductionOrder 一对多 WorkOrderWorkOrder 一对多 WorkOrderOperation任何一层状态变化都不需要去改另外两层的结构。表粒度关键字段回答什么问题ProductionOrder 生产订单一次客户需求或一个生产批次OrderNo、ProductId、PlannedQty、DueDate、Status订了多少、什么时候交WorkOrder 工单按产线或批量拆出的执行单元OrderId、WorkOrderNo、WorkCenterId、Qty、Status哪个产线做、做多少WorkOrderOperation 工序计划按工艺路线展开的每一道工序WorkOrderId、Seq、ProcessCode、StandardTime、ReportQty做到哪一步、报工多少这个三层模型还有一个隐藏收益订单交期变更时只动 ProductionOrder产线调整时只动 WorkOrder报工只落在 WorkOrderOperation三件事互不阻塞。车间排产频繁调整时这个特性比什么都重要。生产概况里的“在制订单数”“各产线完成率”也都能直接按 WorkOrder 聚合不需要碰客户合同字段查询链路短出问题好定位。2.2 订单状态机的C#实现枚举加迁移矩阵订单状态我习惯用枚举而不是字符串。字符串在界面上友好但落到数据库和日志里就是灾难一个“C”和一个“已完成”根本没法做联表聚合。枚举加 int 存储用 0、10、20、30、40 这样的间隔值以后要在 Released 和 InProduction 之间插一个“待料”状态不需要改历史数据。public enum OrderStatus { Draft 0, // 草稿允许修改数量和交期 Released 10, // 已下达允许拆分工单 InProduction 20, // 生产中至少一张工单已开工 Completed 30, // 已完工所有工单报工完毕 Closed 40 // 已关闭归档不再变动 } private static readonly Dictionary(OrderStatus, OrderStatus), string AllowedTransitions new() { [(OrderStatus.Draft, OrderStatus.Released)] 下达, [(OrderStatus.Released, OrderStatus.InProduction)] 开工, [(OrderStatus.InProduction, OrderStatus.Completed)] 完工, [(OrderStatus.Completed, OrderStatus.Closed)] 关闭 }; public bool TryTransit(OrderStatus target, out string? action) { if (AllowedTransitions.TryGetValue((Status, target), out var act)) { Status target; action act; return true; } action $不允许从 {Status} 流转到 {target}; return false; }参数说明AllowedTransitions 的 key 是 (源状态, 目标状态) 二元组value 是操作名称写操作日志时直接拿来用。TryTransit 返回 bool调用方拿到 false 时把 action 里的中文描述弹给用户比抛异常更像业务提示。这里把迁移规则收拢到一张字典里新增状态只加一行映射比每个状态写一个方法的做法好维护得多。注意InProduction 不能靠人工从界面上点出来。它应该由工单状态聚合得出只要存在一张状态为 Running 的 WorkOrder订单就是生产中所有工单都 Completed 后订单才能 Completed。这个聚合逻辑放在服务层每次工单报工后调一次保证订单状态是从执行数据推导的而不是人填的。2.3 生产概况的预聚合字段哪些冗余做得值生产概况大屏轮询时不能每次都去 SUM 报工明细。报工表一旦超过几十万行同时有三四个大屏刷新数据库就会被拖住。常见的做法是在 ProductionOrder 和 WorkOrder 上各冗余一组数量字段报工事务里同步累加。冗余字段更新时机对应生产概况指标CompletedQty工序报工合格数累加完成率 CompletedQty / PlannedQtyScrapQty报废数量累加良率 CompletedQty / (CompletedQty ScrapQty)ReworkQty返工数量累加返工率单独 KPI 卡片这套冗余只在报工入口做也就是通过订单服务层的那个事务方法。只要累加和报工写在同一事务里明细与汇总就不会对不上。不要用数据库触发器去同步车间系统经常要改业务规则触发器每次改动都要重新验证所有写入口而服务层方法只有一个调用点出了问题定位快。数据量小的场景不需要冗余十万行报工明细以内实时 SUM 也能跑这个阈值按你们数据库的实际响应时间定。3. 用C#实现订单管理的服务层事务、防重与状态回写3.1 EF Core与Dapper并存数据访问层的取舍订单管理的写操作比读操作复杂得多创建订单要拆工单、要建工序计划报工要累加数量、要挪状态。这类强关系操作适合 EF Core 的变更追踪改完一个聚合根直接 SaveChanges外键关系不会写错。而生产概况大屏的查询是典型的宽表聚合EF Core 的表达式树在这种 SQL 面前很啰嗦我一般用 Dapper 执行手写 SQL查询路径短执行计划可控。一个项目里同时用两者并不冲突EF Core 管业务写路径Dapper 管查询读路径。依赖注入的注册方式如下builder.Services.AddDbContextMesDbContext(opt opt.UseSqlServer(builder.Configuration.GetConnectionString(MesDb))); builder.Services.AddScopedIOrderService, OrderService(); var connStr builder.Configuration.GetConnectionString(MesDb)!; builder.Services.AddScopedIDashboardQuery(_ new DashboardQuery(connStr));参数说明AddDbContext 默认生命周期是 Scoped和单个请求或单个业务操作保持一致保证同一个 DbContext 实例内的变更追踪不会跨线程。DashboardQuery 不依赖 DbContext直接把连接字符串构造进去查询方法内部用 Dapper 自己开连接、关连接避免长连接占用连接池。场景选用原因订单CRUD、事务、状态机EF Core变更追踪省去手写 UPDATE生产概况聚合、报表查询DapperSQL 直写执行计划可控批量导入导出EF Core 批量 API一条 SQL 插入多行往返少3.2 创建订单并拆分工单一个事务两条 SaveChanges订单创建不是一行 INSERT而是“订单 至少一张工单 每张工单的工序计划”一起落库。任一步失败都要全部回滚不然会出现有订单没工单的空壳数据。EF Core 的事务写法如下public async Tasklong CreateOrderAsync(CreateOrderRequest req) { await using var tx await _db.Database.BeginTransactionAsync(); try { var order new ProductionOrder { OrderNo BuildOrderNo(), // 规则MO yyyyMMdd 三位流水 ProductId req.ProductId, PlannedQty req.TotalQty, DueDate req.DueDate, Status OrderStatus.Draft }; _db.ProductionOrders.Add(order); await _db.SaveChangesAsync(); // 先拿到自增主键 order.Id const int lotSize 500; // 单张工单数量上限按产线产能调整 for (int i 0, seq 1; i req.TotalQty; i lotSize, seq) { var qty Math.Min(lotSize, req.TotalQty - i); _db.WorkOrders.Add(new WorkOrder { OrderId order.Id, WorkOrderNo ${order.OrderNo}-{seq:00}, WorkCenterId req.WorkCenterId, Qty qty, Status WorkOrderStatus.Pending }); // 按产品工艺路线展开 WorkOrderOperation这里省略明细 } await _db.SaveChangesAsync(); // 工单与工序计划一起写入 await tx.CommitAsync(); return order.Id; } catch { await tx.RollbackAsync(); throw; } }逻辑说明第一次 SaveChanges 不是多余的目的是拿到数据库自增的 order.Id工单外键必须引用它。第二次 SaveChanges 批量插入所有工单EF Core 会生成一条多值 INSERT比逐条插入快很多。lotSize 决定工单粒度数值越小排程越灵活但管理成本越高50 到 500 是常见区间具体按产线班次产能来定。事务在这里保护的是“订单和工单整体可见”。注意不要在这个事务里做远程调用比如回传 ERP、发短信远程响应慢会拖长数据库锁的持有时间。把这类通知放到提交成功之后用单独的后台任务去处理。3.3 扫码枪触发完工回写事件识别与防重车间报工最常用的不是鼠标点按钮而是扫码枪扫条码。常见的 USB 扫码枪模拟键盘输入几十毫秒内把一串字符打进来最后跟一个回车。触发事件的处理核心是把这串快速输入识别成一次扫码而不是把每个字符当成一次按键。private readonly StringBuilder _barcode new(); private DateTime _lastKeyTime; private void txtBarcode_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar \r) // 扫码枪以回车结尾 { ProcessBarcode(_barcode.ToString()); _barcode.Clear(); e.Handled true; return; } if (DateTime.Now - _lastKeyTime TimeSpan.FromMilliseconds(200)) _barcode.Clear(); // 间隔过长丢弃上一段 _lastKeyTime DateTime.Now; _barcode.Append(e.KeyChar); }参数说明200 毫秒是区分“人在敲键盘”和“扫码枪连发”的经验阈值扫码枪字符间隔一般小于 50 毫秒人工输入经常超过 200 毫秒。如果你的扫码枪是串口或网络接口的直接监听 SerialPort.DataReceived 或 Socket 接收事件比这个文本框方案更可靠。ProcessBarcode 拿到工单号后调用服务层报工。防重有两道一是数据库唯一索引在 WorkOrderOperation 上建 (WorkOrderId, OperationSeq, Barcode) 的唯一约束同一条码扫第二次直接拒绝二是并发下的乐观锁报工前读 RowVersion更新时比较版本号。用 EF Core 的 ExecuteUpdateAsync 实现public async Taskbool TryReportAsync(long workOrderId, int operationSeq, int qty) { var op await _db.WorkOrderOperations .FirstOrDefaultAsync(x x.WorkOrderId workOrderId x.Seq operationSeq); if (op is null) return false; var affected await _db.WorkOrderOperations .Where(x x.Id op.Id x.RowVersion op.RowVersion) // 版本号一致才更新 .ExecuteUpdateAsync(s s .SetProperty(x x.ReportQty, op.ReportQty qty) .SetProperty(x x.RowVersion, x x.RowVersion 1)); return affected 1; // 为 0 说明版本被改过提示用户重试 }逻辑说明Where 条件里比较 RowVersion 是乐观锁的常见写法ExecuteUpdateAsync 直接生成 UPDATE 语句不需要先把实体加载进跟踪上下文。affected 为 0 时不要重试覆盖返回给调用方由界面提示“该工单已被其他用户报工请刷新后重试”。同一条码的幂等由唯一索引兜底即使两个请求同时到达第二个会撞唯一约束而失败不会把数量加成双份。这段代码要求较新版本的 EF Core项目还在用 EF Core 5/6 的话改成先查实体再改字段后 SaveChanges逻辑等价。4. 生产概况大屏数据轮询、聚合查询与UI刷新卡顿的解法4.1 生产概况的指标口径完成率、良率与在制量怎么算生产概况大屏作为 MES 的驾驶舱页面通常放五类信息各产线今日完成率、整体良率、在制工单数、最近一小时报工趋势、卡在哪个工序的瓶颈列表。口径必须先定清楚否则销售看的是订单百分比车间主任看的是工单百分比两边对不上。建议先定规则订单级指标用 ProductionOrder 的冗余字段算产线级指标用 WorkOrder 聚合算。一个跨多个产线的大订单它的“完成率”是整体口径而“产线完成率”必须拆到工单。两者各有用途放进同一个大屏的不同区域。查询脚本用 Dapper 执行SELECT wo.WorkCenterId, COUNT(DISTINCT wo.OrderId) AS ActiveOrderCount, -- 在制订单数跨产线订单只算一次 SUM(wo.Qty) AS PlanQty, -- 计划总数量 SUM(wo.CompletedQty) AS DoneQty, -- 合格完工数 SUM(wo.ScrapQty) AS ScrapQty -- 报废数 FROM WorkOrder wo WHERE wo.Status IN (1, 2) -- 1:未开工 2:生产中 GROUP BY wo.WorkCenterId;逻辑说明COUNT(DISTINCT wo.OrderId) 避免一张拆成多张工单的订单被重复计数DoneQty 和 ScrapQty 来自上一章的事务冗余更新不走报工明细表这条 SQL 在百万级工单表上也能稳定返回。良率和完成率在 C# 侧算不放在 SQL 里因为涉及除零和精度描述C# 用 decimal 处理完再格式化显示逻辑更好控制。指标计算来源展示位置整体完成率ProductionOrder.CompletedQty / PlannedQty顶部 KPI产线完成率WorkOrder 聚合产线卡片良率DoneQty / (DoneQty ScrapQty)顶部 KPI在制订单数COUNT(DISTINCT OrderId)产线卡片小时报工趋势报工明细按小时聚合趋势图如果大屏要显示趋势曲线按小时聚合报工明细那个查询可以容忍只扫当天数据一旦要回看超过两天的历史曲线就改成预聚合小时表一天一行查询时直接取。4.2 循环数据采集与UI刷新线程模型的正确姿势大屏不是一次性渲染而是每隔几秒拉一次最新数据。最容易踩的坑是在 UI 线程里做轮询每轮都执行 SQL、转换数据、重绑控件UI 线程忙不过来窗口就看起来像卡死。C# 里循环数据采集的正确做法是让数据层跑在线程池UI 只做最终渲染。private CancellationTokenSource _cts new(); private void StartPolling() { _cts new CancellationTokenSource(); _ Task.Run(async () { using var timer new PeriodicTimer(TimeSpan.FromSeconds(5)); while (await timer.WaitForNextTickAsync(_cts.Token)) { var data await _dashQuery.GetWorkCenterKpiAsync(); // 线程池执行 _ Dispatcher.BeginInvoke(() RenderDashboard(data)); // 切回UI线程 } }, _cts.Token); } protected override void OnClosed(EventArgs e) { _cts.Cancel(); // 窗口关闭时停止轮询避免后台线程泄漏 base.OnClosed(e); }参数说明PeriodicTimer 比 Thread.Sleep 可靠——Sleep 每次醒来还要叠加查询耗时轮询间隔会越拖越长PeriodicTimer 按固定周期触发不跑偏。CancellationTokenSource 在窗口关闭时 Cancel后台任务在下一个 WaitForNextTickAsync 抛出 OperationCanceledException 而退出。Dispatcher.BeginInvoke 是异步投递不会因为 UI 忙而阻塞数据采集线程。如果采集任务不是查询数据库而是监听设备比如通过 Modbus 或 Socket 从 PLC 拿计数采集线程里要自己维护重连逻辑不能依赖窗口生命周期。上位机开发里最常见的“采着采着没数据了”九成是设备端断连后没有重试机制。4.3 UI刷新卡顿的三个解法BeginUpdate、增量绑定与节流即使主流程写对了大屏在数据量大时还是会卡。卡顿的根因通常是控件重绘每加一行就重绘一次几百行数据就是几百次重绘。第一招是 BeginUpdate挂起重绘数据填充完再 EndUpdate 一次性刷新private void RefreshCore(KpiData data) { gridView.BeginUpdate(); // 挂起重绘 gridDataSource.Clear(); foreach (var row in data.Rows) gridDataSource.Add(row); gridView.EndUpdate(); // 一次重绘完成整批更新 lblFinishRate.Text data.FinishRate.ToString(P1); // 87.3% 样式 }第二招是增量绑定。生产概况大屏上多数行的值变化其实只有数量列整表重建等于浪费。保留数据源对象的引用只更新变化的那几个单元格配合 BeginUpdate 只会触发一次重绘。第三招是节流数据层每 2 秒返回一次UI 不一定要跟着刷两次。用时间戳判断1 秒内只取最后一次数据private DateTime _lastUiRefresh DateTime.MinValue; private KpiData? _pending; private void RenderDashboard(KpiData data) { if ((DateTime.Now - _lastUiRefresh).TotalMilliseconds 1000) { _pending data; // 距离上次刷新太近先存下最新值 return; } RefreshCore(data); _lastUiRefresh DateTime.Now; }参数说明1000 毫秒的节流窗口适合 5 秒轮询的场景视觉上平顺且 CPU 占用低如果轮询间隔已经大于 3 秒节流可以去掉。_pending 字段保证最终一致性——界面显示的永远是最新到达的数据而不是真正渲染那一瞬间的旧数据。4.4 上位机数据对接Modbus与Socket的接法生产概况里的数据不全是报工产生的有些设备计数直接从 PLC 读。C# 在这条链路上天然顺手上位机进程用 NModbus4 轮询 PLC 保持寄存器把寄存器数值换算成产量也可以用 Socket 接收设备主动上报的 TCP 报文解析后写入报工表。两者的共同点是采集线程和业务线程分离采集线程只负责收数业务线程负责把数据交给 TryReportAsync 事务落库。设备侧的数据往往没有条码防重约束就退化为设备号时间窗口。常见做法是以工单号加 5 分钟窗口做唯一约束窗口内重复报文直接丢弃。还要注意 Modbus 轮询的寄存器地址和字节序上位机里调试好的代码换一台 PLC 型号就可能读到错位数值这部分要单独写成配置别写死在代码里。5. 生产概况刷新耗时的拆解与验证Stopwatch脚本怎么用5.1 把一次刷新拆成三段计时大屏做出来之后最该做的不是加功能而是给刷新链路装上计时器。生产概况的刷新耗时要拆成三段数据库查询耗时、数据传输和映射耗时、UI 绑定重绘耗时。分别计时才能判断瓶颈到底在哪一段。var sw Stopwatch.StartNew(); var rows await _dashQuery.GetWorkCenterKpiAsync(); sw.Stop(); var dbMs sw.ElapsedMilliseconds; sw.Restart(); views rows.ToList(); // 从 DataRow 映射到 ViewModel sw.Stop(); var mapMs sw.ElapsedMilliseconds; sw.Restart(); gridView.BeginUpdate(); gridDataSource.Clear(); foreach (var v in views) gridDataSource.Add(v); gridView.EndUpdate(); sw.Stop(); var uiMs sw.ElapsedMilliseconds; Trace.WriteLine($[Dashboard] db{dbMs}ms, map{mapMs}ms, ui{uiMs}ms);5.2 用日志反推瓶颈与数据增长把这三段计时写进一张 DashboardLog 表每次刷新一行连续跑一天。dbMs 中位数超过 500 毫秒优先查 WorkOrder 表的索引看 WHERE 条件里的 Status 是否走索引以及冗余字段是否真的在事务里更新了。uiMs 长期是 dbMs 的两倍以上回到 4.3 看是否漏了 BeginUpdate或者大屏背景图加了不必要的半透明重绘。一个值得试试的验证技巧把 SQL 原样拷进 SSMS开实际执行计划跑一遍。如果 SSMS 里只要 30 毫秒而应用里要 400 毫秒问题在参数嗅探或连接上下文不在 SQL 本身两边都慢就是缺索引先给 WorkOrder.Status 加包含列索引。最后把 Trace 输出挂到 DebugView或者直接写在这个日志表里等车间真出现“大屏转圈”的投诉时你能掏出数据说明卡在哪一段而不是靠猜。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

音频处理实战|翻唱伴奏升降调要移几个调?音域匹配与移调损耗的四个判断 2026/9/11 19:39:43

音频处理实战|翻唱伴奏升降调要移几个调?音域匹配与移调损耗的四个判断

你找到一版音质不错的伴奏,跟着唱两句就发现副歌顶不上去,高音全靠喊。于是把伴奏降了 4 个半音,唱是舒服了,可整首歌听着不对劲——鼓变闷了,吉他的拨弦声发虚,像隔着一层塑料膜。再往回升 2 个半音&#…

阅读更多 →
西门子PLC智能喷泉控制系统设计与实现 2026/9/11 19:39:43

西门子PLC智能喷泉控制系统设计与实现

1. 项目概述:PLC控制的智能喷泉系统这套基于西门子S7-200 PLC和组态王软件的喷泉控制系统,是我去年为某城市广场改造项目设计的经典案例。系统通过PLC的开关量控制和模拟量调节,实现了喷泉水型组合、灯光联动、音乐同步等12种表演模式。相比传…

阅读更多 →
Prefix Tuning学习 2026/9/11 19:39:43

Prefix Tuning学习

学习概览 学习进程:高效微调 → 高效推理 → LLaMA 项目实战1. Efficient Fine-tuning:怎么更便宜地训练 Prefix Tuning:增加少量可训练 Prefix。Adapter Tuning:在 Transformer 中插入小型 Adapter。LoRA / AdaLoRA / QLoRA&…

阅读更多 →
OpenClaw 移除 BlueBubbles 频道:迁移到 imsg 驱动的官方 iMessage 插件 2026/9/11 19:39:43

OpenClaw 移除 BlueBubbles 频道:迁移到 imsg 驱动的官方 iMessage 插件

OpenClaw 移除 BlueBubbles 频道:迁移到 imsg 驱动的官方 iMessage 插件 【免费下载链接】openclaw The AI that really does things. Any OS. Any Platform. The lobster way. 🦞 项目地址: https://gitcode.com/GitHub_Trending/cl/openclaw O…

阅读更多 →
NDI安装与使用 2026/9/11 19:39:43

NDI安装与使用

小白也能看懂:NDI从安装到使用图文指南 什么是NDI? NDI全称Network Device Interface(网络设备接口),是一种通过网络传输视频信号的协议。简单来说,它可以把视频信号像“网络数据包”一样在局域网里飞来飞去…

阅读更多 →
矩阵函数计算:原理、算法与应用实践 2026/9/11 19:36:43

矩阵函数计算:原理、算法与应用实践

1. 矩阵函数值计算的基本概念矩阵函数值计算是线性代数中一个既基础又重要的课题。简单来说,它研究的是如何将我们熟悉的标量函数(如指数函数、三角函数等)推广到矩阵上的运算。这个概念在控制系统、量子力学、图像处理等领域都有广泛应用。我…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞