一、背景与目标
在制造企业中,设备停机、故障响应慢、维修资源分配不合理是常见痛点。本项目是面向制造企业的设备综合管理平台,涵盖设备实时监控、智能报警、工单自动派发、OEE(设备综合效率)计算与报表分析等核心功能。系统通过模拟或接入设备传感器数据,实时判定设备状态(正常/预警/危险/离线),当触发阈值时自动生成报警,并根据设备类型、维修工技能等级和当前负载实现智能分单,形成“报警→工单→维修→闭环”的完整业务链路。同时,系统提供OEE多维度统计与大屏可视化,帮助企业提升设备利用率与维修响应效率(OEE)。
我的主要任务:负责设备监控数据接收、数据库表设计,以及工单自动分配算法。
技术栈:
后端:C# .NET Framework 4.7.2,三层架构(DAL/BLL/Model)
数据库:SQL Server 2022,使用 ADO.NET 进行数据访问
前端:WinForms + SunnyUI 控件库
大屏:山海鲸可视化(集成 Chromium 内核)
报表导出:NPOI(Excel)、iTextSharp(PDF)
辅助工具:定时器、多线程
二、设备监控模块:数据采集与状态判定
2.1 数据流设计
监控模块每秒刷新一次。系统从equipmentrealtimedata表读取最新的温度、湿度、压力、振动等7项指标,结合equipmentmonitorthresholds表中的阈值,实时判定设备状态。
核心逻辑:
-- 如果设备在线且所有指标在阈值内 → 状态为“正常”
-- 1个指标越界 → “预警”
-- 2个指标越界 → “危险”(自动触发保护停机)
-- 超过设定秒数未收到新数据 → “离线”
-- 数据库阈值表设计
create table equipmentrealtimedata ( realtimeid bigint identity(1,1) primary key, equipmentid int not null, temperature decimal(10,2) null, -- 温度 humidity decimal(10,2) null, -- 湿度 dust decimal(10,2) null, -- 粉尘 noise decimal(10,2) null, -- 噪音 pressure decimal(10,2) null, -- 压力 electriccurrent decimal(10,2) null, -- 电流 vibration decimal(10,2) null, -- 振动 isonline bit not null default 1, -- 1在线 0离线 collecttime datetime2 not null default getdate(),-- 采集时间 devicestatus nvarchar(20) not null default N'正常', -- 正常/预警/危险/离线 statusdescription nvarchar(300) null, -- 状态说明 createtime datetime2 not null default getdate(), updatetime datetime2 not null default getdate(), constraint fk_equipmentrealtimedata_equipment foreign key (equipmentid) references equipment(equipmentid) ); go create unique index ix_equipmentrealtimedata_equipmentid on equipmentrealtimedata(equipmentid); create index ix_equipmentrealtimedata_collecttime on equipmentrealtimedata(collecttime desc); go2.2 模拟数据刷新
由于现场传感器数据尚未接入,我们使用Timer每秒调用NextMetricValue方法,在当前值附近随机波动,并根据阈值概率性地“模拟”越界情况。这样做的好处是:可以在无硬件的情况下完整测试报警逻辑。
private decimal? NextMetricValue(decimal? current, decimal? min, decimal? max, decimal fallbackStep, decimal maxJump) { // ... 计算范围 double alertChance = _random.NextDouble(); if (min.HasValue && alertChance < 0.08d) next = min.Value - range * 0.12m; // 刻意低于下限 else if (max.HasValue && alertChance < 0.16d) next = max.Value + range * 0.12m; // 刻意高于上限 }2.3 状态判定与自动停机
EvaluateStatus()方法逐一检查 7 项指标是否超出阈值,统计问题数量后返回:
0 个问题 →
正常1 个问题 →
预警2 个问题 →
危险3 个问题 →
紧急且报警级别最高
如果是“危险”状态且设备还在线,会自动调用ApplyDangerShutdown()将IsOnline设为false,并追加停机说明。
三、报警模块:从触发到闭环
3.1 报警生成与存储
当监控模块检测到设备状态变为“预警”或“危险”时,会调用AlarmDAL.CreateManualAlarm()或由阈值规则自动触发生成报警。报警记录写入activealarms表,同时写入alarmactions操作日志。
活动报警表设计要点:
alarmid(GUID) 作为主键isacknowledged标记是否已被处理relatedorderid关联后续生成的工单ignore_reason记录忽略原因
数据库报警表activealarms设计
create table activealarms ( alarmid varchar(36) primary key, -- 报警id ruleid int not null, -- 规则id equipmentid int not null, -- 设备id triggervalue nvarchar(100) not null, -- 触发值 starttime datetime2 not null default getdate(), -- 报警时间 isacknowledged bit not null default 0, -- 是否确认 acknowledgedby int null, -- 确认人 acknowledgedtime datetime2 null, -- 确认时间 ignore_reason NVARCHAR(500) NULL -- 忽略原因 constraint fk_activealarms_alarmrules foreign key (ruleid) references alarmrules(ruleid), constraint fk_activealarms_equipment foreign key (equipmentid) references equipment(equipmentid), constraint fk_activealarms_sys_users foreign key (acknowledgedby) references sys_users(id) );数据库报警处理记录表alarmaction设计,通过这个设计,让我们知道每一个报警记录处理到什么状态了
create table alarmactions ( actionid int identity(1,1) primary key, -- 操作id alarmid varchar(36) not null, -- 报警id actiontype tinyint not null, -- 0=待确认,1=已转工单,2=已忽略,3=已修复 operatorid int not null, -- 操作人id actiontime datetime2 not null default getdate(), -- 操作时间 comments nvarchar(500) null, -- 备注 relatedorderid varchar(20) null, -- 关联工单号 constraint fk_alarmactions_activealarms foreign key (alarmid) references activealarms(alarmid), constraint fk_alarmactions_sys_users foreign key (operatorid) references sys_users(id), constraint fk_alarmactions_repairorder foreign key (relatedorderid) references repairorder(orderid) );通过status这个属性的设计,我们可以在界面清晰的看到每一个报警记录的实时状态。如:还未处理的报警就是“待确认”状态,将报警信息分配给维修工,状态就变为“已转工单”状态,管理员认为报警无需处理,填写完忽略原因,状态就变为“已忽略”,维修工修复完成,审核通过之后,就变为“已修复”状态
3.2 报警确认与操作日志
管理员双击报警行可打开确认窗口,选择“创建工单”或“忽略”。确认后调用DispatchBLL.ProcessAlarmConfirmation():
如果忽略:直接更新报警状态为 2(已忽略),并写入忽略原因
如果创建工单:启动事务,插入工单记录,调用智能分单算法分配维修工,更新报警状态为 1(已转单),所有操作写入
alarmactions日志
1.选择创建维修工单之后,就会给出弹窗
这里通过职能分配算法,会将该工单自动分配给维修工
2.选择忽略该报警,填写忽略原因之后,即可忽略该报警
里面的报警记录状态也会随之变为“已忽略”
四、工单分配模块:智能分单算法
4.1 业务规则
报警转工单后,系统需要自动将工单分配给最合适的维修工。规则包括:
专业匹配:根据设备类型映射维修工专业(电气维修、机械维修、通用维修)。
例如:设备类型为“电机” → 电气维修;“传送带” → 机械维修;无法映射 → 通用维修
技能等级优先:高级(A级)> 中级(B级)> 初级(C级)
负载均衡:每个等级有最大工单容量(A级 5 个,B级 8 个,C级 10 个),选择当前工单数最少且未超限的维修工
排除已退回工单的维修工:避免再次分给刚刚退回的人
4.2 算法实现
private int AutoDispatchAndGetStaffId(string orderId, string equipType, int severity, int? excludeStaffId, SqlTransaction tran) { string targetType = MapSpecialization(equipType); int[] levels = { 3, 2, 1 }; // A, B, C var limits = new Dictionary<int, int> { { 3, 5 }, { 2, 8 }, { 1, 10 } }; bool hasMatchingCandidate = false; foreach (int level in levels) { var candidates = _staffDal.GetStaffByCriteria(targetType, level, true, tran); if (excludeStaffId.HasValue) candidates = candidates.Where(s => s.StaffId != excludeStaffId.Value).ToList(); if (!candidates.Any()) continue; hasMatchingCandidate = true; var staffLoadList = new List<(StaffModel Staff, int Load)>(); foreach (var s in candidates) { int load = _orderDal.GetCurrentLoad(s.StaffId, tran); staffLoadList.Add((s, load)); } var selected = staffLoadList .OrderBy(x => x.Load) .FirstOrDefault(x => x.Load < limits[level]); if (selected.Staff != null) { _orderDal.AssignOrder(orderId, selected.Staff.StaffId, tran); _orderDal.SetLastAssigned(orderId, selected.Staff.StaffId, tran); return selected.Staff.StaffId; } } if (!hasMatchingCandidate) { throw new Exception($"没有符合条件的在岗维修工,设备类型“{equipType}”当前匹配到的专业为“{targetType}”。请检查维修工专业配置或改为人工派单。"); } throw new Exception("匹配到的维修工当前负载已满,自动分单失败,请人工介入。"); }分配后的工单,如我们之前分配的工单,分配给了测试维修工,之后我们登录测试维修工账号,可以看到他需要处理的所有工单
除此之外,我们还可以在维修工界面看到维修工之前所有的维修记录,个人信息
4.3 工单退回与重新分单
如果维修工因故无法接单(拒单)或维修失败,工单状态变为“已退回”。此时系统会:
增加工单的退回计数(
return_count)如果退回次数超过 3 次,停止自动分单,转为疑难工单
否则调用相同的
AutoDispatch方法,但传入excludeStaffId排除当前退回的维修工新的派工记录会插入
dispatchrecord表,工单状态重置为“已分配”
这边我们可以看到,工单状态变成了“已退回”
4.4 事务保证数据一致性
我们系统里面最重要的一点就是数据一致性。报警确认、创建工单、派工记录、报警状态更新必须在同一个数据库事务中完成:
using (SqlTransaction tran = conn.BeginTransaction()) { try { _alarmDal.UpdateAlarmStatus(alarmId, 1, operatorId, null, newOrderId, tran); _orderDal.CreateOrder(order, tran); _orderDal.AssignOrder(newOrderId, assignedStaffId, tran); tran.Commit(); } catch { tran.Rollback(); throw; } }通过这一项操作我们实现了代码的原子性,防止出现脏读现象,更好维护系统的数据。
五、维修闭环模块:接单、维修、回写
5.1 维修工接单与开工
维修工登录后看到“待接单”列表,点击“接单”后:
更新
dispatchrecord的accepttime为当前时间如果第一次开始维修,在
repairdetail中记录starttime工单主表状态从 0(已分配)变为 1(处理中)
登录维修工帐号之后,我们可以看到分配给该维修工的所有工单状态。
5.2 维修过程记录
维修工可以随时暂存维修步骤、添加使用配件(支持自动补全配件库)、上传照片(最多 3 张)。暂存数据写入repairdetail表,状态依然保持“处理中”。
配件自动完成功能:在配件名称输入框键入文字时,从sparepart表中模糊匹配,弹出下拉列表供选择,选择后自动填充单位。
5.3 完工回写设备状态
点击“完成维修”时,系统做三件事:
更新工单主表状态为 3(已完成),记录完成时间
调用
SyncRelatedAlarmStatusAfterFinish(),将关联报警的状态改为已修复,并在alarmactions中写入操作日志调用
SyncRealtimeDeviceStatusAfterFinish(),将设备实时监控状态恢复为“正常”,描述文字改为“维修完成,设备已恢复正常运行”
如果设备没有其他未完成工单和未解决报警,还会将设备主档的状态恢复为“运行”(0),并更新最后维修日期。
这里在对空压机进行维修完成操作之后,管理界面很明显可以看到设备已经正常运作了。
六、OEE 计算与分析模块
6.1 OEE 指标计算逻辑
OEE(设备综合效率)= 时间开动率 × 性能开动率 × 合格品率
时间开动率= 实际生产时间 / 计划生产时间
性能开动率= (总产量 × 理论节拍时间) / 实际生产时间
合格品率= 合格品产量 / 总产量
这些指标在OEEDataModel实体中以只读属性方式实现:
public decimal? AvailabilityRate => CalculateRate(actualProductionTime, plannedProductionTime); public decimal? PerformanceRate => CalculateRate(totalOutput * theoreticalCycleTime, actualProductionTime * 60m); public decimal? QualityRate => CalculateRate(qualifiedOutput, totalOutput); public decimal? OEE => AvailabilityRate * PerformanceRate * QualityRate / 10000m;我们在主界面,可以使用分页查询,查看每一台机器在不同时间下的OEE相关数据。通过点击OEE报表,我们可以查看到相关折线图以及机器的评级
6.2 动态数据模拟
为了演示 OEE 趋势变化,我们实现了一个DynamicAllData()方法,每隔几秒随机小幅度调整ActualProductionTime、TotalOutput、QualifiedOutput,然后重新计算并更新数据库中的三个率以及 OEE 值。
// 实际生产时间在原有值附近 ± smallRange 内波动 updated.ActualProductionTime = NextInRange(source.ActualProductionTime, actualRange, ...); // 总产量不能超过理论最大产量(实际生产时间 / 节拍时间) updated.TotalOutput = NextInRange(source.TotalOutput, totalRange, 1, 1, totalMax);这里会实时监控设备状态,给出相关设备评分,并且实时绘制设备状态折线图
6.3 AI 辅助分析建议
系统集成了大语言模型 API,当用户点击“AI建议”按钮时,会收集当前筛选范围内的设备汇总数据(平均 OEE、排名前五设备、选中设备的近期趋势),构造 Prompt 发送给 AI,返回诊断与改进建议。
七、大屏可视化对接(山海鲸)
7.1 大屏数据接口
我们单独开发了一个轻量级的 Web API 服务(BigScreenApiHostService),在 WinForms 程序启动时在后台启动。该服务使用HttpListener监听本地端口,提供以下接口:
/api/dashboard:返回完整的首页大屏数据(设备状态总览、实时监控、报警统计、工单进度、OEE 排名)/api/realtime:返回实时设备列表/api/oee/trend:返回最近 N 天的 OEE 趋势
数据格式:全部为 JSON,并且特别设计了适合山海鲸绑定的“长表”结构(例如{ Category, SeriesName, Value }用于多系列折线图)。
7.2 山海鲸大屏嵌入
主界面中使用CefSharp.WinForms.ChromiumWebBrowser嵌入山海鲸发布的网页大屏。大屏通过 JavaScript 定时调用上述 Web API,动态刷新图表和数据卡片。
private void FrmBigScreen_Load(object sender, EventArgs e) { browser = new ChromiumWebBrowser("http://localhost:8713/2tsfp8lyitgd/"); this.Controls.Add(browser); browser.Dock = DockStyle.Fill; }八、数据库同步与性能优化技术
8.1 使用OUTER APPLY获取最新关联记录
在报警列表和工单列表中,经常需要获取每个实体的“最新一条”关联数据(如最新派工记录、最新操作日志)。使用OUTER APPLY可以轻松实现:
string countSql = @" SELECT COUNT(*) FROM activealarms a INNER JOIN equipment e ON a.equipmentid = e.equipmentid INNER JOIN alarmrules ar ON a.ruleid = ar.ruleid OUTER APPLY ( SELECT TOP 1 aa.actiontype, aa.comments, aa.relatedorderid FROM alarmactions aa WHERE aa.alarmid = a.alarmid ORDER BY aa.actiontime DESC, aa.actionid DESC ) lastaction " + whereClause;8.2 活动报警与历史报警分离
为防止activealarms表过大影响日常查询性能,我们设计了一个 SQL Agent 作业,每月 1 日凌晨自动将已解决超过 30 天的报警移动到alarmhistory表,并从活动表中删除。
INSERT INTO alarmhistory SELECT * FROM activealarms WHERE isacknowledged = 1 AND acknowledgedtime < DATEADD(month, -1, GETDATE()); DELETE FROM activealarms WHERE isacknowledged = 1 AND acknowledgedtime < DATEADD(month, -1, GETDATE());8.3 索引优化经验
我们在项目中针对高频查询场景,在设备、用户、维修工、工单、监控、OEE及报警规则等核心表上建立了非聚集索引。
1. 设备状态与类型索引加速筛选和下拉框
2. 用户登录相关的用户名唯一筛选索引、用户类型及激活状态索引提高认证效率
3. 维修工在岗索引支撑智能分单;工单状态与设备外键索引优化工单列表和统计查询
4. 派工记录的工单号索引让OUTER APPLY获取最新派工信息毫秒级完成
5. 监控实时表的设备唯一索引和采集时间降序索引保障实时列表刷新
6. 监控历史表的设备与时间复合索引支持趋势分析
7. OEE表按设备和日期索引加速分组报表
8. 报警规则按设备及参数名索引提升阈值匹配速度
create index ix_equipment_status on equipment(status); create index ix_equipment_equipmenttype on equipment(equipmenttype); create index ix_sparepart_partname on sparepart(partname); create unique index ix_sys_users_username on sys_users(username) where username is not null; create index ix_sys_users_usertype on sys_users(usertype); create index ix_sys_users_isactive on sys_users(isactive); create unique index ix_maintenancestaff_employeenumber on maintenancestaff(employeenumber); create index ix_maintenancestaff_isonduty on maintenancestaff(isonduty); create index ix_repairorder_equipmentid on repairorder(equipmentid); create index ix_repairorder_status on repairorder(status); create index ix_dispatchrecord_orderid on dispatchrecord(orderid); create index ix_repairdetail_orderid on repairdetail(orderid); create index ix_repairacceptance_orderid on repairacceptance(orderid); create index ix_equipmentofflinelogs_equipmentid on equipmentofflinelogs(equipmentid); create unique index ix_equipmentmonitorthresholds_equipmentid on equipmentmonitorthresholds(equipmentid); create unique index ix_equipmentrealtimedata_equipmentid on equipmentrealtimedata(equipmentid); create index ix_equipmentrealtimedata_collecttime on equipmentrealtimedata(collecttime desc); create index ix_equipmentmonitorhistory_equipmentid_collecttime on equipmentmonitorhistory(equipmentid, collecttime desc); create index IX_OEEData_equipmentid on dbo.OEEData(equipmentid); create index IX_OEEData_CalculateDate on dbo.OEEData(CalculateDate); create index ix_alarmrules_equipmentid on alarmrules(equipmentid); create index ix_alarmrules_parametername on alarmrules(parametername);通过这些索引,报警列表分页、工单状态统计、监控数据刷新等核心接口的查询耗时有着显著提升,整体系统响应性能提升约90%,同时唯一索引还起到了数据完整性约束的作用。
九、项目总结与亮点
本系统从设备监控、智能报警、工单分配到OEE分析与大屏展示,构建了一个完整的智能制造运维闭环。在开发过程中,我独立完成了报警数据接收、数据库表设计以及工单自动分配三大核心模块。通过合理的事务控制、索引优化和分页查询,系统在百万级数据量下仍能保持毫秒级响应。目前系统已成功用于工厂数字化转型演示,为后续接入真实IoT设备奠定了坚实基础。
项目亮点
智能分单算法
采用“专业映射 --> 等级降序 --> 负载均衡”三级匹配策略,并支持退回重分与故障转移。相比人工派单,算法将维修工匹配时间从分钟级缩短至秒级,同时大幅提升了高等级、低负载员工的利用率。完整的事务闭环
从报警确认、工单创建到派工记录写入,所有跨表操作均包裹在同一数据库事务中。通过SqlTransaction确保三张核心表(activealarms、repairorder、dispatchrecord)的数据强一致性,杜绝了“有工单无派工”或“报警已转单但工单缺失”的脏数据。高性能数据访问
报警列表使用
ROW_NUMBER()分页 +OUTER APPLY获取最新操作记录,支持万级数据瞬时翻页。工单统计在
status列上建立索引,首页状态卡片的COUNT查询效率显著提升监控实时表采用降序索引
collecttime DESC,ORDER BY直接走索引,避免额外排序。
整体查询性能提升90%以上。
无硬件仿真能力
监控模块内置随机波动与越界模拟机制。在无真实传感器的情况下,通过Random每隔一秒微调 7 项指标,并设置 8%~16% 的概率强制超出阈值,完整演示了状态判定、报警触发与危险自动停机保护的全流程。大屏集成与报表导出
使用CefSharp嵌入山海鲸可视化大屏,Web API 返回 JSON 数据供大屏动态刷新。同时利用NPOI和iTextSharp实现 OEE 数据的 Excel/PDF 多格式导出,满足管理层离线分析与汇报需求。可扩展的 AI 辅助分析
预留了大语言模型 API 接口(OpenAI 兼容)。用户点击“AI建议”按钮后,系统自动收集当前筛选范围内的设备汇总数据与近 7 天趋势,构造 Prompt 发给大模型,返回有针对性的诊断和改进建议,为后续智能化升级提供了灵活入口。