☰
C# MES双端源码实战:从架构拆解到二次开发改造
2026/10/26 2:07:39 网站建设 项目流程

简介:这是一套面向中高级 C# 开发者与制造业信息化实施人员的 MES 系统完整源代码,基于 Mes.Client 与 Mes.Server 前后端分离结构,覆盖客户端界面、实体模型、服务引用、单元测试以及服务宿主、数据操作与对外接口等模块,适合用于学习 MES 业务建模、服务分层设计与界面重构思路。压缩包共约 2000 个文件,以 1025 个 css、520 个 cs、415 个 js 为主,辅以 dll、resx、csproj、sln 等工程与配置类文件,整体约 35.87MB,目录按 Client 与 Server 两端清晰划分,便于按模块检索。资源围绕 MES2.0 改版展开,涉及界面 UI 调整、源码整理与功能升级,读者可从中获取订单、产品、排程、用户与合作伙伴等业务对象的数据库操作实现,以及服务宿主与接口定义方式,用于对照自身项目进行架构参考与代码复用。目前已有 1561 人学习下载。

1. 一套 C# MES 双端源码,为什么值得先跑起来再谈改造

车间里最常见的场景是这样的:产线工位机要报工、要扫条码、要看工艺参数,办公室的计划员要排产、要查在制品、要导报表,两边用的根本不是同一套东西,数据靠 Excel 来回倒。这套 C# MES 完整源代码程序把这件事拆成了两个工程——Mes.Client 和 Mes.Server,客户端负责工位交互,服务端负责数据与业务规则,中间走网络通信。对正在找 C# 软件/插件类实战项目的开发者来说,它的价值不在于功能多全,而在于「双端 + 通信 + 数据库」这条链路是完整的、能编译、能改。新手可以照着把环境搭起来看数据怎么从工位流到数据库,熟手可以直接看分层和通信协议怎么设计。下面按「能干什么 → 怎么跑 → 怎么改」的顺序拆。

2. 双端架构拆解:Mes.Client 与 Mes.Server 各自管什么

2.1 两端职责边界与典型分层

这套系统的骨架非常清晰:Mes.Server 是唯一的数据权威,Mes.Client 是瘦客户端加本地缓存。服务端一般会分成数据访问层、业务逻辑层、通信服务层三块,客户端则是界面层、通信代理层、本地配置层。这样切的好处是工位机不需要直连数据库,所有写操作都经过服务端校验,避免多工位并发写同一张工单时出现脏数据。

常见的目录组织大致是这样(不同版本命名会有出入,但结构逻辑一致):

Mes.Server/ Mes.Server.sln DAL/ 数据访问,封装连接与 CRUD BLL/ 业务规则,工单、报工、物料校验 Service/ 通信宿主,监听客户端连接 Model/ 实体类,与数据库表对应 Config/ 数据库连接串、端口配置 Mes.Client/ Mes.Client.sln Forms/ 工位界面,报工、扫码、查询 Proxy/ 通信代理,封装请求与响应 Common/ 工具类、本地缓存、日志 Config/ 服务端地址、工位编号

服务端启动后监听固定端口,客户端在配置文件里填服务端 IP 和端口,连上之后所有业务请求都以「命令 + 参数」的形式发过去,服务端解析、走 BLL、落库、回包。这个模型不复杂,但足够支撑一个车间几十个工位的量级,也是很多中小型 MES 的真实做法。

2.2 通信链路:一次报工请求怎么走完

报工是 MES 最高频的操作,理解它就理解了整条链路。工位机扫完工单条码,点「报工」,客户端组装一个请求对象,序列化后发给服务端;服务端收到后先做参数校验(工单是否存在、数量是否超报、工位是否有权限),再写报工记录、更新工单进度,最后返回结果码和提示信息。

客户端侧的核心代码通常长这样:

