资讯动态

C# MES系统源码解析:工单扫码报工与批次追溯

发布时间:2026/10/30 5:06:09 来源:尧图企业网站定制
简介一份面向工业信息化开发者与 .NET 学习者的 C# MES 系统源码可用于理解制造执行系统从基础资料、数据采集到报表输出的整体实现也适合作为课程设计、二次开发或企业原型搭建的参考。压缩包共 95 个文件、约 1.81MB以 54 个 cs 源码和 24 个 resx 界面资源为主辅以 5 个 dll 组件、3 个 sql 脚本及 sln 解决方案、配置文件等目录按基础模块、数据报表、数据采集等主题组织便于按功能检索。已有 3221 人学习下载说明其在 MES 入门与实战参考方面具有一定关注度。源码涵盖三层架构、实体建模、数据库访问、设备通信、工作流、报表可视化、异常与质量控制、权限安全及接口集成等知识点读者可据此梳理业务分层、理解采集与报表链路并借鉴模块划分和编码组织方式。1. 车间白板上的产量为什么总比系统里多三百件做过车间数字化的人大概都见过这个场景计划员用 Excel 排产工艺卡打印出来夹在流转箱里跟着物料走班组长下班前把当班产量写在一块白板上月底财务拿着白板照片和 ERP 里的数据对账差三百件谁也说不出差在哪。问题不在于人不用心而在于中间那段“工单下达到工序、工序报到批次、批次追到原料”的过程没有一条能被系统记住的链路。C# 写的 MES 系统源码干的正是这件事把这条件链路变成可查、可改、可扩展的代码。这套源码的定位很明确——面向中小型离散制造车间机加工、电子组装、注塑这类多工序、多批次的场景覆盖工单下达、派工、扫码报工、工序流转、物料批次追溯、设备状态采集、车间看板和质量报表。技术栈基本是 .NET 那条主线ASP.NET Core 做服务端EF Core 配 SQL Server 做持久化WPF 或 WinForms 跑车间终端Web 端做管理后台SignalR 推看板实时数据。适合谁手上有 .NET 底子、要交付车间改造项目的开发者或者想拿一套能跑通的完整业务系统练手的人。纯前端背景的读者读起来会有点吃力因为一半的价值在服务端的事务和并发处理上。2. 架构与目录一次扫码报工在后端穿过了几层2.1 分层结构与真实目录树拿到一套 C# MES 源码第一件事不是急着 F5 跑起来而是先看目录。分层清不清楚直接决定了你后面加一个字段要改几个文件。这套项目用的是比较典型的四层加多端结构服务端按职责切成 WebApi、Application、Domain、Infrastructure 四个程序集客户端单独放定时任务单独放。MesSolution/ ├── src/ │ ├── Mes.WebApi/ # ASP.NET Core 宿主控制器、中间件、Swagger │ │ ├── Controllers/ # WorkOrderController、ReportController... │ │ ├── Filters/ # 全局异常、操作日志、权限校验 │ │ └── Program.cs │ ├── Mes.Application/ # 用例层一个业务动作一个 Service │ │ ├── WorkOrders/ # 工单下达、拆分、关闭 │ │ ├── Reporting/ # 报工、返工、报废 │ │ ├── Tracing/ # 正反追溯 │ │ └── Dtos/ │ ├── Mes.Domain/ # 实体、值对象、领域事件、枚举 │ │ ├── Entities/ │ │ ├── Enums/ # WorkOrderStatus、ReportType │ │ └── Events/ │ ├── Mes.Infrastructure/ # EF Core、仓储实现、Redis、消息队列 │ │ ├── Persistence/ │ │ │ ├── MesDbContext.cs │ │ │ └── Configurations/ # 每个实体一个 IEntityTypeConfiguration │ │ ├── Repositories/ │ │ └── Messaging/ │ ├── Mes.Client/ # WPF 车间终端扫码枪、打印机 │ └── Mes.Job/ # Quartz 定时任务日结、超期工单提醒 └── tests/ └── Mes.Application.Tests/这个结构的价值在于依赖方向是单向的WebApi 依赖 ApplicationApplication 依赖 DomainInfrastructure 只被外层引用Domain 不依赖任何人。你改一个报工规则只在 Application 里动手不会牵连到控制器你换一个数据库只动 Infrastructure业务代码一行不改。我见过不少车间项目把 EF 的 DbContext 直接注进 Controller前两年看着爽第三年客户要加一个“同一工单同一工序不允许重复报工超过三次”的规则就得满项目找地方塞 if。Mes.Client和Mes.Job是两个容易被忽略但很关键的部分。车间终端跑在工位平板上要处理扫码枪的键盘模拟输入、断网续传、本地缓存定时任务则负责那些不该放在请求链路里的活——日产量汇总、超期工单推送、批次有效期预警。把这两块单独拆出来服务端就不会被车间现场的各种异常拖垮。2.2 报工接口的完整调用链真正体现分层价值的是一次报工从进来到返回的这段路。控制器只做三件事接参数、拿当前登录人、调服务。业务判断全在 Application 层数据访问全在仓储里。// Mes.WebApi/Controllers/ReportController.cs [ApiController] [Route(api/report)] public class ReportController : ControllerBase { private readonly IReportService _reportService; private readonly ICurrentUser _currentUser; public ReportController(IReportService reportService, ICurrentUser currentUser) { _reportService reportService; _currentUser currentUser; } [HttpPost(scan)] public async TaskApiResultReportResultDto ScanReport([FromBody] ScanReportInput input) { // 幂等键由终端生成工单号工序号操作员时间戳防止扫码枪重复触发 var cmd new ScanReportCommand( WorkOrderNo: input.WorkOrderNo, ProcessCode: input.ProcessCode, Barcode: input.Barcode, Qty: input.Qty, OperatorId: _currentUser.UserId, IdempotencyKey: input.IdempotencyKey); var result await _reportService.ReportAsync(cmd); return ApiResultReportResultDto.Ok(result); } }控制器里没有任何 if 判断这是刻意的。IdempotencyKey这个参数值得单独说一句车间扫码枪在手抖或者网络超时重试的时候很容易把同一条报工发两遍产量凭空多出来一截。让终端在扫码那一刻就生成一个唯一键服务端拿它做去重比在数据库层面靠“同一秒同一工单只能有一条记录”这种时间戳判断靠谱得多——后者的血泪经验是两个操作员在两台终端上同时扫时间戳能撞到毫秒级。再看服务层这里是业务规则真正落地的地方。// Mes.Application/Reporting/ReportService.cs public async TaskReportResultDto ReportAsync(ScanReportCommand cmd) { // 1. 幂等校验命中直接返回上次结果不重复写库 var cached await _idempotencyStore.TryGetAsync(cmd.IdempotencyKey); if (cached is not null) return cached; await using var tx await _unitOfWork.BeginTransactionAsync(); // 2. 取出工单与工序校验状态与剩余可报数量 var workOrder await _workOrderRepo.GetWithProcessesAsync(cmd.WorkOrderNo) ?? throw new BizException(工单不存在); if (workOrder.Status is not (WorkOrderStatus.Released or WorkOrderStatus.InProgress)) throw new BizException($工单当前状态 {workOrder.Status} 不允许报工); var process workOrder.Processes.SingleOrDefault(p p.ProcessCode cmd.ProcessCode) ?? throw new BizException(该工单不包含此工序); // 3. 领域方法内部做剩余数量校验和乐观锁版本递增 var record workOrder.Report(process, cmd.Qty, cmd.OperatorId, cmd.Barcode); await _workOrderRepo.UpdateAsync(workOrder); await _reportRepo.AddAsync(record); await _unitOfWork.CommitAsync(tx); // 4. 事务提交后再发事件避免消息先于数据到达看板 await _eventBus.PublishAsync(new ProductionReportedEvent(record)); var dto ReportResultDto.From(record, process); await _idempotencyStore.SaveAsync(cmd.IdempotencyKey, dto, TimeSpan.FromHours(8)); return dto; }逐段说清楚。第一步的幂等存储常见做法是 Redis键设 8 小时过期覆盖一个班次如果客户现场没有 Redis退化成一张IdempotencyRecord表加唯一索引也能扛。第二步的GetWithProcessesAsync必须带工序集合一起加载否则后面workOrder.Processes是空的报错信息还很隐晦——这种坑我吃过不止一次。第三步把校验逻辑放进领域实体而不是服务层是为了让“超报”这条规则只有一个出口将来改判定条件不用满项目搜。第四步的顺序很关键事件必须在事务提交之后发很多项目写成提交前发结果是看板先刷出来一条报工数据库回滚了看板上的数字对不上查起来像玄学。参数上还要注意cmd.Qty的类型。产量用decimal还是int取决于行业机加工常有 0.5 件的计量比如按重量折算用int会截断用decimal(18,3)更稳。条形码Barcode存原始字符串不要在写入时做 trim 或大小写转换——追溯的时候要按原样还原改了就追不上。2.3 车间终端与看板的客户端形态服务端之外这套源码还带两个客户端形态这是它比纯 Web 后台 MES 更贴近现场的地方。第一个是 WPF 工位终端跑在工位平板上负责扫码、显示当前工序作业指导书、打印流转卡标签。第二个是看板大屏纯浏览器页面通过 SignalR 接收服务端推送。终端这边最需要处理的是扫码枪。绝大多数工业扫码枪是键盘模拟模式扫一次等于快速敲一串字符再加一个回车WPF 里监听PreviewKeyDown收集字符、遇到回车就触发查询是常规写法。麻烦在于焦点管理操作员扫完码可能顺手点了别的输入框下一次扫码的字符就散到别处去了。稳一点的做法是给整窗口挂键盘钩子按时间间隔比如 50ms 内连续输入的字符判定这是一次扫码而不是手工打字。看板这边要注意断线重连。车间网络不像办公室交换机在配电柜边上跳闸就断。SignalR 客户端默认会自动重连但重连成功之后要主动拉一次全量快照否则大屏上的数字会停在断线那一刻。这个细节源码里一般有但藏得比较深接手时要专门确认一遍。3. 核心链路拆解工单下达、扫码报工、批次追溯的代码实现3.1 工单下达主工单拆成工序级子任务MES 里最基础也最容易做歪的一层是工单模型。很多项目把工单和工序压成一张表一个工单一条记录工序用逗号分隔塞在字段里前期省事后面要按工序统计良率、按工序派工、按工序卡在制品全得推倒重来。这套源码用的是主工单加工序任务的两级结构工单下达的动作本质上是“把工艺路线实例化成一串可执行的任务”。// Mes.Domain/Entities/WorkOrder.cs节选 public class WorkOrder { public long Id { get; private set; } public string WorkOrderNo { get; private set; } default!; public string ProductCode { get; private set; } default!; public decimal PlanQty { get; private set; } public WorkOrderStatus Status { get; private set; } public byte[] RowVersion { get; private set; } default!; // 乐观锁 private readonly ListWorkOrderProcess _processes new(); public IReadOnlyCollectionWorkOrderProcess Processes _processes; // 下达按工艺路线生成工序任务第一道工序置为可开工其余等待 public void Release(IReadOnlyListRoutingStep routing) { if (Status ! WorkOrderStatus.Created) throw new BizException(只有新建状态的工单可以下达); var seq 1; foreach (var step in routing.OrderBy(r r.Sequence)) { _processes.Add(new WorkOrderProcess( processCode: step.ProcessCode, sequence: seq, planQty: PlanQty, status: seq 1 ? ProcessStatus.Ready : ProcessStatus.Waiting)); } Status WorkOrderStatus.Released; } }Release方法里有两个设计决定值得说。第一是工序状态机只有第一道是 Ready后面全是 Waiting上一道报工完成、且质量判定合格之后才把下一道推进到 Ready。这条规则如果放在服务层很容易在“返工”“跳序”这些分支里被绕过去放在实体上任何调用方都绕不开。第二是PlanQty的下传每道工序的计划数默认等于工单计划数但实际业务里前道工序可能有损耗系数比如注塑件按 1.02 倍投料。这个系数在我的项目里一般是加在工艺路线的step.LossRate上下达时按PlanQty * (1 LossRate)做进位取整而不是简单复制。工序任务上还挂了两个字段容易漏InputQty和OutputQty。前者是本工序实际接收到的上道产出后者是本工序已报工数量。在制品WIP就是两者的差。看板上那条“在制 1,240 件”的数字来源就是全厂所有 Ready 状态工序的InputQty - OutputQty求和不是一个单独的统计表。这一点在二次开发时很重要——你要加在制品预警改的是查询不是新增一张表。3.2 扫码报工并发累计怎么不加锁还能算对报工是整套系统里并发最高的入口。一条产线二十个工位同一时刻可能有七八个人在扫同一个工单的不同工序。如果按“先查库存、再算余额、最后更新”的经典写法超报几乎是必然的。这套源码用的是乐观锁加领域内校验核心在于RowVersion和更新时的并发冲突处理。// Mes.Infrastructure/Repositories/WorkOrderRepository.cs public async Task UpdateAsync(WorkOrder workOrder) { // EF Core 会把 RowVersion 作为 WHERE 条件的一部分 // UPDATE WorkOrders SET ... WHERE Id id AND RowVersion version _db.Entry(workOrder).Property(w w.RowVersion).OriginalValue workOrder.RowVersion; _db.WorkOrders.Update(workOrder); await _db.SaveChangesAsync(); }配合服务层的重试完整逻辑是这样的更新时如果数据库里的RowVersion已经变了EF 会抛DbUpdateConcurrencyException服务层捕获后重新加载工单、重新校验剩余数量、再试一次最多三次。// Mes.Application/Reporting/ReportService.cs重试节选 for (var attempt 1; attempt 3; attempt) { try { var workOrder await _repo.GetWithProcessesAsync(cmd.WorkOrderNo); var process workOrder!.Processes.Single(p p.ProcessCode cmd.ProcessCode); // 领域内校验本工序累计报工不得超过计划数 允许超报比例 workOrder.Report(process, cmd.Qty, cmd.OperatorId, cmd.Barcode); await _repo.UpdateAsync(workOrder); return; } catch (DbUpdateConcurrencyException) when (attempt 3) { await Task.Delay(20 * attempt); // 退避减少同一工单的再次碰撞 } } throw new BizException(报工冲突请重新扫码);参数上Report里的超报判定一般写成process.OutputQty qty process.PlanQty * (1 AllowOverRate)AllowOverRate默认 0客户现场如果确实存在多投料的情况在工单上单独配置不要全局放开。Task.Delay的退避时间也不要设太长车间的操作员没有耐心等两秒第三次失败直接让终端提示重扫用户体感反而更好。为什么不用悲观锁SELECT ... WITH (UPDLOCK)因为报工这条链路上读多写少而且同一个工单被并发报工的概率其实不高大部分碰撞来自扫码枪重复触发那个用幂等键就解决了。悲观锁会把一批本来不冲突的请求串行化产线忙的时候排队明显。乐观锁的代价是偶发的重试收益是吞吐这笔账在车间场景下是划算的。当然如果你做了“工位机 看板 PDA”三端同时报工冲突率上去了可以在UpdateAsync外面加一个按工单号的分布式锁做兜底粒度是工单号不是全局。3.3 双向追溯从成品批号倒推到原料批号追溯这块是整个 MES 里最容易被验收卡住的功能。客户的审核员会拿一个成品箱子上的批号要求你在系统里三十秒内还原出这批用了哪些原材料批次、经过哪些工序、每道工序的操作员和检验记录、对应哪台设备。做不出来就是不符合项。这套源码的追溯数据结构是两张表加一条关系Batch批次记录物料批号、供应商、有效期、BatchFlow批次流转记录每道工序的投入批次和产出批次。核心是正反两个方向的查询。// Mes.Application/Tracing/TraceService.cs // 反查成品批号 - 所有上游原料批次BFS 逐层向上 public async TaskTraceGraphDto TraceBackwardAsync(string batchNo, int maxDepth 10) { var graph new TraceGraphDto(); var queue new Queue(string BatchNo, int Depth)(); var visited new HashSetstring(StringComparer.OrdinalIgnoreCase); queue.Enqueue((batchNo, 0)); while (queue.Count 0) { var (current, depth) queue.Dequeue(); if (depth maxDepth || !visited.Add(current)) continue; graph.Nodes.Add(await _batchRepo.GetNodeAsync(current)); // 查这张流转卡上投入了哪些批次方向是我用了谁 var inputs await _flowRepo.GetInputBatchesAsync(current); foreach (var input in inputs) { graph.Edges.Add(new TraceEdge(input.BatchNo, current)); queue.Enqueue((input.BatchNo, depth 1)); } } return graph; }maxDepth这个参数是防止脏数据把查询打成死循环的保险。批次关系里偶尔会出现环比如返工批次又回到了前道工序没有深度限制BFS 会一直跑。visited集合的双重作用是去重和断环两个都不能省。正查是同一个算法的反方向从原料批号出发查“谁用了我”一路推到成品。实现上就是把GetInputBatchesAsync换成GetOutputBatchesAsync其余结构一样。真正费时间的不是算法是 SQL 单批查询次数——一个成品批号往上追三层可能要发上百次查询。进阶做法是用 SQL Server 的递归 CTE 一次拉平但可维护性会差一些。我的经验是先按 BFS 写对再用AsNoTracking加批量IN查询优化到可接受别一上来就上递归 CTE那种 SQL 出了问题是真难调。追溯结果的呈现也要注意。图形化展示通常给一个树或者桑基图但要按工序分层同一层的批次按时间排。审核员看的是“有没有断点”如果某一层的输入批次为空系统要显式标红提示“该工序未记录上料”而不是默默跳过。很多项目上线后被挑出来的问题不是追溯算错而是漏了输入、界面看不出来。4. 数据层落地实体映射、事务边界和索引怎么定4.1 实体设计与 EF Core 映射约定这套源码的持久层用 EF Core实体配置走IEntityTypeConfiguration分文件而不是把一堆特性Attribute糊在实体类上。这个选择在二次开发阶段的价值极大——业务实体保持干净映射细节全在 Infrastructure 里换个数据库方言只改配置。// Mes.Infrastructure/Persistence/Configurations/WorkOrderConfiguration.cs public class WorkOrderConfiguration : IEntityTypeConfigurationWorkOrder { public void Configure(EntityTypeBuilderWorkOrder builder) { builder.ToTable(MES_WorkOrder); builder.HasKey(x x.Id); builder.Property(x x.WorkOrderNo).HasMaxLength(32).IsRequired(); // 工单号唯一防止重复导入 builder.HasIndex(x x.WorkOrderNo).IsUnique(); builder.Property(x x.PlanQty).HasPrecision(18, 3); builder.Property(x x.Status).HasConversionstring().HasMaxLength(20); builder.Property(x x.RowVersion).IsRowVersion(); // 一对多工序任务随工单聚合保存 builder.HasMany(x x.Processes) .WithOne() .HasForeignKey(p p.WorkOrderId) .OnDelete(DeleteBehavior.Cascade); builder.Navigation(x x.Processes).UsePropertyAccessMode(PropertyAccessMode.Field); } }几个参数值得单独解释。Status用HasConversionstring()存字符串而不是 int看着浪费空间但排查问题时直接SELECT就能读懂值几个字节。UsePropertyAccessMode(PropertyAccessMode.Field)配合实体里的私有_processes字段保证外部只能通过Processes只读集合访问不能直接往里塞聚合边界守住了。RowVersion对应 SQL Server 的rowversion类型如果客户用的是 MySQL这里要换成Timestamp加手动维护EF 的IsRowVersion在 MySQL 上不会自动生成。命名前缀MES_是我个人习惯原因是工厂里一套数据库经常挂好几个系统——ERP、WMS、旧版 MES 都在同一个实例里不加前缀分不清谁是谁。这不是硬性要求但接手别人的库时你会感谢自己。4.2 事务边界一次报工里哪些必须在一个事务里事务边界划得对不对决定了系统在异常时的数据一致性。很多新手把整个请求包进一个事务包括发消息、写日志、调外部接口结果外部接口慢一下数据库锁就压住了。报工这条链路上必须在一个事务里的只有两件事写报工记录、更新工单工序的累计数量。这两条要么都成功要么都失败否则出现“报了工但产量没加”的脏数据追溯的时候断链。消息推送、幂等缓存、操作日志全部放在事务外面异步或者提交后执行。public async TaskT ExecuteAsyncT(FuncTaskT action) { // SQL Server 开启执行策略以应对死锁重试配合乐观锁最多重试 3 次 var strategy _db.Database.CreateExecutionStrategy(); return await strategy.ExecuteAsync(async () { await using var tx await _db.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted); try { var result await action(); await tx.CommitAsync(); return result; } catch { await tx.RollbackAsync(); throw; } }); }隔离级别选ReadCommitted而不是RepeatableRead因为报工路径上主要是单行更新不需要范围锁用更高的隔离级别只会增加阻塞。真正需要防的是幻读的地方比如日结统计那类场景单独开Serializable或者干脆走快照读。还有个细节CreateExecutionStrategy在启用重试策略EnableRetryOnFailure时必须用否则 EF 会直接抛异常说“使用重试策略时不能手动开启事务”。这个报错很多人第一次遇到会懵其实是 EF 在提醒你要把事务包进执行策略里。4.3 索引与冷热分离看板查询为什么会拖垮生产库MES 上线三个月后最常见的性能投诉是“看板卡了”。原因往往不是看板页面写得差而是它的查询和生产库共用一套表一条按时间范围扫全表的统计 SQL把报工接口的锁等了。索引这块报工表至少要三个(WorkOrderId, ProcessId)用于累计查询(ReportTime)用于日报统计(Barcode)用于扫码定位。别再多了报工表是写入最频繁的表每多一个索引写入就多一份开销。工单表上(Status, PlanStartTime)这个组合索引是给“待开工工单列表”用的缺了它车间首页打开要三秒。冷热分离是更根本的解法。日产量、月产量这类统计正确做法不是实时查报工明细而是定时任务每小时把明细聚合进一张MES_ProductionSummary表看板只查汇总表。这套源码里Mes.Job项目干的就是这件事走的是“增量聚合”按LastSummaryTime往后算避免每次全量重算。提示聚合任务的幂等性一定要做。任务重跑很常见发布重启、手动补数据如果按“直接累加”写重跑一次产量就翻倍。正确做法是按业务键先删后插或者用MERGE。5. 避坑与排查上线头一个月最容易翻车的五个地方5.1 报工数量偶尔多出一倍扫码枪重复触发现象某个工位的当班产量比实际多出几十件查报工明细能看到同一秒有两条一模一样的记录操作员编号、条码、数量全相同。原因扫码枪在键盘模拟模式下按下扳机到松开如果时间稍长会连发两次字符加上终端在等待服务端响应期间没有禁用扫码第二次扫的码直接又发了一遍请求。这不是并发问题是终端没做防抖。解决终端侧收到回车后立即置一个IsScanning标志等接口返回再放开服务端侧上幂等键键由终端在扫码瞬间生成工单号 工序号 条码 时间戳毫秒Redis 存 8 小时。两层一起做前者减少无效请求后者兜底。不要只做其中一层只做终端防抖终端崩溃重启就失效只做服务端幂等无效请求照样打满接口。5.2 看板数字比实际晚八小时时区在数据库里就错了现象车间早班八点开始的产量看板上显示在昨天下午。查数据库里的ReportTime时间是对的但前端显示差了八小时。原因服务端用DateTime.Now写入本地时间前端拿到之后又当成 UTC 转了一次或者数据库列类型是datetime不带时区跨时区部署时彻底乱套。解决全链路统一用 UTC 存储列类型改datetimeoffset或者datetime2但约定存 UTC只在展示层转本地时区。Program.cs里显式配置 JSON 序列化options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter())之外日期统一输出 ISO 8601 带偏移量。这个坑属于典型的“上线才炸”开发机上都是本地时区永远测不出来。5.3 报工接口偶发 500DbContext 被并发复用现象压力一上来报工接口随机报A second operation was started on this context instance before a previous operation completed。原因某个 Service 里用了Task.WhenAll并行的两个查询或者在一个BackgroundService里把注入的DbContextScoped 生命周期跨线程用了。EF Core 的DbContext不是线程安全的一个实例同时只能跑一个操作。解决并行查询要么串行化要么每个分支用IDbContextFactoryMesDbContext单独建上下文。后台任务里更要注意不能注入 Scoped 的 DbContext必须用工厂。这个报错信息还算友好但出现在生产日志里的时候往往已经被包装成别的异常了。5.4 部署到客户内网后看板不刷新WebSocket 被拦现象本地开发看板实时刷新正常部署到车间内网后页面数据要手动 F5 才更新浏览器控制台没有明显报错。原因SignalR 优先协商 WebSocket客户内网的某些网络设备不支持协议升级协商失败后回落到长轮询但如果服务端没开长轮询或者反向代理没配Upgrade头连接就彻底连不上。解决服务端MapHub之后显式配置options.Transports HttpTransportType.WebSockets | HttpTransportType.LongPolling让客户端有回落路径反向代理侧放行Upgrade和Connection请求头。前端加withAutomaticReconnect并在onreconnected里主动拉一次全量快照——重连成功不等于数据是新的。5.5 追溯查不出结果条码大小写被系统悄悄改了现象审核员拿箱子上的条码查追溯系统提示“批次不存在”但肉眼看着一模一样。原因条码打印时是大写客户现场用另一台设备录入时输入了小写中间某处代码做了ToUpper()或者数据库排序规则是_CI_AS不区分大小写导致写入时存的是一种查询时用的是另一种。解决批次号、工单号这类业务键写入时保留原始值不 trim、不改大小写查询时统一做规范化比较。数据库排序规则建库时就定好别后期改——ALTER DATABASE ... COLLATE在有大表的时候基本等同于重建。这个坑不显眼但一旦出问题追溯功能就等于废了。6. 二次开发给工序加一个自定义字段从数据库一路改到看板真实项目里交付三个月之后客户一定会提“我们每道工序还想记一个设备编号 / 刀具号 / 环境温湿度”。这种需求看着小但如果一开始架构没留口子改起来就是从数据库到看板的一条长链。下面这套改法我走过很多遍基本是稳的。先做方案选择。加物理列最直接查询快、类型安全缺点是每次加字段都要走一次发布加扩展 JSON 列灵活不动表结构缺点是查询和索引麻烦EF Core 里要用OwnsOne或者自己写转换器。判断标准很简单这个字段需不需要参与筛选、排序、统计需要就加列不需要就进 JSON。方案适用场景改动范围查询性能物理列要筛选、要统计、字段稳定实体 配置 DTO 前端 迁移脚本好可建索引JSON 扩展列只是记录不参与查询实体加一个字典属性 前端动态表单一般全表扫描独立扩展表字段多且按客户定制新增表 关联查询中等需 join以“工序设备编号”为例走物理列方案改动是四步。第一步加实体属性第二步加映射配置第三步改 DTO 和控制器入参第四步前端表单加一个输入框、看板加一列。第三步最容易漏——很多人改完实体直接跑结果接口返回的 DTO 里没这个字段前端一直空白还以为是映射没生效。// 1. Domain工序任务加设备编号 public class WorkOrderProcess { public string? EquipmentCode { get; private set; } public void AssignEquipment(string equipmentCode) { if (string.IsNullOrWhiteSpace(equipmentCode)) throw new BizException(设备编号不能为空); EquipmentCode equipmentCode.Trim().ToUpperInvariant(); } } // 2. Infrastructure映射长度按客户实际编码规则定 builder.Property(p p.EquipmentCode).HasMaxLength(32); // 3. ApplicationDTO 补字段AutoMapper 或手写映射都要加 public record WorkOrderProcessDto( string ProcessCode, int Sequence, decimal PlanQty, decimal OutputQty, string? EquipmentCode); // 4. 前端报工弹窗加输入框看板列配置里加一项验证方法我一般固定三步先在开发库上跑dotnet ef migrations add AddEquipmentCode看一眼生成的 SQL 是不是只有ADD没有DROP然后手工插一条历史工单确认老数据这一列是NULL而不是空字符串避免统计时把空串算成一类最后跑一遍报工-看板-追溯的完整链路特别确认追溯图里工序节点能带出设备编号。有个后悔药要提前备好加字段的迁移脚本一定要写成可回滚的形式Down方法里把列删掉。车间系统发布经常是晚上十点以后出了问题现场没人能等你想办法回滚脚本能在五分钟内把系统恢复到上一个版本。还有一个习惯从那以后我每次给这类系统加字段都强制走一遍“实体-映射-DTO-前端-迁移脚本”这五步清单少一步都不发布。吃过一次亏DTO 漏了字段前端空白了三天客户以为是系统坏了最后发现是我改漏了一行。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