using System; using System.Collections.Generic; using System.Data; using System.Drawing; using System.Drawing.Drawing2D; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; using System.Timers; using System.Windows.Forms; using MySql.Data.MySqlClient; using Newtonsoft.Json; using YiDa_WinForm.Config; using YiDa_WinForm.Model; using YiDa_WinForm.Service; using YiDa_WinForm.Utils; using NPOI.SS.UserModel; namespace YiDa_WinForm { public partial class MainForm : Form { // 依赖注入 private readonly MqttClientService _mqttService; private readonly YiDaUploadService _uploadService; private readonly ButtonOperationService _buttonService; // 通用上传定时器 private System.Timers.Timer _timer; // 日志文件相关路径 private readonly string _logDirectory = "log"; private string _currentLogFile; private DateTime _lastLogDate = DateTime.MinValue; // 工作时间配置 private TimeSpan _workStartTime = new TimeSpan(9, 0, 0); private TimeSpan _workEndTime = new TimeSpan(23, 30, 0); // 连杆测试定时报警相关 private System.Timers.Timer _rodAlarmTimer; private bool _isImageUploadedInCycle = false; // 报废上传定时报警相关 private System.Timers.Timer _scrapAlarmTimer; private bool _isScrapImageUploadedInCycle = false; // 配方 private DataTable _formulas; // 原始配方表(含多行,未去重) private DataTable _dtMqttDic; // MQTT 参数字典表 private List _formulasList1; // 设备1选择的配方(多行) private List _formulasList2; // 设备2选择的配方(多行) // 生产相关标记 private static bool _isProduction = false; private static bool _isRodImageUploaded = false; // 连杆图片是否上传 private static bool _isScrapImageUploaded = false; // 报废图片是否上传 // 报废间隔 private int _lastScrapInterval = 60; // 关键:这个变量在主窗体生命周期内不会销毁,能保留值 // 连杆间隔 private int _lastStrengthInterval = 60; // 持久化保存两个弹窗的上次勾选状态(核心:再次打开弹窗保留用户选择) private bool _lastScrapUploadSelected = true; // 报废:上次是否勾选「上传凭证」 private bool _lastScrapTimedAlarmSelected = true; // 报废:上次是否勾选「定时报警」 private bool _lastRodUploadSelected = true; // 连杆:上次是否勾选「模具上传」 private bool _lastRodTimedAlarmSelected = true; // 连杆:上次是否勾选「定时报警」 // 早晚固定时段连杆上传报警相关 private System.Timers.Timer _morningRodAlarmTimer; // 早上检查点定时器(工作开始+30分钟) private System.Timers.Timer _eveningRodAlarmTimer; // 晚上检查点定时器(工作结束-30分钟) private bool _isMorningRodUploaded = false; // 早上标记(工作开始后30分钟前是否上传) private bool _isEveningRodUploaded = false; // 晚上标记(工作结束前30分钟前是否上传) private TimeSpan _morningCheckTime; // 早上检查节点(_workStartTime + 30分钟) private TimeSpan _eveningCheckTime; // 晚上检查节点(_workEndTime - 30分钟) // ======================================== 初始化相关方法 ======================================== public MainForm() { // 初始化设计器方法 InitializeComponent(); // 订阅日志事件 LogHelper.OnLogGenerated += LogHelper_OnLogGenerated; // 初始化服务实例 _buttonService = new ButtonOperationService(); _mqttService = new MqttClientService(); _uploadService = new YiDaUploadService(); _buttonService.MessageReceived += (msg) => { LogHelper.AppendLog($"服务层消息:{msg}"); }; // 绑定回调事件(连杆+报废) _mqttService.MessageReceived += OnMqttMessage; _uploadService.RodUploadStatusChanged += OnRodUploadStatusChanged; _uploadService.ScrapUploadStatusChanged += OnScrapUploadStatusChanged; // UI初始化配置( buttonDisconnect.Enabled = false; // 初始化日志目录 InitializeLogDirectory(); // 初始化配方相关变量 _formulas = new DataTable(); _dtMqttDic = new DataTable(); _formulasList1 = new List(); _formulasList2 = new List(); InitDailyRodCheckTimes(); } /// /// 初始化日志目录 /// private void InitializeLogDirectory() { if (!Directory.Exists(_logDirectory)) { Directory.CreateDirectory(_logDirectory); } UpdateLogFile(); } /// /// 更新当前日志文件(每日一个新文件) /// private void UpdateLogFile() { DateTime today = DateTime.Now.Date; if (today != _lastLogDate) { _lastLogDate = today; _currentLogFile = Path.Combine(_logDirectory, $"log_{today:yyyyMMdd}.txt"); } } /// /// 加载MainForm信息 /// private async void Form1_Load(object sender, EventArgs e) { try { MakePanelRound(panelLed); await InitFormulas(); // 移植:加载配方 InitMqttDic(); // 移植:加载MQTT字典 timer1.Enabled = true; } catch (Exception ex) { LogHelper.AppendLog($"初始化失败:{ex.Message}"); } } /// /// 将指定 Panel 变成圆形 /// private static void MakePanelRound(Panel panel) { int size = Math.Min(panel.Width, panel.Height); panel.Size = new Size(size, size); GraphicsPath path = new GraphicsPath(); path.AddEllipse(0, 0, panel.Width, panel.Height); panel.Region = new Region(path); } // ======================================== 配方核心方法 ======================================== /// /// 查询配方并去重 /// private async Task InitFormulas() { DataTable dt = await _buttonService.QueryFormulaAsync(); if (dt != null && dt.Rows.Count > 0) { // 新增列用于拼接 part_number|part_name if (!dt.Columns.Contains("part_number_name")) dt.Columns.Add("part_number_name", typeof(string)); foreach (DataRow row in dt.Rows) { row["part_number_name"] = row["part_number"].ToString() + "|" + row["part_name"].ToString(); } // 使用 LINQ 去重,按 part_number + part_name var uniqueParts = dt.AsEnumerable() .GroupBy(r => r["part_number_name"].ToString()) .Select(g => g.First()) .ToList(); // 新建 DataTable 用于绑定下拉框 DataTable dtUnique = dt.Clone(); // 保留原有列 foreach (var row in uniqueParts) { dtUnique.ImportRow(row); } // 新建空白行 DataRow drNew = dtUnique.NewRow(); drNew["id"] = -1; drNew["part_number_name"] = ""; // 空行值 dtUnique.Rows.InsertAt(drNew, 0); _formulas = dt; // 原始配方表(多行) bsPF.DataSource = dtUnique; bsPF2.DataSource = dtUnique; comboBox1.DisplayMember = "part_number_name"; // 显示“零件号|零件名” comboBox1.ValueMember = "part_number_name"; comboBox2.DisplayMember = "part_number_name"; comboBox2.ValueMember = "part_number_name"; // 自定义显示格式:显示“零件号 - 零件名” comboBox1.Format += (s, e) => { if (e.ListItem is DataRowView drv) { e.Value = string.IsNullOrEmpty(drv["part_number_name"].ToString()) ? "" : drv["part_number"].ToString() + " - " + drv["part_name"].ToString(); } }; comboBox2.Format += (s, e) => { if (e.ListItem is DataRowView drv) { e.Value = string.IsNullOrEmpty(drv["part_number_name"].ToString()) ? "" : drv["part_number"].ToString() + " - " + drv["part_name"].ToString(); } }; } } /// /// 移植:初始化MQTT字典(第二份 Form1 的 InitMqttDic() 完整迁移) /// private async void InitMqttDic() { DataTable dt = await _buttonService.InitMqttDic(); // 适配第一份的 _buttonService,替换第二份的 _mqttService if (dt != null) { _dtMqttDic = dt; } } // ======================================== 按钮相关方法 ======================================== /// /// 网关连接按钮 /// private async void buttonConnect_Click(object sender, EventArgs e) { try { LogHelper.AppendLog("正在连接MQTT服务器..."); await _mqttService.MqttClientStartAsync(); LogHelper.AppendLog("连接成功!"); panelLed.BackColor = Color.Green; toolStripStatusLabel3.Text = "已连接"; buttonConnect.Enabled = false; buttonDisconnect.Enabled = true; // 1. 重置静态上传标记(每次连接都重新开始判断) _isRodImageUploaded = false; _isScrapImageUploaded = false; LogHelper.AppendLog("重置上传标记:连杆未上传、报废未上传"); // 2. 启动一次性定时任务(复用用户保存的间隔值) StartOneTimeAlarmTimers(); StartDailyRodFixedTimers(); } catch (Exception ex) { panelLed.BackColor = Color.Red; LogHelper.AppendLog($"连接失败:{ex.Message}"); toolStripStatusLabel3.Text = "连接失败"; } } /// /// 网关断开按钮 /// private async void buttonDisconnect_Click(object sender, EventArgs e) { try { await _mqttService.MqttClientStopAsync(); LogHelper.AppendLog("已断开连接"); panelLed.BackColor = Color.Red; toolStripStatusLabel3.Text = "未连接"; buttonConnect.Enabled = true; buttonDisconnect.Enabled = false; StopAllAlarmTimers(); StopDailyRodFixedTimers(); } catch (Exception ex) { LogHelper.AppendLog($"无法断开连接:{ex.Message}"); } } /// /// 配方导入按钮 /// private async void button1_Click(object sender, EventArgs e) { try { OpenFileDialog openFile = new OpenFileDialog(); openFile.Filter = "Excel文件|*.xlsx;*.xls"; if (openFile.ShowDialog() != DialogResult.OK) { return; } // 显示加载提示 this.Cursor = Cursors.WaitCursor; LogHelper.AppendLog("正在导入配方,请稍候..."); string filePath = openFile.FileName; // 优化:使用 Task.Run 异步执行,不阻塞UI线程 DataTable excelDt = await Task.Run(() => ExcelHelper.GetExcel(filePath)); if (excelDt == null || excelDt.Rows.Count == 0) { MessageBox.Show("Excel解析后无有效数据,不允许导入!"); LogHelper.AppendLog("Excel无有效数据,导入中止"); return; } // excel导入列 string[] requiredColumns = { "供应商代码", "供应商名称", "车型", "零件号", "零件名", "参数名" }; var missingColumns = requiredColumns.Where(col => !excelDt.Columns.Contains(col)).ToList(); if (missingColumns.Count > 0) { string msg = $"Excel缺少必要列:{string.Join("、", missingColumns)},导入失败"; MessageBox.Show(msg); LogHelper.AppendLog(msg); return; } // 异步执行批量插入 await _buttonService.SaveFormulaByExcel(excelDt); // 异步刷新配方 await InitFormulas(); MessageBox.Show("配方信息已导入!"); LogHelper.AppendLog($"配方导入成功:{Path.GetFileName(filePath)},共{excelDt.Rows.Count}行"); } catch (Exception ex) { LogHelper.AppendLog($"配方导入失败:{ex.Message}"); MessageBox.Show($"导入失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { // 恢复光标 this.Cursor = Cursors.Default; } } /// /// 上传宜搭按钮(第一份原有,仅替换配方筛选逻辑为第二份的多行筛选) /// private async void button2_Click(object sender, EventArgs e) { try { if (toolStripStatusLabel3.Text != "已连接") //如果 MQTT 没连接,就不允许上传。 { MessageBox.Show("请先连接MQTT网关!"); return; } if (this.comboBox1.Text.Length == 0 && this.comboBox2.Text.Length == 0) { //如果两个下拉框都还没数据,说明没导入过配方,直接中止。 MessageBox.Show("请先导入配方信息,再刷新!"); return; } this.button2.Enabled = false; // 设备1选择配方 string selected1 = this.comboBox1.SelectedValue?.ToString(); if (string.IsNullOrEmpty(selected1) || selected1 == "-1") { _formulasList1 = null; } else { string[] arr1 = selected1.Split('|'); if (arr1.Length == 2) { string partNumber1 = arr1[0]; string partName1 = arr1[1]; _formulasList1 = _formulas.AsEnumerable() .Where(r => r.Field("part_number") == partNumber1 && r.Field("part_name") == partName1) .ToList(); } else { _formulasList1 = null; } } // 设备2选择配方 string selected2 = this.comboBox2.SelectedValue?.ToString(); if (string.IsNullOrEmpty(selected2) || selected2 == "-1") { _formulasList2 = null; } else { string[] arr2 = selected2.Split('|'); if (arr2.Length == 2) { string partNumber2 = arr2[0]; string partName2 = arr2[1]; _formulasList2 = _formulas.AsEnumerable() .Where(r => r.Field("part_number") == partNumber2 && r.Field("part_name") == partName2) .ToList(); } else { _formulasList2 = null; } } double seconds; if (!double.TryParse(txtLUploadPL.Text, out seconds) || seconds <= 0) { seconds = 10; //控制上传间隔,默认10秒。 } // 立即上传一次 await MqttYiDaUpload(); _timer = new System.Timers.Timer(seconds * 1000); _timer.Elapsed += async (s, evt) => { await MqttYiDaUpload(); }; _timer.AutoReset = true; // 是否重复 _timer.Enabled = true; // 开始计时 LogHelper.AppendLog($"自动上传已启动,间隔:{seconds}秒"); } catch (Exception ex) { this.button2.Enabled = true; LogHelper.AppendLog($"上传宜搭失败:{ex.Message}"); } } /// /// 中止上传按钮 /// private void button3_Click(object sender, EventArgs e) { button2.Enabled = true; if (_timer != null) { _timer.Enabled = false; _timer.Dispose(); _timer = null; LogHelper.AppendLog("已停止自动上传数据"); MessageBox.Show("已停止自动上传", "信息", MessageBoxButtons.OK, MessageBoxIcon.Information); } else { LogHelper.AppendLog("无运行中的上传定时器"); MessageBox.Show("当前无自动上传任务", "信息", MessageBoxButtons.OK, MessageBoxIcon.Information); } } /// /// 报废上传凭证按钮 /// private async void button4_Click(object sender, EventArgs e) { // 传入上次的勾选状态+间隔值(核心:持久化用户选择) using (var scrapForm = new ScrapUploadForm(_lastScrapUploadSelected, _lastScrapTimedAlarmSelected, _lastScrapInterval)) { DialogResult result = scrapForm.ShowDialog(this); if (result == DialogResult.OK) { // 1. 提取用户本次选择结果 bool isScrapUpload = scrapForm.IsScrapUploadSelected; bool isScrapTimedAlarm = scrapForm.IsScrapTimedAlarmSelected; int interval = scrapForm.ScrapTimedInterval; // 2. 持久化保存用户选择(核心:下次打开弹窗保留本次状态) _lastScrapUploadSelected = isScrapUpload; _lastScrapTimedAlarmSelected = isScrapTimedAlarm; _lastScrapInterval = interval; // 3. 上传逻辑:仅当勾选「上传凭证」时,才弹出文件选择框(分离上传和报警逻辑) bool isImageUploadSuccess = false; if (isScrapUpload) { OpenFileDialog openFile = new OpenFileDialog(); if (openFile.ShowDialog() == DialogResult.OK) { string filePath = openFile.FileName; if (!string.IsNullOrEmpty(filePath)) { isImageUploadSuccess = await _uploadService.UploadScrapImageInternalAsync(filePath); LogHelper.AppendLog(isImageUploadSuccess ? "报废凭证图片已上传!" : "报废凭证图片上传失败!"); if (isImageUploadSuccess) { _buttonService.MergeAndSaveData("1"); // 上传成功:直接标记为已上传(避免后续定时报警) _isScrapImageUploaded = true; } } else { MessageBox.Show("不允许上传空文件!"); } } } // 4. 仅日志提示(报警逻辑已移到网关连接时,此处不启动定时) LogHelper.AppendLog( $"选择结果:报废凭证上传={isScrapUpload}(上传结果={isImageUploadSuccess}),定时上传报警={isScrapTimedAlarm},时间间隔={interval}分钟"); if (isScrapTimedAlarm) { MessageBox.Show("报废定时报警已启用,连接MQTT网关后将自动启动定时任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } else { MessageBox.Show("报废定时报警已禁用,连接MQTT网关后将不启动该定时任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } } } } /// /// 连杆测试上传按钮 /// private async void button5_Click(object sender, EventArgs e) { // 传入上次的勾选状态+间隔值(核心:持久化用户选择) using (var selectForm = new StrengthTestUploadForm(_lastRodUploadSelected, _lastRodTimedAlarmSelected, _lastStrengthInterval)) { DialogResult result = selectForm.ShowDialog(this); if (result == DialogResult.OK) { // 1. 提取用户本次选择结果 bool isMoldUpload = selectForm.IsMoldProductionSelected; bool isTimedAlarm = selectForm.IsTimedAlarmSelected; int interval = selectForm.TimedInterval; // 2. 持久化保存用户选择(核心:下次打开弹窗保留本次状态) _lastRodUploadSelected = isMoldUpload; _lastRodTimedAlarmSelected = isTimedAlarm; _lastStrengthInterval = interval; // 3. 上传逻辑:仅当勾选「模具上传」时,才弹出文件选择框(分离上传和报警逻辑) bool isImageUploadSuccess = false; if (isMoldUpload) { OpenFileDialog openFile = new OpenFileDialog(); if (openFile.ShowDialog() == DialogResult.OK) { string filePath = openFile.FileName; if (!string.IsNullOrEmpty(filePath)) { isImageUploadSuccess = await _uploadService.UploadRodTestImageInternalAsync(filePath); LogHelper.AppendLog(isImageUploadSuccess ? "连杆测试图片已上传!" : "连杆测试图片上传失败!"); if (isImageUploadSuccess) { _buttonService.MergeAndSaveData("2"); // 上传成功:直接标记为已上传(避免后续定时报警) _isRodImageUploaded = true; } } else { MessageBox.Show("不允许上传空文件!"); } } } // 4. 仅日志提示(报警逻辑已移到网关连接时,此处不启动定时) LogHelper.AppendLog( $"选择结果:模具投产上传={isMoldUpload}(上传结果={isImageUploadSuccess}),定时上传报警={isTimedAlarm},时间间隔={interval}分钟"); if (isTimedAlarm) { MessageBox.Show("连杆定时报警已启用,连接MQTT网关后将自动启动定时任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } else { MessageBox.Show("连杆定时报警已禁用,连接MQTT网关后将不启动该定时任务", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } } } } /// /// 配置按钮 /// private void button6_Click(object sender, EventArgs e) { try { using (var configForm = new WorkTimeConfigForm(_workStartTime, _workEndTime)) { DialogResult result = configForm.ShowDialog(this); if (result == DialogResult.OK) { _workStartTime = configForm.WorkStartTime; _workEndTime = configForm.WorkEndTime; // ========== 保留:重新初始化早晚连杆检查节点 ========== InitDailyRodCheckTimes(); // ========== 修改:如果网关已连接,重启早晚固定检查定时器 ========== if (toolStripStatusLabel3.Text == "已连接") { StartDailyRodFixedTimers(); } LogHelper.AppendLog($"工作时间配置更新:{_workStartTime:hh\\:mm} - {_workEndTime:hh\\:mm}"); MessageBox.Show("工作时间配置成功!", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information); } } } catch (Exception ex) { LogHelper.AppendLog($"工作时间配置失败:{ex.Message}"); MessageBox.Show($"配置失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// /// 关闭MainForm按钮 /// private async void MainFormClosing(object sender, FormClosingEventArgs e) { try { if (_timer != null) { _timer.Enabled = false; _timer.Dispose(); } if (_rodAlarmTimer != null) { _rodAlarmTimer.Enabled = false; _rodAlarmTimer.Dispose(); } if (_scrapAlarmTimer != null) { _scrapAlarmTimer.Enabled = false; _scrapAlarmTimer.Dispose(); } StopDailyRodFixedTimers(); await _mqttService.MqttClientStopAsync(); } catch (Exception ex) { LogHelper.AppendLog($"关闭窗口失败:{ex.Message}"); } } /// /// 刷新配方按钮 /// private async void btnRefresh_Click(object sender, EventArgs e) { try { await InitFormulas(); InitMqttDic(); MessageBox.Show("配方已刷新!", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { LogHelper.AppendLog($"刷新配方失败:{ex.Message}"); MessageBox.Show($"刷新配方失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// /// 上传间隔输入框校验 /// private void txtLUploadPL_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == (char)Keys.Back) { e.Handled = false; return; } if (char.IsDigit(e.KeyChar) || (e.KeyChar == '.' && !txtLUploadPL.Text.Contains("."))) { e.Handled = false; } else { e.Handled = true; } } /// /// 更新时间显示 /// private void timer1_Tick(object sender, EventArgs e) { label4.Text = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"); } // ======================================== 定时报警核心方法 ======================================== /// /// 新增:启动连杆+报废的一次性定时任务(仅当用户勾选了对应报警时才启动) /// private void StartOneTimeAlarmTimers() { // 1. 连杆测试一次性定时(仅当用户勾选了连杆报警时启动) if (_lastRodTimedAlarmSelected) { if (_rodAlarmTimer != null) { _rodAlarmTimer.Enabled = false; _rodAlarmTimer.Dispose(); } int rodIntervalMs = _lastStrengthInterval * 60 * 1000; // 分钟 → 毫秒 _rodAlarmTimer = new System.Timers.Timer(rodIntervalMs); _rodAlarmTimer.AutoReset = false; // 关键:一次性执行,不循环 _rodAlarmTimer.Elapsed += RodAlarmTimer_Elapsed; _rodAlarmTimer.Enabled = true; // 启动定时 LogHelper.AppendLog($"连杆测试一次性报警定时已启动:若{_lastStrengthInterval}分钟内未上传图片,将触发钉钉报警"); } else { LogHelper.AppendLog("用户未勾选连杆定时报警,跳过连杆定时任务启动"); } // 2. 报废上传一次性定时(仅当用户勾选了报废报警时启动) if (_lastScrapTimedAlarmSelected) { if (_scrapAlarmTimer != null) { _scrapAlarmTimer.Enabled = false; _scrapAlarmTimer.Dispose(); } int scrapIntervalMs = _lastScrapInterval * 60 * 1000; // 分钟 → 毫秒 _scrapAlarmTimer = new System.Timers.Timer(scrapIntervalMs); _scrapAlarmTimer.AutoReset = false; // 关键:一次性执行,不循环 _scrapAlarmTimer.Elapsed += ScrapAlarmTimer_Elapsed; _scrapAlarmTimer.Enabled = true; // 启动定时 LogHelper.AppendLog($"报废上传一次性报警定时已启动:若{_lastScrapInterval}分钟内未上传凭证,将触发钉钉报警"); } else { LogHelper.AppendLog("用户未勾选报废定时报警,跳过报废定时任务启动"); } } /// /// 终止所有报警定时任务并重置标记 /// private void StopAllAlarmTimers() { // 1. 终止连杆定时 if (_rodAlarmTimer != null) { _rodAlarmTimer.Enabled = false; _rodAlarmTimer.Dispose(); _rodAlarmTimer = null; LogHelper.AppendLog("连杆测试报警定时已终止(网关断开)"); } // 2. 终止报废定时 if (_scrapAlarmTimer != null) { _scrapAlarmTimer.Enabled = false; _scrapAlarmTimer.Dispose(); _scrapAlarmTimer = null; LogHelper.AppendLog("报废上传报警定时已终止(网关断开)"); } // 3. 重置静态上传标记 _isRodImageUploaded = false; _isScrapImageUploaded = false; } private void RodAlarmTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { DateTime now = DateTime.Now; TimeSpan currentTime = now.TimeOfDay; // 1. 工作时间判断 if (currentTime < _workStartTime || currentTime > _workEndTime) { LogHelper.AppendLog($"当前时间{currentTime:hh\\:mm}不在工作时间内,跳过本次连杆报警判断"); _rodAlarmTimer?.Dispose(); _rodAlarmTimer = null; return; } // ========== 保留:早晚节点标记判断(已上传则跳过原有报警) ========== if (_isMorningRodUploaded || _isEveningRodUploaded) { LogHelper.AppendLog("连杆图片已在早晚强制时段内上传,跳过本次原有定时报警"); _rodAlarmTimer?.Dispose(); _rodAlarmTimer = null; _isRodImageUploaded = false; return; } // 2. 使用静态标记 _isRodImageUploaded 判断是否报警 if (!_isRodImageUploaded) { string alarmContent = $"连杆测试超时未上传图片!
当前时间:{now:yyyy-MM-dd HH:mm:ss}"; this.Invoke(new Action(() => SendDingDingAlarm(alarmContent, true))); } else { LogHelper.AppendLog($"连杆测试{_lastStrengthInterval}分钟内已上传图片,无需触发报警"); } // 3. 销毁定时器,重置标记 _rodAlarmTimer?.Dispose(); _rodAlarmTimer = null; _isRodImageUploaded = false; } private void ScrapAlarmTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { DateTime now = DateTime.Now; TimeSpan currentTime = now.TimeOfDay; // 1. 工作时间判断 if (currentTime < _workStartTime || currentTime > _workEndTime) { LogHelper.AppendLog($"当前时间{currentTime:hh\\:mm}不在工作时间内,跳过本次报废报警判断"); // 销毁定时器 _scrapAlarmTimer?.Dispose(); _scrapAlarmTimer = null; return; } // 2. 使用静态标记 _isScrapImageUploaded 判断是否报警(核心:上传成功则不报警) if (!_isScrapImageUploaded) { string alarmContent = $"报废上传超时未上传凭证!
当前时间:{now:yyyy-MM-dd HH:mm:ss}"; this.Invoke(new Action(() => SendDingDingAlarm(alarmContent, false))); } else { LogHelper.AppendLog($"报废上传{_lastScrapInterval}分钟内已上传凭证,无需触发报警"); } // 3. 销毁定时器,重置标记 _scrapAlarmTimer?.Dispose(); _scrapAlarmTimer = null; _isScrapImageUploaded = false; } private async void SendDingDingAlarm(string alarmContent, bool isRodAlarm) { try { LogHelper.AppendLog($"【钉钉报警】{(isRodAlarm ? "连杆" : "报废")}:{alarmContent}"); await _uploadService.SendDingDingTextAlarmAsync(alarmContent, isRodAlarm); // MessageBox.Show($"已发送钉钉{(isRodAlarm ? "连杆" : "报废")}报警:{alarmContent}", "报警提示", MessageBoxButtons.OK, // MessageBoxIcon.Warning); } catch (Exception ex) { LogHelper.AppendLog($"{(isRodAlarm ? "连杆" : "报废")}钉钉报警发送失败:{ex.Message}"); } } // ======================================== 上传状态回调方法(连杆+报废) ======================================== /// /// 连杆上传状态回调 /// private void OnRodUploadStatusChanged(object sender, RodUploadStatusEventArgs e) { if (this.InvokeRequired) { this.Invoke(new Action(OnRodUploadStatusChanged), sender, e); return; } _isImageUploadedInCycle = e.IsUploadSuccess; // 上传成功时更新静态标记 if (e.IsUploadSuccess) { _isRodImageUploaded = true; LogHelper.AppendLog("连杆图片上传成功,标记为「已上传」,定时结束后将不触发报警"); // ========== 保留:同步更新早晚连杆上传标记(未到节点时) ========== TimeSpan currentTime = DateTime.Now.TimeOfDay; // 早上节点前上传,更新早上标记 if (currentTime < _morningCheckTime && !_isMorningRodUploaded) { _isMorningRodUploaded = true; LogHelper.AppendLog("连杆图片上传成功,同步标记:早上无需触发强制上传报警"); } // 晚上节点前上传,更新晚上标记 else if (currentTime >= _morningCheckTime && currentTime < _eveningCheckTime && !_isEveningRodUploaded) { _isEveningRodUploaded = true; LogHelper.AppendLog("连杆图片上传成功,同步标记:晚上无需触发强制上传报警"); } } LogHelper.AppendLog($"连杆上传状态:{(e.IsUploadSuccess ? "成功" : "失败")},消息:{e.Message}"); } /// /// 报废上传状态回调 /// private void OnScrapUploadStatusChanged(object sender, ScrapUploadStatusEventArgs e) { if (this.InvokeRequired) { this.Invoke(new Action(OnScrapUploadStatusChanged), sender, e); return; } _isScrapImageUploadedInCycle = e.IsUploadSuccess; // 上传成功时更新静态标记 if (e.IsUploadSuccess) { _isScrapImageUploaded = true; LogHelper.AppendLog("报废凭证上传成功,标记为「已上传」,定时结束后将不触发报警"); } LogHelper.AppendLog($"报废上传状态:{(e.IsUploadSuccess ? "成功" : "失败")},消息:{e.Message}"); } /// /// 初始化早晚连杆检查节点时间(基于配置的工作时间) /// private void InitDailyRodCheckTimes() { // 计算早上检查节点:工作开始时间 + 30分钟 _morningCheckTime = _workStartTime.Add(TimeSpan.FromMinutes(30)); // 计算晚上检查节点:工作结束时间 - 30分钟 _eveningCheckTime = _workEndTime.Subtract(TimeSpan.FromMinutes(30)); // 校验:防止晚上检查节点早于早上检查节点(避免用户配置异常) if (_eveningCheckTime <= _morningCheckTime) { LogHelper.AppendLog("警告:工作时间配置异常,结束时间前30分钟早于开始时间后30分钟,早晚连杆检查功能将失效"); } LogHelper.AppendLog($"早晚连杆检查节点已初始化:早上{_morningCheckTime:hh\\:mm},晚上{_eveningCheckTime:hh\\:mm}"); } /// /// 计算当前时间到目标检查节点的毫秒数(用于设置定时器延迟) /// /// 目标检查节点(当天的时间点) /// 延迟毫秒数(若目标时间已过,返回-1) private long CalculateDelayToTargetTime(TimeSpan targetTime) { DateTime now = DateTime.Now; // 拼接当天的目标日期时间 DateTime targetDateTime = new DateTime(now.Year, now.Month, now.Day, targetTime.Hours, targetTime.Minutes, targetTime.Seconds); // 若目标时间已过当天当前时间,返回-1(无需启动当天定时器) if (targetDateTime <= now) { return -1; } // 计算延迟毫秒数 TimeSpan delay = targetDateTime - now; return (long)delay.TotalMilliseconds; } /// /// 启动早晚连杆检查定时器(一次性,到点触发) /// private void StartDailyRodFixedTimers() { // 先停止并释放原有定时器(防止重复启动) StopDailyRodFixedTimers(); // 重置早晚标记 ResetDailyRodMarks(); // 1. 启动早上检查定时器(若目标时间未过) long morningDelayMs = CalculateDelayToTargetTime(_morningCheckTime); if (morningDelayMs > 0 && _morningCheckTime > _workStartTime) { _morningRodAlarmTimer = new System.Timers.Timer(morningDelayMs); _morningRodAlarmTimer.AutoReset = false; // 一次性执行,不循环 _morningRodAlarmTimer.Elapsed += MorningRodAlarmTimer_Elapsed; _morningRodAlarmTimer.Enabled = true; // 启动定时器 LogHelper.AppendLog( $"早上连杆检查定时器已启动:将在{_morningCheckTime:hh\\:mm}触发"); } else { LogHelper.AppendLog("早上检查节点已过当天当前时间,跳过早上连杆检查定时器启动"); _isMorningRodUploaded = true; // 标记为已检查,避免重复判断 } // 2. 启动晚上检查定时器(若目标时间未过) long eveningDelayMs = CalculateDelayToTargetTime(_eveningCheckTime); if (eveningDelayMs > 0 && _eveningCheckTime < _workEndTime) { _eveningRodAlarmTimer = new System.Timers.Timer(eveningDelayMs); _eveningRodAlarmTimer.AutoReset = false; // 一次性执行,不循环 _eveningRodAlarmTimer.Elapsed += EveningRodAlarmTimer_Elapsed; _eveningRodAlarmTimer.Enabled = true; // 启动定时器 LogHelper.AppendLog( $"晚上连杆检查定时器已启动:将在{_eveningCheckTime:hh\\:mm}触发"); } else { LogHelper.AppendLog("晚上检查节点已过当天当前时间,跳过晚上连杆检查定时器启动"); _isEveningRodUploaded = true; // 标记为已检查,避免重复判断 } } /// /// 停止早晚连杆检查定时器并重置标记 /// private void StopDailyRodFixedTimers() { // 停止早上定时器 if (_morningRodAlarmTimer != null) { _morningRodAlarmTimer.Enabled = false; _morningRodAlarmTimer.Dispose(); _morningRodAlarmTimer = null; } // 停止晚上定时器 if (_eveningRodAlarmTimer != null) { _eveningRodAlarmTimer.Enabled = false; _eveningRodAlarmTimer.Dispose(); _eveningRodAlarmTimer = null; } LogHelper.AppendLog("早晚连杆检查定时器已停止"); } /// /// 重置早晚连杆上传标记(每日启动定时器或网关断开时调用) /// private void ResetDailyRodMarks() { _isMorningRodUploaded = false; _isEveningRodUploaded = false; LogHelper.AppendLog("早晚连杆上传标记已重置:早上未上传、晚上未上传"); } /// /// 早上连杆检查定时器回调(工作开始+30分钟) /// private void MorningRodAlarmTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { // 1. 判断是否已上传,未上传则触发报警 if (!_isMorningRodUploaded) { DateTime now = DateTime.Now; string morningAlarmContent = $"【早上连杆强制上传报警】
当前时间{now:yyyy-MM-dd HH:mm:ss},
已超过工作开始后30分钟({_morningCheckTime:hh\\:mm}),
未上传连杆测试图片!"; this.Invoke(new Action(() => SendDingDingAlarm(morningAlarmContent, true))); } else { LogHelper.AppendLog($"早上{_morningCheckTime:hh\\:mm}:连杆图片已上传,无需触发强制上传报警"); } // 2. 销毁早上定时器,标记为已检查(避免重复触发) _morningRodAlarmTimer?.Dispose(); _morningRodAlarmTimer = null; _isMorningRodUploaded = true; } /// /// 晚上连杆检查定时器回调(工作结束-30分钟) /// private void EveningRodAlarmTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { // 1. 判断是否已上传,未上传则触发报警 if (!_isEveningRodUploaded) { DateTime now = DateTime.Now; string eveningAlarmContent = $"【晚上连杆强制上传报警】
当前时间{now:yyyy-MM-dd HH:mm:ss},
已超过工作结束前30分钟({_eveningCheckTime:hh\\:mm}),
未上传连杆测试图片!"; this.Invoke(new Action(() => SendDingDingAlarm(eveningAlarmContent, true))); } else { LogHelper.AppendLog($"晚上{_eveningCheckTime:hh\\:mm}:连杆图片已上传,无需触发强制上传报警"); } // 2. 销毁晚上定时器,标记为已检查(避免重复触发) _eveningRodAlarmTimer?.Dispose(); _eveningRodAlarmTimer = null; _isEveningRodUploaded = true; } // ======================================== 辅助方法 ======================================== private async void Timer_Elapsed(object sender, ElapsedEventArgs e) { // 适配移植的配方变量,刷新选中配方 string selected1 = this.comboBox1.SelectedValue?.ToString(); string selected2 = this.comboBox2.SelectedValue?.ToString(); RefreshSelectedFormula(selected1, selected2); await MqttYiDaUpload(); } /// /// 适配移植:刷新选中配方(辅助方法) /// private void RefreshSelectedFormula(string selected1, string selected2) { if (string.IsNullOrEmpty(selected1) || selected1 == "-1") { _formulasList1 = null; return; } string[] arr1 = selected1.Split('|'); if (arr1.Length == 2) { string partNumber1 = arr1[0]; string partName1 = arr1[1]; _formulasList1 = _formulas.AsEnumerable() .Where(r => r.Field("part_number") == partNumber1 && r.Field("part_name") == partName1) .ToList(); } if (string.IsNullOrEmpty(selected2) || selected2 == "-1") { _formulasList2 = null; return; } string[] arr2 = selected2.Split('|'); if (arr2.Length == 2) { string partNumber2 = arr2[0]; string partName2 = arr2[1]; _formulasList2 = _formulas.AsEnumerable() .Where(r => r.Field("part_number") == partNumber2 && r.Field("part_name") == partName2) .ToList(); } } // ======================================== 核心上传方法 ======================================== private async Task MqttYiDaUpload() { try { DataTable dtMqtt = await _buttonService.GetLatestMqttDataAsync(); if (dtMqtt == null || dtMqtt.Rows.Count == 0) { LogHelper.AppendLog("未查询到MQTT设备数据,跳过本次上传"); return; } LogHelper.AppendLog($"本次读取到{dtMqtt.Rows.Count}条MQTT设备数据"); foreach (DataRow dr in dtMqtt.Rows) { string deviceCode = dr["device_code"].ToString(); LogHelper.AppendLog($"开始处理设备[{deviceCode}]数据"); // ======================================== 使用移植的配方列表变量 ======================================== List formulaList = null; string site = null; if (string.Equals(deviceCode, "device1", StringComparison.Ordinal)) { formulaList = _formulasList1; site = "1号设备"; } else if (string.Equals(deviceCode, "device2", StringComparison.Ordinal)) { formulaList = _formulasList2; site = "2号设备"; } if (formulaList == null || formulaList.Count == 0) { LogHelper.AppendLog($"设备[{deviceCode}]未选择配方,跳过"); continue; } string strMessage = dr["receive_data"].ToString(); if (string.IsNullOrEmpty(strMessage)) { LogHelper.AppendLog($"设备[{deviceCode}]无有效receive_data,跳过"); continue; } SQLDataModel data = JsonConvert.DeserializeObject(strMessage); if (data == null || data.@params == null) { LogHelper.AppendLog($"设备[{deviceCode}]反序列化后无params数据,跳过"); continue; } Type paramsType = data.@params.GetType(); PropertyInfo[] properties = paramsType.GetProperties(BindingFlags.Public | BindingFlags.Instance); List mqttLists = new List(); List yidaLists = new List(); foreach (PropertyInfo prop in properties) { string paramCode = prop.Name; object value = prop.GetValue(data.@params); if (value == null || string.IsNullOrWhiteSpace(value.ToString())) { continue; } // ======================================== 使用移植的MQTT字典进行参数映射 ======================================== DataRow[] drs = _dtMqttDic.Select($"param_code = '" + paramCode + "'"); string paramName = paramCode; if (drs != null && drs.Length > 0) { paramName = drs[0]["param_name"].ToString(); } DataRow drMatch = formulaList.FirstOrDefault(r => r.Field("parameter_name") == paramName); if (drMatch == null) { LogHelper.AppendLog($"设备[{deviceCode}]配方中未找到参数[{paramName}],跳过"); continue; } MqttModel mqttNew = BuildMqttModel(drMatch, paramName, value, site); YiDaModel yidaNew = BuildYiDaModel(drMatch, paramName, value, site); mqttLists.Add(mqttNew); yidaLists.Add(yidaNew); } int len = Math.Min(mqttLists.Count, yidaLists.Count); if (len > 0) { List mqttLists1 = mqttLists.Take(len).ToList(); List yidaLists1 = yidaLists.Take(len).ToList(); string token = _uploadService.GetDingDingToken(); if (string.IsNullOrEmpty(token)) { LogHelper.AppendLog("获取 token 失败,请检查 AppKey/AppSecret"); this.Invoke(new Action(() => { MessageBox.Show("获取 token 失败,请检查 AppKey/AppSecret", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); })); return; } this.Invoke(new Action(() => { _uploadService.UploadDatabaseDataToYiDaWithLogging(token, yidaLists1, mqttLists, this); })); LogHelper.AppendLog($"设备[{deviceCode}]成功上传{len}条数据到宜搭"); } else { LogHelper.AppendLog($"设备[{deviceCode}]无有效参数数据可上传"); } } } catch (Exception ex) { LogHelper.AppendLog($"MqttYiDaUpload执行失败:{ex.Message}"); this.Invoke(new Action(() => { MessageBox.Show($"上传失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); })); } } private MqttModel BuildMqttModel(DataRow drMatch, string paramName, object value, string site) { MqttModel mqttNew = new MqttModel(); mqttNew.SupplierCode = drMatch["supplier_code"].ToString(); mqttNew.SupplierName = drMatch["supplier_name"].ToString(); mqttNew.VehicleModel = drMatch["vehicle_model"].ToString(); mqttNew.PartNumber = drMatch["part_number"].ToString(); mqttNew.PartName = drMatch["part_name"].ToString(); mqttNew.ParameterName = paramName; mqttNew.ParameterValue = value.ToString(); mqttNew.ToleranceLower = drMatch["tolerance_lower"].ToString(); mqttNew.ToleranceUpper = drMatch["tolerance_upper"].ToString(); mqttNew.LeaderPart = drMatch["leader_part"].ToString(); mqttNew.WorkStation = site; mqttNew.LeaderOutProtection = drMatch["leader_out_protection"].ToString(); mqttNew.IsQualification = JudgeQualification(paramName, value, drMatch); return mqttNew; } private YiDaModel BuildYiDaModel(DataRow drMatch, string paramName, object value, string site) { YiDaModel yidaNew = new YiDaModel(); yidaNew.textField_mha98neu = drMatch["supplier_code"].ToString(); yidaNew.textField_mha98nev = drMatch["supplier_name"].ToString(); yidaNew.textField_mha98new = drMatch["vehicle_model"].ToString(); yidaNew.textField_mha98nex = drMatch["part_number"].ToString(); yidaNew.textField_mha98ney = drMatch["part_name"].ToString(); yidaNew.textField_mha98nf1 = paramName; yidaNew.textField_mhx44i2i = value.ToString(); yidaNew.textField_mhx44i2j = drMatch["tolerance_lower"].ToString(); yidaNew.textField_mhx44i2k = drMatch["tolerance_upper"].ToString(); yidaNew.textField_mha98nf7 = drMatch["leader_part"].ToString(); yidaNew.textField_mha98nf0 = site; yidaNew.textField_mha98nfh = drMatch["leader_out_protection"].ToString(); yidaNew.textField_mhlvt8ht = Convert.ToInt64((DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)) .TotalMilliseconds); yidaNew.textField_mha98nf5 = JudgeQualification(paramName, value, drMatch); return yidaNew; } private string JudgeQualification(string paramName, object value, DataRow drMatch) { if (paramName == "报警信息") { return string.Equals(value?.ToString(), "无错误", StringComparison.Ordinal) ? "合格" : "不合格"; } else if (paramName == "开模总数实时" || paramName == "托模次数" || paramName == "报废图片" || paramName == "连杆测试") { return "合格"; } else { if (float.TryParse(value?.ToString(), out float paramValue) && float.TryParse(drMatch["tolerance_lower"].ToString(), out float toleranceLower) && float.TryParse(drMatch["tolerance_upper"].ToString(), out float toleranceUpper)) { return (paramValue >= toleranceLower && paramValue <= toleranceUpper) ? "合格" : "不合格"; } else { LogHelper.AppendLog( $"数值转换失败:参数名={paramName},参数值={value},下公差={drMatch["tolerance_lower"]},上公差={drMatch["tolerance_upper"]}"); return "不合格"; } } } // ======================================== 日志相关方法 ======================================== private void OnMqttMessage(string msg) { if (this.InvokeRequired) { this.Invoke(new Action(OnMqttMessage), msg); return; } LogHelper.AppendLog($"收到消息: {msg}"); } private void LogHelper_OnLogGenerated(string logContent) { if (textBoxLog.InvokeRequired) { textBoxLog.Invoke(new Action(() => { AppendLogToTextBox(logContent); })); } else { AppendLogToTextBox(logContent); } } private void AppendLogToTextBox(string logContent) { textBoxLog.Text += $"{logContent}\r\n"; textBoxLog.SelectionStart = textBoxLog.Text.Length; textBoxLog.ScrollToCaret(); } // ======================================== 未实现方法 ======================================== private void comboBox1_SelectedIndexChanged(object sender, EventArgs e) { } private void button5_Click_1(object sender, EventArgs e) { } private void txtLUploadPL_TextChanged(object sender, EventArgs e) { } private void panel1_Paint(object sender, PaintEventArgs e) { } private void panel1_Paint_1(object sender, PaintEventArgs e) { } private void label6_Click(object sender, EventArgs e) { } private void toolStripStatusLabel3_Click(object sender, EventArgs e) { } } }