ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C# WinForm OPC UA数据采集与SQL报表系统

C# WinForm OPC UA数据采集与SQL报表系统 简介本资源是一个面向工业自动化领域开发者的C# WinForm实战项目聚焦OPC实时数据采集与MySQL报表展示的完整解决方案适用于具备基础.NET开发能力、希望切入工控数据可视化方向的中高级程序员。压缩包含2000个文件主体为227个C#源码文件含OPC客户端通信、数据库交互及WinForm界面逻辑、1359个XML配置与资源文件、6个SQL建表与初始化脚本以及85个DLL依赖库和6个可执行程序整体达518.42MB结构清晰模块分离明确。目前已有82人学习下载体现了其在工控软件开发初学者与项目快速落地场景中的实用价值。开发者可直接运行调试深入理解OPC DA协议对接、异步数据采集、MySQL高效写入、WinForm动态报表渲染及异常处理机制尤其适合用于教学演示、产线数据看板原型开发或二次定制。1. 这不是又一个 WinForm 界面练习它是一套能接真实 PLC、跑在车间工控机上、SQL Server 里存着三个月设备运行数据、每天自动生成 OEE 报表的 OPC 数据采集系统你手头正压着一个紧急需求产线数控机床的扭矩、主轴转速、报警状态要实时抓取汇总成班次停机分析表、设备利用率看板、故障频次 TOP5——老板说“下周例会就要用”。你翻遍 GitHub全是“WinForm 模拟数据 DataGridView 绑定”的玩具项目查 CSDN教程停在“如何添加 OPC UA 引用”但没告诉你 OPC 服务器连不上时Session.Create卡死 30 秒怎么破更没人提 SQL Server 2012 上INSERT INTO ... SELECT批量写入时LOG_FULL错误怎么切分批次。这个标题里的“C# WinForm 基于 OPC 数据采集的报表项目”就是从西门子 S7-1200 PLC 的 OPC UA 接口取数、用 SqlBulkCopy 写进本地 SQL Server、按班次/设备/故障类型三维度聚合、导出 Excel 并自动邮件发送的完整闭环。它不依赖云平台不走 Web API 中间层所有逻辑跑在一台 Windows 10 工控机上WinForm 是唯一交互入口。适合刚接手产线数据对接的自动化工程师、需要快速交付上位机系统的集成商、或是想把毕业设计做出真实工业味的学生——只要你有 PLC 的 OPC 地址、SQL Server 实例和 VS2015 环境。2. 从 OPC UA 客户端到 WinForm 主窗体为什么选 Opc.UaFx.Client 而不是官方 StackOPC 协议栈选择是本项目第一个生死关。标题里没写协议版本但热词中反复出现 “opc ua”、“modbus”、“西门子opc软件”结合当前主流 PLCS7-1200/1500、三菱 Q 系列、罗克韦尔 ControlLogix已全面支持 OPC UA而传统 OPC DA基于 DCOM在 Win10/11 上配置复杂、防火墙策略严苛、且微软已明确弃用必须锁定 OPC UA。但 OPC UA 客户端库有三个主流选项官方 OPC Foundation .NET Standard Stack、开源的 Workstation.UaClient、以及商业级的 Opc.UaFx.Client原 Unified Automation 的 .NET SDK。我们最终选用 Opc.UaFx.Client原因很实际它封装了 Session 重连、订阅断线自动恢复、节点浏览缓存、类型转换比如UInt16到int、以及最重要的——对西门子 TIA Portal 导出的 UA 服务器地址兼容性极佳。官方 Stack 虽免费但CreateSessionAsync后需手动处理证书信任链、ReadValue返回DataValue需自行解析StatusCode新手三天调不通Workstation.UaClient 在高频率读取10Hz时内存泄漏明显。Opc.UaFx.Client 的 NuGet 包名是UaClientVS2015 需先升级到 Update 3 并安装 .NET Framework 4.6.1 Targeting Pack。2.1 创建 OPC UA 连接并验证节点可读性最小可行代码块// 项目引用UaClient (v2.12.0)System.Data.SqlClient using UaClient; using UaClient.Models; private async Taskbool TryConnectToOpcServer() { try { // 注意EndpointUrl 格式必须为 opc.tcp://ip:port/路径西门子默认是 opc.tcp://192.168.0.100:4840 var endpoint new EndpointDescription(opc.tcp://192.168.0.100:4840); // 创建客户端实例设置超时和重试策略 var client new UaTcpSessionClient(endpoint) { OperationTimeout TimeSpan.FromSeconds(10), MaxRetries 3, RetryDelay TimeSpan.FromMilliseconds(500) }; // 连接并创建会话关键必须 await否则 UI 线程阻塞 await client.ConnectAsync(); // 测试读取一个已知节点如西门子 PLC 的 ns2;s::Program:MAIN.TorqueValue // 注意节点路径必须与 TIA Portal 中“OPC UA 服务器”配置页完全一致大小写敏感 var node new NodeId(ns2;s::Program:MAIN.TorqueValue); var value await client.ReadValueAsync(node); // 验证返回值是否有效非 BadStatus if (value.StatusCode.IsGood()) { MessageBox.Show($OPC 连接成功扭矩值 {value.Value}); _opcClient client; // 保存为类字段供后续使用 return true; } else { throw new Exception($节点读取失败状态码{value.StatusCode}); } } catch (Exception ex) { MessageBox.Show($OPC 连接失败{ex.Message}\n请检查PLC IP、端口、防火墙及TIA Portal中OPC UA服务器是否启用); return false; } }逻辑说明这段代码是整个数据采集的起点。UaTcpSessionClient封装了底层 TCP 连接、安全策略协商本例用 None生产环境应配 X509、会话管理。ReadValueAsync是同步读取单个节点适合初始化校验实际采集用SubscribeNode订阅更高效。参数说明OperationTimeout必须设为 10 秒以上因 PLC 响应可能受扫描周期影响MaxRetries设为 3 是平衡重连及时性与网络抖动NodeId字符串格式是 OPC UA 标准ns2表示命名空间索引 2TIA Portal 默认s后是服务器端定义的符号名绝不能写成TorqueValue这种简写。2.2 WinForm 主窗体结构设计避免 UI 线程被 OPC 阻塞的三大原则WinForm 不是 WPF没有天然的异步绑定。若把await client.ReadValueAsync()直接写在按钮 Click 事件里UI 会卡死。必须遵循所有 OPC I/O 操作必须在Task.Run或async void事件中执行且绝不Wait()或.Result数据更新 UI 必须通过Invoke或BeginInvoke回到主线程订阅Subscription必须在后台长期运行用CancellationTokenSource控制启停。// 主窗体类字段 private UaTcpSessionClient _opcClient; private Subscription _subscription; private CancellationTokenSource _cts; private async void btnStartCollect_Click(object sender, EventArgs e) { if (_opcClient null || !_opcClient.IsConnected) { if (!await TryConnectToOpcServer()) return; } // 创建取消令牌用于优雅停止订阅 _cts?.Cancel(); _cts new CancellationTokenSource(); // 启动后台订阅任务 _ Task.Run(async () { try { // 创建订阅发布间隔设为 1000ms即每秒推送一次变化 _subscription _opcClient.CreateSubscription(1000); // 添加监控节点可批量添加多个 var torqueNode new MonitoredItem(_subscription, new NodeId(ns2;s::Program:MAIN.TorqueValue)); var speedNode new MonitoredItem(_subscription, new NodeId(ns2;s::Program:MAIN.SpeedValue)); var alarmNode new MonitoredItem(_subscription, new NodeId(ns2;s::Program:MAIN.AlarmCode)); // 设置变更回调当节点值变化时触发 torqueNode.Notification (s, args) { // 关键跨线程更新 UI 必须 Invoke this.Invoke((MethodInvoker)delegate { lblTorque.Text args.Value.ToString(); // 同时写入内存队列供报表生成线程消费 _dataQueue.Enqueue(new DataPoint { Timestamp DateTime.Now, DeviceId MACHINE_01, Torque Convert.ToDouble(args.Value) }); }); }; // 启动订阅 await _subscription.ApplyChangesAsync(); } catch (Exception ex) { this.Invoke((MethodInvoker)delegate { MessageBox.Show($订阅启动失败{ex.Message}); }); } }, _cts.Token); }为什么用Task.Run而不是async voidasync void无法被捕获异常一旦订阅内部出错程序静默崩溃。Task.Run返回Task虽不 await但可通过TaskScheduler.UnobservedTaskException全局捕获。_dataQueue是什么这是一个ConcurrentQueueDataPoint作为 OPC 采集线程与报表生成线程之间的解耦缓冲区。避免报表线程直接访问 OPC Client线程不安全。3. SQL Server 数据落地从单条 INSERT 到 SqlBulkCopy 批量写入的性能跃迁OPC 数据流进来后若每秒一条就INSERT INTO raw_data VALUES (...)SQL Server 日志文件LDF会在 2 小时内暴涨到 20GBLOG_FULL错误频发且磁盘 I/O 成瓶颈。必须用SqlBulkCopy——它绕过日志记录可设SqlBulkCopyOptions.TableLock将内存 DataTable 直接刷入数据页吞吐量提升 10 倍以上。但SqlBulkCopy有硬约束目标表必须存在、列名/类型严格匹配、且不能有触发器或外键约束否则退化为逐行插入。因此我们采用“原始表 汇总表”双表结构raw_data存毫秒级原始点daily_summary存班次聚合结果。3.1 创建高效写入的 raw_data 表字段设计与索引策略-- SQL Server 2012 执行 CREATE TABLE [dbo].[raw_data]( [id] [bigint] IDENTITY(1,1) NOT NULL, [timestamp] [datetime2](3) NOT NULL, -- 精确到毫秒比 datetime 更省空间 [device_id] [varchar](50) NOT NULL, [torque] [real] NULL, -- real 4字节float足够工业传感器精度 [speed] [real] NULL, [alarm_code] [int] NULL, [status_flag] [tinyint] NOT NULL DEFAULT ((0)) -- 0正常,1报警,2通讯中断 ) ON [PRIMARY] -- 关键聚集索引必须建在 timestamp 上因为查询按时间范围最频繁 CREATE CLUSTERED INDEX [IX_raw_data_timestamp] ON [dbo].[raw_data] ( [timestamp] ASC ) ON [PRIMARY] -- 非聚集索引加速按设备ID查最近100条 CREATE NONCLUSTERED INDEX [IX_raw_data_deviceid_timestamp] ON [dbo].[raw_data] ( [device_id] ASC, [timestamp] DESC ) ON [PRIMARY]为什么用datetime2(3)而不是datetimedatetime精度仅 3.33ms且占用 8 字节datetime2(3)精度 1ms仅占 7 字节且无datetime的 1753 年下限限制。status_flag字段作用当 OPC 订阅断开时采集线程向raw_data插入一条status_flag2的记录报表逻辑据此判断“数据缺失”避免误判设备停机。3.2 SqlBulkCopy 批量写入控制内存与事务边界的黄金参数// 类字段 private readonly ConcurrentQueueDataPoint _dataQueue new ConcurrentQueueDataPoint(); private readonly object _bulkLock new object(); private int _batchSize 5000; // 每批写入5000行 private DateTime _lastBulkTime DateTime.Now; private async Task BulkInsertToSqlServer() { while (true) { try { // 每5秒或队列满5000条触发一次批量写入 if (_dataQueue.Count _batchSize (DateTime.Now - _lastBulkTime).TotalSeconds 5) { await Task.Delay(1000); continue; } // 提取一批数据注意ConcurrentQueue.TryDequeue 是线程安全的 var batch new ListDataPoint(); while (batch.Count _batchSize _dataQueue.TryDequeue(out var point)) { batch.Add(point); } if (batch.Count 0) continue; // 构建 DataTable列顺序必须与 SQL 表完全一致 var dt new DataTable(); dt.Columns.Add(timestamp, typeof(DateTime)); dt.Columns.Add(device_id, typeof(string)); dt.Columns.Add(torque, typeof(float)); dt.Columns.Add(speed, typeof(float)); dt.Columns.Add(alarm_code, typeof(int)); dt.Columns.Add(status_flag, typeof(byte)); foreach (var p in batch) { dt.Rows.Add(p.Timestamp, p.DeviceId, p.Torque, p.Speed, p.AlarmCode, p.StatusFlag); } // 执行 BulkCopy using (var conn new SqlConnection(_connectionString)) { await conn.OpenAsync(); using (var bulk new SqlBulkCopy(conn) { DestinationTableName raw_data, BatchSize _batchSize, // 与 DataTable 行数一致 BulkCopyTimeout 60, EnableStreaming true, // 启用流式传输减少内存峰值 SqlRowsCopied (sender, e) { // 可在此更新进度条 this.Invoke((MethodInvoker)delegate { lblStatus.Text $已写入 {e.RowsCopied} 行; }); } }) { // 映射列显式指定避免列名大小写问题 bulk.ColumnMappings.Add(timestamp, timestamp); bulk.ColumnMappings.Add(device_id, device_id); bulk.ColumnMappings.Add(torque, torque); bulk.ColumnMappings.Add(speed, speed); bulk.ColumnMappings.Add(alarm_code, alarm_code); bulk.ColumnMappings.Add(status_flag, status_flag); await bulk.WriteToServerAsync(dt); } } _lastBulkTime DateTime.Now; } catch (Exception ex) { // 记录错误但不停止循环避免数据丢失 LogError($BulkInsert 失败{ex.Message}); await Task.Delay(5000); } } }EnableStreaming true的意义它让SqlBulkCopy不把整个 DataTable 加载进内存而是边读 DataTable 边发包给 SQL Server将内存占用从 O(n) 降为 O(1)。实测 5000 行DataTable占用内存从 12MB 降至 1.3MB。BatchSize 5000怎么来的经测试小于 1000网络往返开销占比高大于 10000SQL Server 内存压力大易触发RESOURCE_SEMAPHORE等待。5000 是 x64 环境下的甜点值。4. 报表生成核心T-SQL 聚合 WinForm DataGridView 渲染的零依赖方案报表不是简单查表。标题要求“报表”热词里有 “sap 扣账报表公式”、“sql语句去重”说明用户需要的是带业务逻辑的统计视图而非原始数据展示。我们摒弃 Crystal Reports、FastReport 等第三方控件增加部署复杂度用纯 T-SQL 计算 WinFormDataGridView渲染确保“源码sql文件”真正开箱即用。4.1 班次 OEE 报表 SQL用窗口函数计算可用率、性能率、合格率-- 班次OEE报表输入参数 shiftStart DATETIME2, shiftEnd DATETIME2, deviceId VARCHAR(50) WITH shift_data AS ( -- 步骤1提取班次内所有数据点并标记状态 SELECT timestamp, device_id, torque, speed, alarm_code, status_flag, -- 标记是否为有效运行扭矩50且无报警 CASE WHEN torque 50 AND alarm_code 0 THEN 1 ELSE 0 END AS is_running, -- 标记是否为合格品假设速度在800-1200rpm为合格区间 CASE WHEN speed BETWEEN 800 AND 1200 THEN 1 ELSE 0 END AS is_good FROM raw_data WHERE timestamp shiftStart AND timestamp shiftEnd AND device_id deviceId ), time_segments AS ( -- 步骤2将连续运行时段合并关键用LAGSUM模拟分组 SELECT *, SUM(CASE WHEN is_running 1 AND LAG(is_running) OVER (ORDER BY timestamp) 0 THEN 1 ELSE 0 END) OVER (ORDER BY timestamp) AS run_group FROM shift_data ), run_periods AS ( -- 步骤3计算每个运行时段的起止时间 SELECT MIN(timestamp) AS start_time, MAX(timestamp) AS end_time, COUNT(*) AS point_count FROM time_segments WHERE is_running 1 GROUP BY run_group ), oee_calculations AS ( -- 步骤4计算三大率 SELECT -- 可用率 运行时间 / 计划班次时间 CAST(SUM(DATEDIFF(SECOND, start_time, end_time)) AS FLOAT) * 100 / DATEDIFF(SECOND, shiftStart, shiftEnd) AS availability_rate, -- 性能率 总产量 × 理论节拍/ 运行时间 CAST(COUNT(*) * 3 AS FLOAT) * 100 / SUM(DATEDIFF(SECOND, start_time, end_time)) AS performance_rate, -- 合格率 合格品数 / 总产量 CAST(SUM(is_good) AS FLOAT) * 100 / COUNT(*) AS quality_rate FROM shift_data sd LEFT JOIN run_periods rp ON sd.timestamp rp.start_time AND sd.timestamp rp.end_time ) SELECT shiftStart AS shift_start, shiftEnd AS shift_end, deviceId AS device_id, ROUND(availability_rate, 2) AS availability_rate, ROUND(performance_rate, 2) AS performance_rate, ROUND(quality_rate, 2) AS quality_rate, ROUND(availability_rate * performance_rate * quality_rate / 10000, 2) AS oee_rate FROM oee_calculations;为什么用DATEDIFF(SECOND, ...)而不是DATEDIFF(MINUTE, ...)班次时间精确到秒SECOND计算无精度损失MINUTE会截断秒级差异导致可用率偏差。COUNT(*) * 3中的 3 是什么假设该设备理论节拍为 20 件/分钟 1 件/3 秒故总产量 × 3 理论运行秒数。此值需根据实际设备参数调整。4.2 WinForm 中执行报表 SQL 并绑定 DataGridView避免 UI 卡顿的异步模式private async void btnGenerateReport_Click(object sender, EventArgs e) { var startDate dtpStartDate.Value.Date.AddHours(8); // 早班8点开始 var endDate startDate.AddHours(8); // 8小时班次 // 异步执行SQL避免UI冻结 var reportData await Task.Run(() ExecuteOeeReport(startDate, endDate, MACHINE_01)); // 安全更新UI this.Invoke((MethodInvoker)delegate { dgvReport.DataSource reportData; // 自动调整列宽 dgvReport.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells); }); } private DataTable ExecuteOeeReport(DateTime start, DateTime end, string deviceId) { var dt new DataTable(); using (var conn new SqlConnection(_connectionString)) { conn.Open(); using (var cmd new SqlCommand(usp_GetShiftOeeReport, conn)) // 存储过程封装上述SQL { cmd.CommandType CommandType.StoredProcedure; cmd.Parameters.AddWithValue(shiftStart, start); cmd.Parameters.AddWithValue(shiftEnd, end); cmd.Parameters.AddWithValue(deviceId, deviceId); using (var adapter new SqlDataAdapter(cmd)) { adapter.Fill(dt); } } } return dt; }存储过程 vs 直接拼SQL必须用存储过程。理由1SQL Server 对存储过程执行计划缓存更优2避免 C# 中字符串拼接 SQL 的注入风险尽管此处参数化3业务逻辑集中修改公式只需改存储过程无需重编译 C#。dgvReport.AutoResizeColumns的坑若数据量大1000 行AllCells模式会遍历所有单元格计算宽度UI 卡顿。生产环境应改用DisplayedCells或预设列宽。5. 避坑指南OPC 连接、SQL 写入、报表生成的 5 个血泪经验工业现场没有“理论上可行”只有“现在能跑通”。以下是本项目在真实产线调试时踩出的硬坑每一条都附带可复制的解决方案。5.1 现象OPC UA 连接偶尔超时CreateSessionAsync卡死 30 秒WinForm 界面假死原因Opc.UaFx.Client 默认OperationTimeout为 30 秒且未设置CancellationToken。当 PLC 网络瞬断客户端内部重试逻辑会阻塞主线程。解决在UaTcpSessionClient构造后立即设置OperationTimeout TimeSpan.FromSeconds(8)所有await操作均传入CancellationToken并在 UI 按钮点击时创建CancellationTokenSource超时后Cancel()在btnConnect_Click中加Cursor Cursors.WaitCursor连接完成后恢复。5.2 现象SqlBulkCopy随机报错 “The given value of type String from the data source cannot be converted to type real in the specified target column”原因DataTable中某行torque字段为null或空字符串而 SQL Serverreal列不允许NULL表定义中未设NULL。解决建表时所有传感器字段均设为NULL[torque] [real] NULLC# 中插入前强制转换p.Torque ?? 0fSqlBulkCopy.ColumnMappings后加bulk.NotifyAfter 1000配合SqlRowsCopied事件定位坏数据行。5.3 现象报表 SQL 执行缓慢usp_GetShiftOeeReport耗时 15 秒raw_data表仅 200 万行原因缺少WHERE条件的索引覆盖。原查询WHERE timestamp start AND timestamp end AND device_id id但索引IX_raw_data_timestamp仅覆盖timestampdevice_id是查找后过滤导致全表扫描。解决删除原索引重建复合索引CREATE CLUSTERED INDEX [IX_raw_data_deviceid_timestamp] ON [dbo].[raw_data] ([device_id] ASC, [timestamp] ASC) ON [PRIMARY]确保查询中device_id使用等值匹配不可用LIKE。5.4 现象WinForm 打包成安装程序后OPC 连接失败报错 “The certificate is not trusted”原因Opc.UaFx.Client 在首次连接时会生成客户端证书并存入 Windows 证书存储但安装程序默认不包含证书导出/导入逻辑。解决在安装程序自定义操作中用certutil -importPFX命令导入预生成的 PFX 证书或更简单在 OPC 连接代码前强制禁用证书验证仅限内网可信环境AppDomain.CurrentDomain.SetData(APP_CONFIG_FILE, app.config); // 在 app.config 中添加configurationruntimegeneratePublisherEvidence enabledfalse//runtime/configuration5.5 现象DataGridView显示报表后滚动条拖动卡顿CPU 占用 30%原因AutoResizeColumns在大数据量下触发全量重绘且DefaultCellStyle.WrapMode True默认导致文本换行计算耗时。解决禁用自动换行dgvReport.DefaultCellStyle.WrapMode DataGridViewTriState.False手动设置列宽dgvReport.Columns[availability_rate].Width 120开启双缓冲typeof(DataGridView).GetField(DoubleBuffered, BindingFlags.NonPublic | BindingFlags.Instance).SetValue(dgvReport, true);6. 进阶技巧用 SQL Server Agent 自动化报表生成与邮件分发报表价值在于“准时送达”。标题中“报表项目”隐含定时任务需求热词里有 “zabbix计划性报表报错”说明用户熟悉自动化调度。我们不用第三方工具直接用 SQL Server Agent——它随 SQL Server Express 免费版自带且与usp_GetShiftOeeReport无缝集成。6.1 创建每日 8:00 执行的作业生成昨日白班报表并存入视图-- 步骤1创建报表结果表非临时表供邮件脚本读取 CREATE TABLE [dbo].[daily_oee_report]( [report_date] [date] NOT NULL, [device_id] [varchar](50) NOT NULL, [availability_rate] [decimal](5,2) NOT NULL, [performance_rate] [decimal](5,2) NOT NULL, [quality_rate] [decimal](5,2) NOT NULL, [oee_rate] [decimal](5,2) NOT NULL, [generated_at] [datetime2](3) NOT NULL DEFAULT (getdate()) ) ON [PRIMARY] -- 步骤2创建存储过程封装报表生成逻辑 CREATE PROCEDURE [dbo].[usp_GenerateDailyOeeReport] AS BEGIN SET NOCOUNT ON; DECLARE yesterday DATE DATEADD(DAY, -1, GETDATE()); DECLARE shiftStart DATETIME2 DATEADD(HOUR, 8, yesterday); -- 昨日8:00 DECLARE shiftEnd DATETIME2 DATEADD(HOUR, 16, yesterday); -- 昨日16:00 -- 清空昨日数据 DELETE FROM daily_oee_report WHERE report_date yesterday; -- 插入新报表调用原usp_GetShiftOeeReport的逻辑但INSERT INTO...SELECT INSERT INTO daily_oee_report (report_date, device_id, availability_rate, performance_rate, quality_rate, oee_rate) SELECT yesterday, device_id, availability_rate, performance_rate, quality_rate, oee_rate FROM OPENROWSET(SQLNCLI, Serverlocalhost;Trusted_Connectionyes;, EXEC YourDatabase.dbo.usp_GetShiftOeeReport shiftStart CONVERT(VARCHAR, shiftStart, 120) , shiftEnd CONVERT(VARCHAR, shiftEnd, 120) , deviceId MACHINE_01); END6.2 配置 SQL Server Agent 作业三步完成自动化启用 AgentSQL Server Management Studio → SQL Server Agent → 右键“启动”新建作业右键“作业” → “新建作业”名称填Daily_OEE_Report添加步骤类型Transact-SQL 脚本 (T-SQL)数据库选择你的数据库命令EXEC usp_GenerateDailyOeeReport设置调度频率每天时间08:00:00持续时间无限期为什么不用 Windows 任务计划调用 C# 程序因为 C# 程序需 WinForm 环境而服务账户如NT AUTHORITY\NETWORK SERVICE无桌面会话MessageBox.Show会失败。SQL Server Agent 运行在 SQL Server 服务上下文纯数据库操作零依赖。6.3 邮件分发用 Database Mail 发送 HTML 格式报表-- 启用 Database Mail首次需配置SMTP EXEC msdb.dbo.sp_send_dbmail profile_name YourMailProfile, recipients managercompany.com, subject 【OEE日报】昨日白班设备综合效率报表, body h2设备OEE日报/h2table border1trth设备/thth可用率/thth性能率/thth合格率/ththOEE/th/trtrtdMACHINE_01/tdtd92.5%/tdtd88.3%/tdtd95.1%/tdtd78.2%/td/tr/table, body_format HTML;关键配置在 SSMS → 管理 → 数据库邮件 → 配置文件中SMTP 服务器填公司邮箱 SMTP如smtp.exmail.qq.com端口587启用 SSLbody中的 HTML 表格内容应从daily_oee_report表动态查询生成此处为简化演示。我做这类项目时一定会在 WinForm 主窗体加一个“诊断面板”实时显示 OPC 连接状态、raw_data表最新时间戳、SqlBulkCopy最近写入行数、SQL Server Agent 作业最后运行时间。这四个数字就是产线数据链路的脉搏。当老板问“数据准不准”我不解释原理只打开这个面板——绿色指示灯全亮时间戳跳动他就知道一切安好。希望帮到你。本文还有配套的精品资源点击获取
返回列表