// Mes.Client/Proxy/MesProxy.cs public class MesProxy { private readonly string _serverIp; private readonly int _port; public MesProxy(string serverIp, int port) { _serverIp = serverIp; _port = port; } // 发送报工请求,返回服务端结果 public ReportResult SubmitReport(ReportRequest req) { using (var client = new TcpClient()) { client.Connect(_serverIp, _port); // 建立短连接 var stream = client.GetStream(); var payload = Serialize(req); // 序列化为字节流 stream.Write(payload, 0, payload.Length); var buffer = new byte[4096]; int len = stream.Read(buffer, 0, buffer.Length); return Deserialize<ReportResult>(buffer, len); } } }

这里几个参数值得说清楚:_serverIp和_port来自客户端配置文件,改部署环境只改配置不动代码;ReportRequest里一般带工单号、工位号、报工数量、操作员、时间戳;短连接写法简单,但高频报工时会有连接开销,量大的场景常见做法是改成连接池或长连接加心跳。服务端对应要有一个分发入口,按请求类型路由到不同处理方法:

// Mes.Server/Service/RequestDispatcher.cs public class RequestDispatcher { public Response Handle(RequestBase request) { switch (request.Command) { case "SubmitReport": return _reportService.Submit((ReportRequest)request); case "QueryOrder": return _orderService.Query((QueryRequest)request); default: return Response.Fail("未知命令"); } } }

Command字段是路由键,新增业务只要加一个 case 和一个 Service 方法,客户端加一个代理方法,扩展成本很低。这种「命令字符串 + 强类型参数」的写法比纯反射调用好调试,出问题看日志里的 Command 就知道走到哪了。

2.3 数据库与实体映射

服务端的数据访问层一般用 ADO.NET 或轻量 ORM,实体类和表一一对应。工单表、报工记录表、物料表、工位表是核心四张表。报工写入时要注意事务:写报工记录和更新工单已报数量必须在同一个事务里,否则中途失败会出现「报了工但进度没加」的玄学问题。

// Mes.Server/BLL/ReportService.cs public Response Submit(ReportRequest req) { using (var conn = new SqlConnection(_connStr)) { conn.Open(); using (var tran = conn.BeginTransaction()) { try { // 1. 校验工单状态与剩余可报数量 var order = _orderDal.GetForUpdate(conn, tran, req.OrderNo); if (order == null) return Response.Fail("工单不存在"); if (order.RemainQty < req.Qty) return Response.Fail("超出可报数量"); // 2. 写报工记录 _reportDal.Insert(conn, tran, req); // 3. 更新工单进度 _orderDal.AddReportedQty(conn, tran, req.OrderNo, req.Qty); tran.Commit(); return Response.Ok("报工成功"); } catch (Exception ex) { tran.Rollback(); _logger.Error("报工失败", ex); return Response.Fail("系统异常,请重试"); } } } }

GetForUpdate里带行锁,防止两个工位同时报同一张工单导致超报。这是血泪经验:早期没加锁,月底对账总差几十件,查了半天才发现是并发写。事务范围只包住必要的三条语句,不要把手写日志、发消息这类操作塞进去,否则锁持有时间变长,工位机就会感觉「点了没反应」。

3. 把项目跑起来:环境、配置与首次联调

3.1 开发环境与依赖准备

这套代码是 C# 工程,服务端和客户端都是 .NET 项目。环境上需要 Visual Studio(2019 或更新版本均可)或 Rider,安装 .NET Framework 或 .NET SDK 取决于工程的目标框架,打开解决方案文件看项目属性里的目标框架即可确认。数据库一般是 SQL Server,装一个本地实例或者用现成的测试库都行。

准备顺序建议这样:先装开发工具和数据库,再还原 NuGet 包,最后分别编译两个解决方案。常见依赖包括数据库驱动、日志组件、序列化库,还原时如果提示包源不可达,检查 NuGet 源配置,换成可访问的镜像源即可。这一步翻车的概率不低,尤其是内网环境,提前把包源配好能省很多时间。

3.2 服务端配置与启动

服务端启动前必须改配置文件里的数据库连接串和监听端口。连接串一般放在App.config或appsettings.json里,形如:

<!-- Mes.Server/App.config --> <configuration> <appSettings> <add key="DbConn" value="Server=127.0.0.1;Database=MesDb;User Id=sa;Password=你的密码;"/> <add key="ListenPort" value="8899"/> </appSettings> </configuration>

