C#+Vue MES源码实战:前后端分离与工单追溯避坑指南
简介基于C#与Vue构建的iMES工厂管家是一套面向中小型制造企业的生产制造执行系统MES涵盖生产排产、工单管理、过程追溯、质量管控等核心模块适合.NET开发人员学习或作为企业数字化改造的基础原型。压缩包共1456个文件约7.06MB其中982个C#源文件对应后端服务与业务逻辑156个Vue文件构成前端交互界面另有SQL数据库脚本、部署批处理.bat、Docker配置及项目工程文件便于快速还原前后端环境并理解完整代码结构。已有587人学习下载。资源内附带可执行的构建/运行脚本与数据表结构说明开发时即可参照实际MES模块组织方式也能直接编译部署到本地进行功能验证。系统采用前后端分离架构代码结构清晰适合具备一定C#和Vue基础、希望掌握工业管理系统全栈落地思路的开发者深入研读和二次开发。1. 一套自带前后端和数据库的MES源码到底能帮你省掉多少事做车间数字化的时候我经常被问到一个问题网上常见的基于C#和Vue开发的MES系统源码包比如这套iMES工厂管家前后源代码加数据库一起给到底值不值得拿来改我的回答是如果你手里正好有离散制造或者小批量装配的车间项目这类项目能帮你把工单、报工、追溯和看板的地基打好省掉从零搭架构的两到三周时间。它适合三类人接MES项目的软件工程师想给自家车间做数字化的工厂IT以及想从单体ERP转行做MES的C#程序员。但前提是你得知道哪些代码是通用能力哪些是演示版本埋的雷。2. 把架构先立住C#后端和Vue前端的MES系统为什么这样拆拿到一套MES源码第一件事不是点开解决方案跑起来而是先看它的前后端边界。C#后端管什么Vue前端管什么数据库脚本藏在哪个目录这些决定了你后续能不能改得动。MES系统的核心不是界面好不好看而是工单状态、报工数量、不良品去向这些数据能不能闭环。用C#做后端是因为工厂现场大量设备SDK、上位机程序、条码采集软件本来就是C#写的后端可以直接引用那些动态库用Vue做前端是因为工单列表、看板、质检录入对交互要求高组件化开发比传统脚本顺手很多。这套iMES工厂管家虽然代码在压缩包里但架构套路是统一的。2.1 MES的模块边界工单、报工、追溯、看板谁在前谁在后MES功能模块看起来很多但核心链条只有一条基础数据 - 计划工单 - 派工 - 报工 - 质检 - 入库。工单是主轴报工是数据来源追溯是结果。前端展示的是操作界面和数据看板后端维护的是状态机和业务规则。我一般会把后端按Controller、Service、Repository三层拆Controller只做参数校验和HTTP响应Service写业务规则Repository管EF Core访问。比如工单实体至少要有这些字段public class WorkOrder { public int Id { get; set; } public string WorkOrderNo { get; set; } // 工单号例如 WO20250601001 public string ProductCode { get; set; } // 产品料号 public string ProductName { get; set; } // 产品名称 public decimal PlanQty { get; set; } // 计划数量decimal 避免 float 误差 public int Status { get; set; } // 0 待下达1 生产中2 完工3 取消 public DateTime CreatedAt { get; set; } // 创建时间 public int WorkshopId { get; set; } // 车间ID }这个实体看起来简单但设计时要注意数量字段必须用decimal不能用float或double状态字段建议用int或枚举不要用字符串时间字段我建议统一用DateTime且明确时区后面避坑章节会专门说。C#的强类型在这里的好处是字段写错了编译期就能发现而不是上线后看报表数据发疯。Vue前端负责的是表单、路由、状态展示。比如用Vue Router管理页面跳转工单列表、报工页面、看板各占一个路由表格用组件封装操作列用插槽扩展按钮。很多新手容易把业务规则写进前端比如在Vue里判断工单能不能报工这是翻车的开始——前端只能做交互控制和简单校验真正判断必须回到C#的Service层。2.2 前后端数据流从Vue页面到C#服务再到数据库的一条完整链路以最常见的工单分页查询为例完整链路是Vue组件调用封装的request方法GET请求发到/api/workorder/pageC# Controller接收参数EF Core转SQL查询数据库结果转JSON回前端前端渲染表格。这里面每一步都有讲究。C#接口一般长这样[HttpGet(api/workorder/page)] public async TaskIActionResult Page(int pageIndex, int pageSize, int status) { var query _db.WorkOrders.Where(w status -1 || w.Status status); int total await query.CountAsync(); var rows await query .OrderByDescending(w w.Id) .Skip((pageIndex - 1) * pageSize) .Take(pageSize) .ToListAsync(); return Ok(new { total, rows }); }这个接口的逻辑是status等于-1时不过滤状态否则按状态过滤先Count得到总数再分页取数据排序用Id倒序因为MES工单一般看最新。参数pageIndex从1开始pageSize我推荐20到50太大会把数据库和执行时间拖垮。前端分页组件传这两个参数就行。Vue侧的调用封装很关键直接决定联调效率。一般会写一个request模块// api/request.js import axios from axios const http axios.create({ baseURL: /api, timeout: 30000 })这里baseURL写成/api开发环境靠Vite代理转到后端端口生产环境靠Nginx反代前端代码不需要改域名。timeout设置30秒MES报工接口经常因为现场网络慢5秒超时太容易误判。后面的步骤会在这个基础上加token拦截器。2.3 数据库选型和ORM这套源码默认给你留了什么C#的MES系统最常见的数据库搭配是SQL Server理由很简单EF Core的老家就是SQL Server做事务和行锁最省心。SQL脚本在压缩包的database目录下一般是一个iMES.sql包含建库、建表、初始数据。如果工厂IT那边只有MySQL也可以改但要注意分页语法和自增列定义不同。配套的appsettings.json连接串默认长这样{ ConnectionStrings: { MESDb: Serverlocalhost;DatabaseiMES;User Idsa;Passwordyour_password;TrustServerCertificateTrue } }参数说明Server写数据库地址本地调试就localhostDatabase是库名User Id和Password是登录账号。如果你的SQL Server是Windows集成认证可以改成Trusted_ConnectionTrue。TrustServerCertificateTrue是给本机开发用的避免证书校验报错生产环境建议改成False并用正式证书。ORM部分EF Core主张Code First但MES项目我更推荐Database First或直接用SQL脚本管理原因在后面避坑章会讲。你要先理解的是数据库脚本是这个系统的源头前后端代码都在为它服务。3. 跑通C#后端服务项目结构、数据库初始化与最小接口验证把压缩包解压后最怕的是打开Visual Studio找不到启动项目。其实C#和Vue的MES解决方案结构很固定目录树一看就明白。后端启动之后还要把数据库脚本导入到SQL Server里最后用Swagger验证接口这样才算真正跑通。整个过程顺序不能乱否则你会被一堆连不上数据库的报错淹没。3.1 先认清压缩包里的三个工程API、Web、共享库正常情况下压缩包根目录会是这样一个结构iMES/ ├── src/ │ ├── iMES.Api/ # 后端Web API启动项目 │ ├── iMES.Web/ # Vue前端工程NPM项目 │ └── iMES.Core/ # 实体、DTO、数据访问公共类库 ├── database/ │ └── iMES.sql # 数据库初始化脚本 └── docs/ # 部署说明这个结构里iMES.Api是启动项目它引用iMES.CoreiMES.Web是Vue前端工程和后端Api通过HTTP协议通信不是直接项目引用。iMES.Core里放的是数据库实体、数据访问上下文和DTO后端所有业务逻辑都依赖这个类库。先看这段目录的意义在于你要确认数据库脚本是不是独立的。如果database目录里只有一个sql文件说明这套代码的数据库结构靠脚本管理如果这个目录不存在那就要去iMES.Core里找Migrations文件夹用EF Core迁移命令去生成。这两种初始化方式不一样别走错路。3.2 数据库初始化SQL脚本还是EF Core迁移iMES这种带数据库脚本的源码包最直接的初始化方式是先用SQL Server建一个空库再执行脚本。命令行执行如下sqlcmd -S localhost -U sa -P your_password -d master -Q CREATE DATABASE iMES sqlcmd -S localhost -U sa -P your_password -d iMES -i database/iMES.sql第一条命令创建名为iMES的数据库第二条命令把脚本里的建表语句和初始数据一次性执行进去。执行完可以用下面命令确认关键表是否创建成功sqlcmd -S localhost -U sa -P your_password -d iMES -Q SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES如果看到WorkOrder、ProductionReport、ProcessRoute这些表说明数据库已经就位。有些源码包提供的是EF Core迁移脚本那就不需要手动执行sql而是在iMES.Api目录下运行dotnet ef database update但这个命令依赖项目里的DbContext配置好的连接串而且生产环境用迁移改表容易出权限问题。我的习惯是如果源码带sql脚本就用脚本迁移脚本只作为参考。数据库同步软件的坑后面会提到这里先记住一个原则——MES的表结构变更必须有记录不能直接在生产库里手工加列。3.3 用Swagger验证最小接口工单分页查询数据库就绪后启动后端服务。在src/iMES.Api目录下执行dotnet run启动成功后控制台会打印监听地址通常是http://localhost:5000。打开浏览器访问http://localhost:5000/swagger如果Swagger页面能打开说明ASP.NET Core框架起来了。接下来不要急着乱点先找登录接口拿token再调工单分页接口。因为MES几乎所有业务接口都需要登录不先登录的话返回401会把你搞晕。Swagger页面里一般会有/api/auth/login和/api/workorder/page两个接口。登录接口的请求体是用户名和密码返回一个token字符串。工单接口的query参数是pageIndex、pageSize、statusstatus传-1代表查询全部。手动测试一次返回JSON里total大于0rows数组里有工单数据这个后端就真正通了。提示如果启动时提示连接数据库失败回到appsettings.json检查连接串不要急着改代码。数据库服务没起、用户名密码错误、实例名是Named Instance这三个原因占了九成。4. 让Vue前端真正连上C#接口登录、工单列表与看板刷新后端起来以后前端这一步更磨人。Vue工程要安装依赖、配代理、处理跨域、挂token稍有一个环节没对上页面就是白屏或者一直转圈。我见过太多人卡在这里不是因为代码复杂而是不知道Vite代理和Nginx反代之间的关系。前端工程跑通后整套系统的面貌才算完整。4.1 Vue工程初始化环境配置和Vite代理iMES.Web这个前端工程常见做法是基于Vue 3加Vite构建。先打开终端进入前端目录安装依赖cd src/iMES.Web npm install不要跳过这一步直接npm run dev不然会报缺模块。装完之后重点检查根目录的vite.config.js里面要配置开发代理// vite.config.js export default { server: { port: 8080, proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } }这段配置的意思是前端跑在8080端口所有以/api开头的请求都转发到后端5000端口changeOrigin让后端收到的请求头Host变成5000。没有这个代理浏览器直接用8080请求5000接口就会触发CORS拦截。配置好之后执行npm run dev浏览器访问http://localhost:8080打开开发者工具看网络请求。如果登录接口状态码是200而不是CORS错误说明代理生效。生产环境不需要这个代理直接用Nginx把/api反向代理到后端地址前端代码里baseURL不变。4.2 登录和请求拦截把token挂到axios上MES登录成功后后端返回一个token前端要把它存起来并且每次请求都带上。在request.js里加拦截器是标准做法// api/request.js import axios from axios const http axios.create({ baseURL: /api, timeout: 30000 }) http.interceptors.request.use(config { const token localStorage.getItem(imes_token) if (token) { config.headers.Authorization Bearer ${token} } return config }) http.interceptors.response.use( resp resp.data, err { if (err.response err.response.status 401) { localStorage.removeItem(imes_token) window.location.href /login } return Promise.reject(err) } ) export default http这段代码里请求拦截器从localStorage里拿token放到Authorization头响应拦截器做两件事返回resp.data让业务代码直接拿到数据体不用再嵌一层遇到401就清理token并跳转登录页。在MES系统里401不只代表登录过期还可能是用户被禁用、权限变更统一跳登录是止损最直接的做法。登录表单提交时用vue-router跳转页面。我一般会在登录成功后用router.push(/dashboard)然后在路由守卫里判断token是否存在这样未登录的人访问不了看板页面。注意token的存储位置localStorage刷新不丢sessionStorage关浏览器就丢工厂环境网络不稳定建议用localStorage。4.3 工单看板的实时刷新轮询和SignalR哪个够用MES看板要显示每个工位的完工数量、当前工单、设备状态。很多开发者第一反应是上SignalR但在我看来现场看板刷新频率10到15秒就足够轮询比WebSocket简单得多也不容易断线重连出问题。只有当你看板要精确到秒、或者需要服务端主动推送报警时再上SignalR不迟。Vue里用轮询的写法很简洁script setup import { ref, onMounted, onBeforeUnmount } from vue import { fetchDashboard } from ../api/dashboard const panel ref([]) let timer null async function refresh() { panel.value await fetchDashboard({ workshop: SMT }) } onMounted(() { refresh() timer setInterval(refresh, 15000) }) onBeforeUnmount(() { clearInterval(timer) }) /script这里refresh函数每次从后端拉取看板数据onMounted里先拉一次然后每15秒拉一次组件卸载时清除定时器避免页面切走后还在后台请求。参数workshop是车间编码写死成SMT只是演示实际应该由登录用户所在车间决定。看板卡片我习惯抽成通用组件用vue插槽把当前异常信息设备状态点这些动态内容插进去这样不同车间复用同一套样式只需要传入不同数据。轮询的间隔不要设太短MES数据库扛不住一秒一次的查询尤其是老工厂的SQL Server15秒是安全值。5. 数据库设计与联调避坑五条别等上线再翻车这一章是我最想写的部分。C#和Vue的MES系统前后端代码其实都还好调真正让项目延期的全是数据库和联调阶段的细节。我在多个工厂踩过同样的坑今天挑五条最典型的写出来每一条都是真实发生过的翻车现场。5.1 工单号唯一性翻车同一产品在不同产线重复建单现象很直接数据导入后生产日报显示同一张工单的完工数量翻倍追溯一查发现同一条工单号插了两条记录分别挂在A线和B线。原因不在接口而在表结构——WorkOrder表只有自增Id主键没有对业务唯一键做约束导入程序写入前也没查重数据库层面就放行了。解决方法是先清理重复数据再补联合唯一索引ALTER TABLE WorkOrder ADD CONSTRAINT UQ_WorkOrder_No UNIQUE (FactoryCode, WorkOrderNo, ProductVersion);这里FactoryCode是多工厂部署必需的同一工厂内WorkOrderNo加ProductVersion才能唯一。索引建好之后重复插入会报2627错误你在C#接口里catch住这个唯一键异常返回工单号已存在而不是500这一步就是MES系统专业和业余的分水岭。5.2 报工数量重复提交现场在制永远对不上现象操作工双击报工按钮或者报工请求超时后客户端自动重试同一笔合格数被写进数据库两次。结果就是WIP在制数量虚高车间盘点的实际数永远比系统少。查下来原因在于报工接口不是幂等的前端没有防重复后端也没有唯一约束。解决思路是给报工表加一个业务流水号字段ReportUid由前端提交时生成一次后端插入时做唯一约束ALTER TABLE ProductionReport ADD CONSTRAINT UQ_Report_Uid UNIQUE (ReportUid);C#端插入时捕获唯一键冲突返回请勿重复报工。同时在前端点击报工后立即禁用按钮等接口返回成功或失败再恢复。如果车间网络差导致超时让用户重新查询当前工位报工记录而不是让他再点一次提交。5.3 DateTime时区错乱日报统计少8小时现象ERP传过来的完工时间是2025-06-01 08:30:00MES界面显示成2025-06-01 00:30:00夜班日报始终对不上。原因很简单C#里DateTime默认不带时区信息序列化成JSON时按服务器本地时间输出前端用JavaScript的Date解析时又按浏览器时区转了一次一来一回就少8小时。解决方法是统一约定API传输用ISO8601带时区偏移的字符串或者数据库字段直接用DateTimeOffset。如果源码已经大量用了DateTime就在后端序列化配置里固定格式builder.Services.AddControllers() .AddJsonOptions(o o.JsonSerializerOptions.Converters .Add(new DateTimeConverter(yyyy-MM-dd HH:mm:ss)));实际项目里我只在数据库层用本地时间API输出显式带偏移前端不做任何时区转换。记住MES和ERP接口对接时宁可两边都写死用东八区也不要让JavaScript参与时区判断。5.4 Decimal精度丢失Vue端算出奇怪小数现象报工数量输入3.6到C#后端一校验变成3.5999999999999996合格率计算出来是99.999966%产线主管直接截图来问是不是系统bug。原因在于前端JavaScript Number是双精度浮点JSON传输时把decimal转成double损失了精度。解决方式前端输入框用字符串类型绑定后端模型属性必须用decimal不能用double。C#这样定义字段public decimal ReportQty { get; set; } // 必须是 decimal不能是 double如果前端确实需要计算汇总数用整数运算比如数量乘以1000再算或者引入decimal.js。不要在axios拦截器里做parseFloat那只会把精度问题带到更深层。MES是计数的系统每一件都关系工资和成本这块不能马虎。5.5 CORS跨域把联调堵住现象前端在8080端口后端在5000端口浏览器直接请求接口Console里全是CORS policy: No Access-Control-Allow-Origin header红字vue路由跳转正常但所有数据都请求不到。原因多半是开发时没走代理直接跨域调后端。解决开发环境用4.1节里的Vite代理生产环境用Nginx统一反代这两个方案都不用后端开CORS。如果出于特殊原因一定要后端开CORS把来源写死builder.Services.AddCors(o o.AddPolicy(Default, p p.WithOrigins(http://localhost:8080) .AllowAnyHeader() .AllowAnyMethod()));注意生产环境不要用AllowAnyOrigin配合AllowCredentials那是安全漏洞。自己写MES接口时CORS策略里只放行你知道的前端域名其他一律拒绝。6. 用一组带刺的测试数据验证整条生产追溯链系统第一次上线前很多人会拿一套正常生产的数据做演示所有工单都顺顺利利完工追溯也一路绿灯。可真正上线后只要出现返修、报废、换线系统就露馅。我现在接手任何一套C#和Vue的MES源码都会先灌一组带刺数据把流程里的岔路都走一遍再决定要不要往下改。我习惯构造这样的场景一张工单WO-TEST-001产品经过3道工序在第2道工序做例行质检时判出1件返修、1件报废返修品回到第1道工序前重做报废品单独记录不良代码最后剩余数量完工入库。验证的关键不是看工单状态而是查追溯表看这1件返修品能否追到原始报工记录报废品能否关联到操作工、设备和不良原因。我常用的验证SQL是一条递归查询产品批次链路SELECT * FROM Traceability WHERE BatchNo WO-TEST-001-SN-05 ORDER BY ProcessSeq, OperateTime;期望的结果是这个批次号在生产报表里只有一个去向要么在成品库要么在报废表绝对不能既算完工又算报废。如果你的MES源码在这条查询下出现两条路径说明工单状态流转还没闭环上线前必须修。我现在每接一套新系统第一件事就是跑这个场景确认追溯链通畅了才敢往下加功能。这个习惯救过我好几回很多看起来不起眼的数据库同步、状态判断问题都要在这种带分支的测试数据里才暴光。希望帮到你。本文还有配套的精品资源点击获取