DbConn指向你的数据库实例,ListenPort是客户端要连的端口,改完记得防火墙放行。数据库要先建库建表,源码里通常带 SQL 脚本,找到后按顺序执行。启动服务端时先看控制台或日志有没有「监听已启动」之类的输出,没有就说明端口被占用或配置没读到。

提示:首次启动前把数据库脚本完整跑一遍,缺表导致的报错往往藏在业务层,排查起来比启动失败更费劲。

3.3 客户端连接与首次报工验证

客户端配置里填服务端地址和本机工位号:

<!-- Mes.Client/App.config --> <configuration> <appSettings> <add key="ServerIp" value="127.0.0.1"/> <add key="ServerPort" value="8899"/> <add key="StationNo" value="ST-01"/> </appSettings> </configuration>

两端都起来后,先在客户端做一次查询操作验证连通性,再走报工。验证顺序建议是:连接测试 → 工单查询 → 单件报工 → 数据库核对。数据库里能看到报工记录、工单已报数量同步增加,这条链路就算通了。如果查询通但报工失败,重点看服务端日志里的业务校验提示,多半是工单状态或数量限制的问题,不是通信问题。

4. 核心模块代码精读:报工、查询与异常处理

4.1 工单查询的缓存与刷新策略

工位机查询工单的频率很高,每次都打服务端会给数据库压力。常见做法是客户端本地缓存一份工单列表,定时刷新加手动刷新结合。缓存要有失效机制,否则计划员改了工单,工位机还显示旧数据,现场就会吵起来。

// Mes.Client/Common/OrderCache.cs public class OrderCache { private List<OrderInfo> _orders = new List<OrderInfo>(); private DateTime _lastRefresh = DateTime.MinValue; private readonly TimeSpan _ttl = TimeSpan.FromSeconds(30); // 缓存30秒 public List<OrderInfo> GetOrders(MesProxy proxy, bool forceRefresh = false) { if (forceRefresh || DateTime.Now - _lastRefresh > _ttl) { _orders = proxy.QueryOrders(); // 走服务端拉取 _lastRefresh = DateTime.Now; } return _orders; } }

_ttl设 30 秒是个折中:太短等于没缓存,太长数据滞后明显。报工成功后要主动调forceRefresh或把对应工单从缓存里剔除,保证工位看到的剩余数量是准的。这个细节不做,操作员会重复报工然后被服务端拒绝,体验很差。

4.2 服务端异常兜底与日志落盘

服务端最怕的是未捕获异常导致整个通信线程挂掉。分发入口必须包一层 try-catch,把异常记日志并返回统一错误码,不能让异常冒到监听循环外面。

// Mes.Server/Service/Listener.cs private void HandleClient(TcpClient client) { try { var request = ReadRequest(client); var response = _dispatcher.Handle(request); WriteResponse(client, response); } catch (Exception ex) { _logger.Error($"处理请求异常: {ex.Message}", ex); WriteResponse(client, Response.Fail("服务端处理异常")); } finally { client.Close(); } }

日志要落到文件并按天切分,字段至少包含时间、请求命令、工位号、异常堆栈。现场出问题时,这份日志就是黑匣子,没有它只能靠猜。日志级别别全开 Debug,生产环境 Info 起步,否则文件涨得比数据还快。

4.3 客户端超时与重试的边界

网络抖动在车间很常见,客户端请求要有超时,不能无限等。但重试要谨慎:查询类请求可以重试,报工这类写操作重试可能导致重复写入,必须靠服务端的幂等校验兜底,比如用「工单号 + 工位号 + 时间戳」做唯一约束。

// Mes.Client/Proxy/MesProxy.cs public ReportResult SubmitReportWithRetry(ReportRequest req, int maxRetry = 2) { for (int i = 0; i <= maxRetry; i++) { try { return SubmitReport(req); // 内部设置 Socket 超时 } catch (IOException) when (i < maxRetry) { Thread.Sleep(500); // 退避后重试 } } return ReportResult.NetworkError("网络异常,请确认后重试"); }

重试次数别设太多,两次足够,再多只会让操作员干等。真正稳妥的做法是服务端幂等加客户端提示「请先查询确认是否已报工」,把判断权交回给人。

5. 避坑与排查:这套双端源码最容易翻车的五个地方

5.1 现象:客户端连不上服务端,提示连接超时

原因通常是三选一:服务端没启动、端口被防火墙拦、客户端配置的 IP 不是服务端实际地址。排查顺序是先在本机用telnet 服务端IP 端口测通不通,不通就看服务端监听日志和防火墙规则,通了但客户端还报错就检查配置文件是否被读到了正确路径(有些工程会读输出目录下的配置副本,改源码目录的配置不生效)。

5.2 现象:报工成功但工单进度没变

原因多半是事务没包住两条写操作,或者更新语句的条件写错(比如按工单号更新但传了空值)。解决方式是先在数据库里手动执行更新语句验证条件,再检查 BLL 里事务范围和参数传递。这类问题上线前一定要用并发脚本压一遍,单线程测不出来。

5.3 现象:中文乱码

原因在序列化编码不一致,一端用 UTF-8 另一端用默认编码。解决方式是两端统一显式指定 UTF-8,读写字节流时不要依赖系统默认编码。这个坑在跨机器部署时特别容易踩,本机联调往往正常。

5.4 现象:服务端跑一段时间后内存持续上涨

原因通常是连接或流对象没释放,TcpClient、NetworkStream、数据库连接都要用 using 包住。排查可以用性能计数器看句柄数,涨得快就是泄漏。解决方式是逐个检查通信和数据库代码块的释放逻辑,别指望 GC 兜底。

5.5 现象:多工位同时报工出现超报

原因就是前面说的缺行锁或校验与写入不在同一事务。解决方式是查询时加UPDLOCK,把校验和更新放进同一事务,并在数据库层加唯一约束做最后一道防线。改完用两个客户端同时点报工验证,能稳定复现再修复才算数。

6. 二次开发改造:从能跑到好用

拿到这套源码,改造优先级我一般按「数据准确性 → 通信稳定性 → 界面效率」排。第一步先把报工幂等做扎实,给报工记录表加唯一索引,服务端写入前先查重,这一步做完,后面所有功能都建立在可信数据上。第二步把短连接改成连接池或长连接加心跳,工位数量上去之后短连接的开销会很明显,改的时候注意服务端要能处理半包和粘包,读数据按长度头解析而不是一次 Read 就当完整包。

第三步是扩展性改造。新增业务类型时,服务端的RequestDispatcher加 case、BLL 加方法、客户端代理加方法,三步走完就能用。如果业务类型会持续增加,可以把 switch 换成字典注册:

// 用字典替代 switch,新增命令只注册不改分发逻辑 private readonly Dictionary<string, Func<RequestBase, Response>> _handlers; public RequestDispatcher() { _handlers = new Dictionary<string, Func<RequestBase, Response>> { ["SubmitReport"] = r => _reportService.Submit((ReportRequest)r), ["QueryOrder"] = r => _orderService.Query((QueryRequest)r), }; } public Response Handle(RequestBase request) { return _handlers.TryGetValue(request.Command, out var handler) ? handler(request) : Response.Fail("未知命令"); }

验证改造是否成功,别只看界面提示,直接查数据库核对记录数和工单数量,再用两个客户端并发跑一轮。界面上的「成功」两个字最会骗人,数据对得上才算数。

改造点改动位置验证方式
报工幂等报工表唯一索引 + BLL 查重重复提交同一请求,只落一条
长连接客户端 Proxy + 服务端 Listener断网重连后业务不中断
命令注册化RequestDispatcher新增命令不改分发代码即可生效
日志分级服务端日志组件生产环境日志量可控、可检索

最后说个习惯:这套代码我每次改完通信层,都会强制走一遍「启动服务端 → 启动两个客户端 → 并发报工 → 查库核对」的完整流程,不管改动多小。有一次只改了一个字段名,觉得没必要全跑,结果序列化对不上,现场工位全报错,返工成本比跑一遍高十倍。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询