using FATrace.Com; using FATrace.HKNetLib.Hardware; using FATrace.HKNetLib.Wrapper; using FATrace.Model; using FATrace.OEMApp.Services; using FATrace.OEMApp.Model; using System.Diagnostics; using System.Threading.Tasks; using LibVLCSharp.Shared; using NLog; using ReaLTaiizor.Forms; using System.Collections.Concurrent; using System.ComponentModel; using System.Threading; using TaskStatus = FATrace.Model.TaskStatus; namespace FATrace.OEMApp { public partial class MainApp : MaterialForm { /// /// 日志 /// private Logger Logger { get; set; } = LogManager.GetCurrentClassLogger(); /// /// 海康Camera 客户端 /// public HkCamera HkCameraClient { get; set; } // 历史表列头中文映射 private readonly Dictionary _historyHeaderMap = new Dictionary { { nameof(OEMRawUse.InBagCode), "内袋二维码" }, { nameof(OEMRawUse.RawName), "原料名称" }, { nameof(OEMRawUse.RawCode), "原料条码" }, { nameof(OEMRawUse.VideoFilePath), "视频路径" }, { nameof(OEMRawUse.VideoName), "视频名称" }, { nameof(OEMRawUse.User), "用户" }, { nameof(OEMRawUse.CreateTime), "创建时间" } }; private int _lvLogMaxItems = 1000; private int _lastProgressLogged = -1; /// /// 关闭确认标记:用于避免重复弹窗(如关闭过程触发多次 OnFormClosing) /// private bool _closeConfirmed; private void InitLvLog() { try { LvLog.BeginUpdate(); LvLog.Clear(); LvLog.View = View.Details; LvLog.FullRowSelect = true; LvLog.GridLines = true; LvLog.HeaderStyle = ColumnHeaderStyle.Nonclickable; LvLog.Columns.Add("时间", 150); LvLog.Columns.Add("级别", 100); LvLog.Columns.Add("消息", 800); } finally { LvLog.EndUpdate(); } } private void AppendLog(string level, string message, Color? color = null) { if (LvLog == null || LvLog.IsDisposed) return; if (InvokeRequired) { try { BeginInvoke(new Action(() => AppendLog(level, message, color))); } catch { } return; } try { var item = new ListViewItem(DateTime.Now.ToString("HH:mm:ss.fff")); item.SubItems.Add(level); item.SubItems.Add(message ?? string.Empty); if (color.HasValue) item.ForeColor = color.Value; LvLog.Items.Add(item); try { LvLog.EnsureVisible(LvLog.Items.Count - 1); } catch { } if (LvLog.Items.Count > _lvLogMaxItems) { for (int i = 0; i < 200; i++) { if (LvLog.Items.Count == 0) break; LvLog.Items.RemoveAt(0); } } } catch (Exception ex) { LogError($"AppendLog: {ex.Message}"); } } private void LogInfo(string msg) => AppendLog("INFO", msg, Color.FromArgb(33, 33, 33)); private void LogWarn(string msg) => AppendLog("WARN", msg, Color.DarkOrange); private void LogError(string msg) => AppendLog("ERROR", msg, Color.DarkRed); private void BtnNavMain_Click(object? sender, EventArgs e) { materialTabControl1.SelectedIndex = 0; UpdateNavButtonStyles(0); } private void BtnNavHistory_Click(object? sender, EventArgs e) { materialTabControl1.SelectedIndex = 1; UpdateNavButtonStyles(1); } private void BtnNavSettings_Click(object? sender, EventArgs e) { materialTabControl1.SelectedIndex = 2; UpdateNavButtonStyles(2); } private void UpdateNavButtonStyles(int selectedIndex) { var buttons = new[] { btnNavMain, btnNavHistory, btnNavSettings }; for (int i = 0; i < buttons.Length; i++) { if (i == selectedIndex) { buttons[i].BackColor = Color.FromArgb(63, 81, 181); buttons[i].ForeColor = Color.White; } else { buttons[i].BackColor = Color.FromArgb(240, 240, 240); buttons[i].ForeColor = Color.FromArgb(64, 64, 64); } } } public MainApp() { InitializeComponent(); } /// /// PLC数据服务 /// private PLCDataService PLCDataService { get; set; } //private TimeClearDataService TimeClearService { get; set; } private System.Windows.Forms.Timer _statusTimer; /// /// TouchSocket Server /// private TouchSocketServer TouchSocketServer { get; set; } /// /// 主窗体加载: /// - 初始化海康客户端与基础配置(日志目录与保存路径) /// - 初始化播放器与历史记录网格绑定 /// - 启动后台任务服务(顺序下载队列 + Jellyfin 批量监听) /// - 初始化并启动 gridRULog(DataGridView)定时刷新,实时展示下载/监听任务状态 /// private async void MainApp_Load(object sender, EventArgs e) { InitLvLog(); LogInfo("主界面初始化"); HkCameraClient = new HkCamera(); //保存SDK日志 CHCNetSDK.NET_DVR_SetLogToFile(3, "C:\\SdkLog\\", true); LogInfo("已启用海康SDK日志输出 C:\\SdkLog\\"); try { HkCameraClient.NVRLoadVideoProcessEventHandler += HkCameraClient_NVRLoadVideoProcessEventHandler; } catch (Exception ex) { LogError($"AppendLog: {ex.Message}"); } //读取配置 //HkCameraClient.NVR_IP = ConfigHelper.GetValue("NVRIP"); //HkCameraClient.NVR_Port = ConfigHelper.GetValue("NVRPort"); //HkCameraClient.NVR_UserName = ConfigHelper.GetValue("NVRUserName"); //PLC时间读取 //PLCDataService = new PLCDataService(); //PLCDataService.PlcConnectedEventHandler += PLCDataService_PlcConnectedEventHandler; //PLCDataService.ScanCodeEventHandler += PLCDataService_ScanCodeEventHandler; HkCameraClient.NVRVideoSavePath = ConfigHelper.GetValue("NVRVideoSavePath"); var TcpServerIp = ConfigHelper.GetValue("TcpServerIp"); var TcpServerPort = int.Parse(ConfigHelper.GetValue("TcpServerPort")); TouchSocketServer = new TouchSocketServer(TcpServerIp, TcpServerPort, 30000, 30000); TouchSocketServer.ClientConnected += TouchSocketServer_ClientConnected; TouchSocketServer.ClientDisconnected += TouchSocketServer_ClientDisconnected; TouchSocketServer.DataReceived += TouchSocketServer_DataReceived; TouchSocketServer.ServerError += TouchSocketServer_ServerError; var serverStarted = await TouchSocketServer.StartAsync(); if (serverStarted) { LogInfo($"TouchSocket Server 已启动,监听 {TcpServerIp}:{TcpServerPort}"); } else { LogError($"TouchSocket Server 启动失败,监听 {TcpServerIp}:{TcpServerPort}"); } //NVR登录 NVRLogin(); InitHistoryGridBinding(); // 启动后台任务服务:下载队列(单线程顺序)与 Jellyfin 批量监控(并行匹配) try { DownloadTaskWorker.Instance.Start(HkCameraClient); LogInfo("下载队列服务已启动"); try { DownloadTaskWorker.Instance.DownloadFileNameChanged += OnDownloadFileNameChanged; } catch { } try { DownloadTaskWorker.Instance.TaskStarted += OnTaskStatusChanged; } catch { } try { DownloadTaskWorker.Instance.TaskCompleted += OnTaskStatusChanged; } catch { } try { DownloadTaskWorker.Instance.TaskFailed += OnTaskFailed; } catch { } // JellyfinMonitorQueueService.Instance.Start(); // LogInfo("Jellyfin 监控服务已启动"); //LogInfo("Jellyfin 监控服务已停用"); //TimeClearService = new TimeClearDataService(); //TimeClearService.Info += (m) => LogInfo($"[清理]{m}"); //TimeClearService.Start(); //LogInfo("定时清理服务已启动"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[TaskServices] 启动失败: {ex.Message}"); LogError($"后台服务启动失败: {ex.Message}"); } // 初始化 gridRULog 并启动 UI 定时刷新 InitRuLogGrid(); // 仅初始化一次,当天数据 RefreshRuLogGrid(); // 初始化底部连接状态(立即检测一次 + 定时轻量检测) //SafeSetStatus(tslPlcConnection, PLCDataService?.PlcConnected == true, "Plc连接状态"); _ = UpdateDbStatusAsync(); _ = UpdateNasStatusAsync(); _ = UpdateNvrStatusAsync(); StartStatusTimer(); try { var systemName = Program.SystemName; var authText = Program.IsActive ? "(添加剂追溯系统)" : "(OEM)"; this.Text = string.IsNullOrWhiteSpace(systemName) ? $"添加剂追溯系统 {authText}" : $"{systemName} {authText}"; } catch (Exception) { // 安静失败,不影响主界面显示 } try { } catch (Exception ex) { MessageBox.Show($"初始化界面失败: {ex.Message}"); LogError($"初始化界面失败: {ex.Message}"); } } /// /// PLC数据服务:扫描条码 /// /// /// private void PLCDataService_ScanCodeEventHandler(object? sender, string Code) { //解析Code条码数据,内包条码数据 CurParsedCodeInfo = NVRCom.ParseCodeFull(Code); BeginInvoke(new Action(() => { txtRUInBagCode.Text = CurParsedCodeInfo.Code; txtRURawName.Text = CurParsedCodeInfo.RawName; txtRURawCode.Text = CurParsedCodeInfo.RawCode; })); var taskId = DownloadTaskWorker.Instance.Enqueue( CurParsedCodeInfo, user: CurUserName, start: DateTime.Now.AddDays(-2), end: DateTime.Now.AddDays(-2).AddSeconds(DownloadTaskWorker.VideoTime + 2) ); //MessageBox.Show($"[Test] 已入队下载任务,Id={taskId}"); LogInfo($"扫码: {Code}"); } /// /// PLC数据服务:PLC连接 /// /// /// private void PLCDataService_PlcConnectedEventHandler(object? sender, string e) { LogInfo($"PLC连接: {e}"); var ok = string.Equals(e, "OK", StringComparison.OrdinalIgnoreCase); SafeSetStatus(tslPlcConnection, ok, "Plc连接状态"); } private void NVRLogin() { HkCameraClient.NVR_IP = ConfigHelper.GetValue("NVRIP"); HkCameraClient.NVR_Port = ushort.Parse(ConfigHelper.GetValue("NVRPort")); HkCameraClient.NVR_UserName = ConfigHelper.GetValue("NVRUserName"); HkCameraClient.NVR_Pw = ConfigHelper.GetValue("NVRPw"); LogInfo($"尝试登录NVR {HkCameraClient.NVR_IP}:{HkCameraClient.NVR_Port} 用户 {HkCameraClient.NVR_UserName}"); var result = HkCameraClient.Sdk_NET_DVR_Login_V30(HkCameraClient.NVR_IP, HkCameraClient.NVR_Port, HkCameraClient.NVR_UserName, HkCameraClient.NVR_Pw); if (result) { LogInfo("NVR 登录成功"); _ = UpdateNvrStatusAsync(); return; } //MessageBox.Show($"登录失败:{HkCameraClient.LastMsgErr}"); LogError($"NVR 登录失败: {HkCameraClient.LastMsgErr}"); SafeSetStatus(tslNVRConnection, false, "NVR连接状态"); return; } /// /// 当前登录用户 /// public string CurUserName { get; set; } = "Admin"; #region 视频处理变量 /// /// 当前播放的视频 /// public string CurrentVideoPath { get; set; } /// /// 按文件名/路径去重,避免同一下载完成事件被重复处理 /// key: 文件名或路径(小写) value: 对应插入的 OEMRawUse.Id(0 表示尚未完成插入) /// private readonly ConcurrentDictionary _downloadProcessingKeys = new(); #endregion #region 内包信息 ///// ///// 内包条码 ///// //public string CurInBagCode { get; set; } = "AAAAAA"; ///// ///// 内包条码 原料名称 ///// //public string CurRawName { get; set; } = "添加剂Test"; ///// ///// 内包条码 原料代码 ///// //public string CurRawCode { get; set; } = "ASASASS"; /// /// 内包条码 解析结果 /// public ParsedCodeInfo CurParsedCodeInfo { get; set; } #endregion private void btnNVRLogin_Click(object sender, EventArgs e) { HkCameraClient.NVR_IP = ConfigHelper.GetValue("NVRIP"); HkCameraClient.NVR_Port = ushort.Parse(ConfigHelper.GetValue("NVRPort")); HkCameraClient.NVR_UserName = ConfigHelper.GetValue("NVRUserName"); HkCameraClient.NVR_Pw = ConfigHelper.GetValue("NVRPw"); LogInfo($"尝试登录NVR {HkCameraClient.NVR_IP}:{HkCameraClient.NVR_Port} 用户 {HkCameraClient.NVR_UserName}"); var result = HkCameraClient.Sdk_NET_DVR_Login_V30(HkCameraClient.NVR_IP, HkCameraClient.NVR_Port, HkCameraClient.NVR_UserName, HkCameraClient.NVR_Pw); if (result) { LogInfo("NVR 登录成功"); return; } LogError($"NVR 登录失败: {HkCameraClient.LastMsgErr}"); return; } private void HkCameraClient_NVRLoadVideoProcessEventHandler(object? sender, short value) { this.BeginInvoke(new Action(() => { DownloadProgressBarMain.Value = value; })); if (value >= 100 && _lastProgressLogged != 100) { _lastProgressLogged = 100; LogInfo($"{DownloadTaskWorker.Instance.CurDownloadTask.Code} - 下载进度 100%"); } } private void btnDownloadName_Click(object sender, EventArgs e) { //if (m_lDownHandle >= 0) //{ // MessageBox.Show("Downloading, please stop firstly!");//正在下载,请先停止下载 // return; //} //string sVideoFileName; //录像文件保存路径和文件名 the path and file name to save //sVideoFileName = "D:\\Downtest1111_" + sPlayBackFileName + ".mp4"; ////按文件名下载 Download by file name //m_lDownHandle = CHCNetSDK.NET_DVR_GetFileByName(m_lUserID, sPlayBackFileName, sVideoFileName); //if (m_lDownHandle < 0) //{ // iLastErr = CHCNetSDK.NET_DVR_GetLastError(); // str = "NET_DVR_GetFileByName failed, error code= " + iLastErr; // MessageBox.Show(str); // return; //} //uint iOutValue = 0; ////设置转封装格式 ////UInt32 iInValue = 5; ////IntPtr lpInValue = Marshal.AllocHGlobal(4); ////Marshal.StructureToPtr(iInValue, lpInValue, false); ////if (!CHCNetSDK.NET_DVR_PlayBackControl_V40(m_lDownHandle, CHCNetSDK.NET_DVR_SET_TRANS_TYPE, lpInValue, 4, IntPtr.Zero, ref iOutValue)) ////{ //// iLastErr = CHCNetSDK.NET_DVR_GetLastError(); //// str = "NET_DVR_PLAYSTART failed, error code= " + iLastErr; //下载控制失败,输出错误号 //// MessageBox.Show(str); //// return; ////} //if (!CHCNetSDK.NET_DVR_PlayBackControl_V40(m_lDownHandle, CHCNetSDK.NET_DVR_PLAYSTART, IntPtr.Zero, 0, IntPtr.Zero, ref iOutValue)) //{ // iLastErr = CHCNetSDK.NET_DVR_GetLastError(); // str = "NET_DVR_PLAYSTART failed, error code= " + iLastErr; //下载控制失败,输出错误号 // MessageBox.Show(str); // return; //} //timerDownload.Interval = 1000; //timerDownload.Enabled = true; //btnStopDownload.Enabled = true; } private void btnStopLoadVideo_Click(object sender, EventArgs e) { var Result = HkCameraClient.Sdk_NET_DVR_StopGetFile(); if (Result.Result) { MessageBox.Show($"[暂停成功] :{Result.Msg}"); LogInfo($"下载暂停: {Result.Msg}"); } else { MessageBox.Show($"[暂停失败] :{Result.Msg}"); LogWarn($"下载暂停失败: {Result.Msg}"); } } #region 视频播放 private static async Task FileExistsWithTimeoutAsync(string path, int timeoutMs, CancellationToken cancellationToken) { var task = Task.Run(() => File.Exists(path), cancellationToken); var done = await Task.WhenAny(task, Task.Delay(timeoutMs, cancellationToken)); if (done != task) return false; return await task; } private void OpenVideoWithDefaultPlayer(string path) { if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("视频路径为空", nameof(path)); } try { var psi = new ProcessStartInfo { FileName = path, UseShellExecute = true }; Process.Start(psi); LogInfo($"已使用系统播放器打开: {path}"); } catch (Exception ex) { LogError($"系统播放器打开失败: {ex.Message} | {path}"); throw; } } #endregion #region 历史数据查询 /// /// 采用 BindingList + BindingSource,使数据变化自动触发 UI 刷新 /// private BindingSource historyBindingSource { get; set; } private BindingList historyVideoBindingList { get; set; } /// /// 初始化历史记录表格的数据绑定 /// private void InitHistoryGridBinding() { historyBindingSource = new BindingSource(); historyVideoBindingList = new BindingList(); historyBindingSource.DataSource = historyVideoBindingList; // 绑定到 WinForms 原生 DataGridView dataGridView1.AutoGenerateColumns = true; dataGridView1.DataSource = historyBindingSource; // 绑定完成后统一调整列头与可见性 dataGridView1.DataBindingComplete -= DataGridView1_DataBindingComplete; dataGridView1.DataBindingComplete += DataGridView1_DataBindingComplete; // 双击行播放对应视频 dataGridView1.CellDoubleClick -= DataGridView1_CellDoubleClick; dataGridView1.CellDoubleClick += DataGridView1_CellDoubleClick; // 若此时已存在列,立即调整一次 ConfigureHistoryGridColumns(); } private void btnHistoryVideoSearch_Click(object sender, EventArgs e) { var query = FSqlContext.FDb.Select(); if (!string.IsNullOrEmpty(txtSearchCode.Text.Trim())) { var kw = txtSearchCode.Text.Trim(); query = query.Where(a => (a.InBagCode != null && a.InBagCode.Contains(kw)) || (a.RawCode != null && a.RawCode.Contains(kw)) || (a.RawName != null && a.RawName.Contains(kw))); } // 日期范围:起始日00:00:00(含)到结束日次日00:00:00(不含),覆盖整天 var startDate = PdtHistorySearchStart.Value.Date; var endDate = PdtHistorySearchEnd.Value.Date; if (endDate < startDate) endDate = startDate; var endExclusive = endDate.AddDays(1); query = query.Where(a => a.CreateTime >= startDate && a.CreateTime < endExclusive); // 拉取结果并刷新绑定列表 var resultList = query.ToList(); // 暂停变更通知,批量更新提高效率 historyVideoBindingList.RaiseListChangedEvents = false; historyVideoBindingList.Clear(); foreach (var item in resultList) { historyVideoBindingList.Add(item); } historyVideoBindingList.RaiseListChangedEvents = true; // 通知 UI 刷新 historyVideoBindingList.ResetBindings(); } // 与历史记录表格列配置相关的成员(WinForms DataGridView) private void DataGridView1_DataBindingComplete(object? sender, DataGridViewBindingCompleteEventArgs e) { try { ConfigureHistoryGridColumns(); } catch (Exception ex) { // 仅记录,不中断界面 System.Diagnostics.Debug.WriteLine($"[DataBindingComplete] 列配置失败: {ex.Message}"); } } /// /// 双击行,播放该行对应的视频文件 /// private async void DataGridView1_CellDoubleClick(object? sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0) return; // 双击列头等 try { var row = dataGridView1.Rows[e.RowIndex]; if (row?.DataBoundItem is OEMRawUse item) { var path = item.VideoFilePath; if (string.IsNullOrWhiteSpace(path)) { MessageBox.Show("该记录没有可播放的视频路径。", "播放提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var exists = await FileExistsWithTimeoutAsync(path, timeoutMs: 4000, cts.Token); if (!exists) { MessageBox.Show($"未找到视频文件或访问超时(网络盘可能较慢):\n{path}", "播放失败", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } OpenVideoWithDefaultPlayer(path); } } catch (Exception ex) { MessageBox.Show($"播放失败: {ex.Message}", "播放失败", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// /// 按照 _historyHeaderMap 配置列头、隐藏不需要的列,并做常用格式化 /// private void ConfigureHistoryGridColumns() { if (dataGridView1 == null || dataGridView1.Columns.Count == 0) return; var allowedProps = new HashSet { nameof(OEMRawUse.InBagCode), nameof(OEMRawUse.RawName), nameof(OEMRawUse.RawCode), nameof(OEMRawUse.VideoFilePath), nameof(OEMRawUse.VideoName), nameof(OEMRawUse.User), nameof(OEMRawUse.CreateTime) }; foreach (DataGridViewColumn col in dataGridView1.Columns) { var propName = col.DataPropertyName; // 设置列头中文 if (!string.IsNullOrWhiteSpace(propName) && _historyHeaderMap.TryGetValue(propName, out var headerText)) { col.HeaderText = headerText; } switch (col.Name) { case "InBagCode": col.Width = 400; break; case "RawName": col.Width = 200; break; case "RawCode": col.Width = 200; break; case "VideoFilePath": col.Width = 490; break; case "VideoName": col.Width = 450; break; case "User": col.Width = 100; break; case "CreateTime": col.Width = 200; break; default: break; } // 隐藏 Id 列 if (propName == nameof(OEMRawUse.Id)) { col.Visible = false; continue; } // 隐藏视频路径列(仍保留数据用于双击播放) if (propName == nameof(OEMRawUse.VideoFilePath)) { col.Visible = false; continue; } // 仅显示指定字段,其他隐藏 if (!allowedProps.Contains(propName)) { col.Visible = false; continue; } //// 常用列宽与格式 //if (propName is nameof(VideoAction.Code)) //{ // col.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells; //} //else if (propName is nameof(VideoAction.VideoFilePath)) //{ // col.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill; //} // 时间列格式化 if (propName is nameof(OEMRawUse.CreateTime)) { col.DefaultCellStyle.Format = "yyyy-MM-dd HH:mm:ss"; col.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells; } } // 选中整行,易于操作 dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect; dataGridView1.MultiSelect = false; dataGridView1.ReadOnly = true; dataGridView1.AllowUserToAddRows = false; } #endregion /// /// 测试/结束操作按钮:同样仅入队一条下载任务,方便快速验证下载->监听全流程。 /// /// /// private void btnTestAction_Click(object sender, EventArgs e) { if (CurParsedCodeInfo == null) { CurParsedCodeInfo = new ParsedCodeInfo() { Batch = "260131", Code = "DYG05030013,250710,8228,12,01,7", Count = 1, RawCode = "DYG05030013", RawName = "", RegionCode = "2", RegionName = "1", ShelfLifeMonths = 12, Weight = (decimal)80.25, }; //return; } var taskId = DownloadTaskWorker.Instance.Enqueue( CurParsedCodeInfo, user: CurUserName, start: DateTime.Now, end: DateTime.Now.AddSeconds(DownloadTaskWorker.VideoTime) ); //MessageBox.Show($"[Test] 已入队下载任务,Id={taskId}"); } /// /// 下载完成事件:触发后入库并启动 Jellyfin 轮询匹配 /// /// /// private void HkCameraClient_NVRLoadVideoCompleteEventHandler(object? sender, string e) { // 计算用于去重的 key(文件路径或文件名,小写) if (string.IsNullOrWhiteSpace(e) && string.IsNullOrWhiteSpace(this.CurrentVideoPath)) { LogWarn("下载完成事件未提供文件名,忽略"); return; } var localNameOrPath = (!string.IsNullOrWhiteSpace(e) && (e.Contains("\\") || e.Contains("/") || e.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase))) ? e : this.CurrentVideoPath; var safePath = localNameOrPath ?? string.Empty; var key = (System.IO.Path.GetFileName(safePath) ?? safePath).ToLowerInvariant(); // 若同一 key 已在处理中,则忽略本次回调 if (!_downloadProcessingKeys.TryAdd(key, 0)) { System.Diagnostics.Debug.WriteLine($"[NVRLoadVideoComplete] 正在处理相同文件,忽略: {key}"); return; } LogInfo($"下载完成: {localNameOrPath}"); // Jellyfin 监控已停用:仅移除去重 key _downloadProcessingKeys.TryRemove(key, out _); } #region 任务监控(gridRULog / DataGridView) private BindingSource ruLogBindingSource; private BindingList ruLogBindingList; /// /// DataGridView 行模型 /// private sealed class RuLogRow { public string TimeText { get; set; } = string.Empty; public string Type { get; set; } = string.Empty; // 下载/监听 public string Code { get; set; } = string.Empty; public long TaskId { get; set; } public string StatusText { get; set; } = string.Empty; public string? ProgressText { get; set; } public string Remark { get; set; } = string.Empty; // 错误优先,否则文件路径/ItemId } /// /// 初始化 gridRULog:列定义/数据绑定/低闪烁 /// private void InitRuLogGrid() { ruLogBindingSource = new BindingSource(); ruLogBindingList = new BindingList(); ruLogBindingSource.DataSource = ruLogBindingList; gridRULog.AutoGenerateColumns = false; gridRULog.Columns.Clear(); // 列:时间、类型、任务Id、状态、进度、备注(填充) gridRULog.Columns.Add(new DataGridViewTextBoxColumn { Name = "colTime", HeaderText = "时间", DataPropertyName = nameof(RuLogRow.TimeText), Width = 100, AutoSizeMode = DataGridViewAutoSizeColumnMode.None }); gridRULog.Columns.Add(new DataGridViewTextBoxColumn { Name = "colType", HeaderText = "类型", DataPropertyName = nameof(RuLogRow.Type), Width = 100, AutoSizeMode = DataGridViewAutoSizeColumnMode.None }); gridRULog.Columns.Add(new DataGridViewTextBoxColumn { Name = "colCode", HeaderText = "条码", DataPropertyName = nameof(RuLogRow.Code), Width = 400, AutoSizeMode = DataGridViewAutoSizeColumnMode.None }); gridRULog.Columns.Add(new DataGridViewTextBoxColumn { Name = "colTaskId", HeaderText = "任务Id", DataPropertyName = nameof(RuLogRow.TaskId), Width = 120, AutoSizeMode = DataGridViewAutoSizeColumnMode.None }); gridRULog.Columns.Add(new DataGridViewTextBoxColumn { Name = "colStatus", HeaderText = "状态", DataPropertyName = nameof(RuLogRow.StatusText), Width = 120, AutoSizeMode = DataGridViewAutoSizeColumnMode.None }); gridRULog.Columns.Add(new DataGridViewTextBoxColumn { Name = "colProgress", HeaderText = "进度", DataPropertyName = nameof(RuLogRow.ProgressText), Width = 80, AutoSizeMode = DataGridViewAutoSizeColumnMode.None }); gridRULog.Columns.Add(new DataGridViewTextBoxColumn { Name = "colRemark", HeaderText = "备注", DataPropertyName = nameof(RuLogRow.Remark), AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill }); // 双缓存减少闪烁 SetDataGridViewDoubleBuffered(gridRULog); gridRULog.DataSource = ruLogBindingSource; gridRULog.ColumnHeadersVisible = true; gridRULog.EnableHeadersVisualStyles = false; gridRULog.AllowUserToResizeRows = false; gridRULog.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.DisplayedCells; gridRULog.BackgroundColor = Color.White; gridRULog.BorderStyle = BorderStyle.FixedSingle; gridRULog.ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single; gridRULog.ColumnHeadersDefaultCellStyle.BackColor = Color.Gainsboro; gridRULog.ColumnHeadersDefaultCellStyle.ForeColor = Color.Black; gridRULog.RowHeadersVisible = false; // 使用 Designer 中的 Location/Size 与 Anchor,不额外 Dock // 跟随父容器尺寸变化进行定位与大小调整 materialCard2.Resize -= MaterialCard2_Resize; materialCard2.Resize += MaterialCard2_Resize; LayoutRuLogGrid(); } /// /// 根据左侧控件宽度动态计算 gridRULog 的 Left/Size,确保位置与尺寸正确 /// private void LayoutRuLogGrid() { if (gridRULog == null || materialCard2 == null) return; try { int leftReserved = 540; // 默认左侧操作区宽度 try { var candidates = new Control[] { txtRUInBagCode, txtRURawName, btnTestAction, btnRawStopLoadVideo, DownloadProgressBarMain }; foreach (var c in candidates) { if (c?.Parent == materialCard2) { leftReserved = Math.Max(leftReserved, c.Right); } } leftReserved += 20; // 预留间距 } catch { } var client = materialCard2.ClientSize; var left = leftReserved; var top = 9; var width = Math.Max(200, client.Width - left - 14); var height = Math.Max(120, client.Height - top - 9); gridRULog.Location = new Point(left, top); gridRULog.Size = new Size(width, height); } catch { } } private void MaterialCard2_Resize(object? sender, EventArgs e) { LayoutRuLogGrid(); } // 定时刷新已移除:仅在下载开始/完成/失败时按需更新 /// /// 刷新 gridRULog(仅当天下载) /// private void RefreshRuLogGrid() { try { var db = FSqlContext.FDb; var today = DateTime.Today; var downloads = db.Select() .Where(a => a.UpdateTime >= today) .OrderByDescending(a => a.UpdateTime) .ToList(); var rows = new List(downloads.Count); // 运行中优先 foreach (var t in downloads.Where(x => x.Status == TaskStatus.Running)) { rows.Add(new RuLogRow { TimeText = t.UpdateTime.ToString("HH:mm:ss"), Type = "下载", Code = t.Code ?? string.Empty, TaskId = t.Id, StatusText = t.Status.ToString(), ProgressText = t.Progress.ToString(), Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.VideoFilePath ?? string.Empty) : t.Error! }); } // 监听任务展示已移除 // 其余 foreach (var t in downloads.Where(x => x.Status != TaskStatus.Running)) { rows.Add(new RuLogRow { TimeText = t.UpdateTime.ToString("HH:mm:ss"), Type = "下载", Code = t.Code ?? string.Empty, TaskId = t.Id, StatusText = t.Status.ToString(), ProgressText = t.Progress.ToString(), Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.VideoFilePath ?? string.Empty) : t.Error! }); } // 监听任务展示已移除 // 批量更新绑定列表 ruLogBindingList.RaiseListChangedEvents = false; ruLogBindingList.Clear(); foreach (var r in rows) { ruLogBindingList.Add(r); } ruLogBindingList.RaiseListChangedEvents = true; ruLogBindingList.ResetBindings(); } catch (Exception ex) { LogError($"[gridRULog] 刷新失败: {ex.Message}"); System.Diagnostics.Debug.WriteLine($"[gridRULog] 刷新失败: {ex.Message}"); } } private void OnTaskStatusChanged(DownloadTask t) { try { if (t.UpdateTime.Date != DateTime.Today) return; if (InvokeRequired) { BeginInvoke(new Action(() => UpsertRuLogRowFromTask(t))); } else { UpsertRuLogRowFromTask(t); } if (t.Status == TaskStatus.Completed || t.Status == TaskStatus.Failed) { ResetProgressBar(); } if (t.Status == TaskStatus.Completed) { _ = SaveCsvForTaskAsync(t); } } catch (Exception ex) { LogError($"[OnTaskStatusChanged] 失败: {ex.Message}"); } } private void OnTaskFailed(DownloadTask t, string? error) { try { if (t.UpdateTime.Date != DateTime.Today) return; // 仅当天 if (InvokeRequired) { BeginInvoke(new Action(() => UpsertRuLogRowFromTask(t))); } else { UpsertRuLogRowFromTask(t); } // 失败同样清零进度 ResetProgressBar(); } catch (Exception ex) { LogError($"[OnTaskFailed] 失败: {ex.Message}"); } } private void UpsertRuLogRowFromTask(DownloadTask t) { if (ruLogBindingList == null) return; var existing = ruLogBindingList.FirstOrDefault(x => x.TaskId == t.Id); if (existing == null) { ruLogBindingList.Insert(0, new RuLogRow { TimeText = t.UpdateTime.ToString("HH:mm:ss"), Type = "下载", Code = t.Code ?? string.Empty, TaskId = t.Id, StatusText = t.Status.ToString(), ProgressText = t.Progress.ToString(), Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.VideoFilePath ?? string.Empty) : t.Error! }); } else { existing.TimeText = t.UpdateTime.ToString("HH:mm:ss"); existing.StatusText = t.Status.ToString(); existing.ProgressText = t.Progress.ToString(); existing.Code = t.Code ?? string.Empty; existing.Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.VideoFilePath ?? string.Empty) : t.Error!; } ruLogBindingList.ResetBindings(); } private void ResetProgressBar() { try { if (DownloadProgressBarMain == null || DownloadProgressBarMain.IsDisposed) return; if (InvokeRequired) { BeginInvoke(new Action(() => DownloadProgressBarMain.Value = 0)); } else { BeginInvoke(new Action(() => DownloadProgressBarMain.Value = 0)); } _lastProgressLogged = -1; } catch { } } private async Task SaveCsvForTaskAsync(DownloadTask t) { try { var dto = new RawUseCsvDto { RawCode = t.RawCode, RawName = t.RawName, InBagCode = t.Code, OpUser = t.User, VideoSavePath = t.VideoFilePath, UseTime = t.UpdateTime }; var svc = new CsvService(); var path = await Task.Run(() => svc.ExportSingle(dto)); SafeSetCsvState($"CSV保存成功: {path}", true); } catch (Exception ex) { SafeSetCsvState($"CSV保存失败: {ex.Message}", false); } } private void SafeSetCsvState(string text, bool ok) { try { if (txtCsvSaveState == null || txtCsvSaveState.IsDisposed) return; if (InvokeRequired) { BeginInvoke(new Action(() => { txtCsvSaveState.Text = text ?? string.Empty; txtCsvSaveState.ForeColor = ok ? Color.ForestGreen : Color.DarkRed; })); } else { txtCsvSaveState.Text = text ?? string.Empty; txtCsvSaveState.ForeColor = ok ? Color.ForestGreen : Color.DarkRed; } } catch (Exception ex) { LogError($"[SafeSetCsvState] 失败: {ex.Message}"); } } private static void SetDataGridViewDoubleBuffered(DataGridView dgv) { try { typeof(DataGridView).InvokeMember( "DoubleBuffered", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.SetProperty, null, dgv, new object[] { true } ); } catch { } } #endregion private void btnRawStopLoadVideo_Click(object sender, EventArgs e) { HkCameraClient.Sdk_NET_DVR_StopGetFile(); } /// /// 当前下载文件的名称改变 /// /// private void OnDownloadFileNameChanged(string name) { try { if (DownloadFileName == null || DownloadFileName.IsDisposed) return; if (InvokeRequired) { try { BeginInvoke(new Action(() => DownloadFileName.Text = name ?? string.Empty)); } catch { } } else { try { BeginInvoke(new Action(() => DownloadFileName.Text = name ?? string.Empty)); } catch { } } } catch (Exception ex) { LogError($"OnDownloadFileNameChanged: {ex.Message}"); } } private void TouchSocketServer_ClientConnected(object? sender, TouchSocketServer.ClientConnectedEventArgs e) { LogInfo($"[TCP] 客户端连接: {e.ClientId} {e.RemoteIp}:{e.RemotePort}"); } private void TouchSocketServer_ClientDisconnected(object? sender, TouchSocketServer.ClientDisconnectedEventArgs e) { LogInfo($"[TCP] 客户端断开: {e.ClientId} {e.RemoteIp}:{e.RemotePort} 原因={e.Reason}"); } private void TouchSocketServer_DataReceived(object? sender, TouchSocketServer.DataReceivedEventArgs e) { Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} : - IP : {e.RemoteIp} {e.Data}"); // 解析二维码为 QRModel try { if (!string.IsNullOrWhiteSpace(e.Data)) { //解析Code条码数据,内包条码数据 CurParsedCodeInfo = NVRCom.ParseCodeFull(e.Data!.Trim()); BeginInvoke(new Action(() => { txtRUInBagCode.Text = CurParsedCodeInfo.Code; txtRURawCode.Text = CurParsedCodeInfo.RawCode; })); try { if (CurParsedCodeInfo == null) return; var taskId = DownloadTaskWorker.Instance.Enqueue( CurParsedCodeInfo, user: CurUserName, start: DateTime.Now, end: DateTime.Now ); } catch (Exception ex) { LogError($"TouchSocketServer_DataReceived: {ex.Message}"); return; } } } catch (Exception ex) { // 解析失败仅记录错误,不影响后续逻辑 LogError($"[TCP] QR 解析失败: {ex.Message}"); } TouchSocketServer.SendToClient(e.ClientId, "OK"); } private void TouchSocketServer_ServerError(object? sender, Exception ex) { LogError($"[TCP] 服务器错误: {ex.Message}"); } #region 底部状态栏(PLC/DB/NAS/NVR) private void StartStatusTimer() { try { _statusTimer = new System.Windows.Forms.Timer(); _statusTimer.Interval = 30000; // 30s 轻量检测 _statusTimer.Tick += async (s, e) => { await UpdateDbStatusAsync(); await UpdateNasStatusAsync(); await UpdateNvrStatusAsync(); }; _statusTimer.Start(); } catch (Exception ex) { LogError($"StartStatusTimer: {ex.Message}"); } } private void SafeSetStatus(ToolStripStatusLabel lbl, bool ok, string title) { try { string text = ok ? $"{title}: 正常" : $"{title}: 异常"; var fore = ok ? Color.ForestGreen : Color.DarkRed; if (InvokeRequired) { BeginInvoke(new Action(() => { lbl.Text = text; lbl.ForeColor = fore; })); } else { lbl.Text = text; lbl.ForeColor = fore; } } catch (Exception ex) { LogError($"SafeSetStatus: {ex.Message}"); } } private async Task UpdateDbStatusAsync() { bool ok = false; try { var db = FSqlContext.FDb; var ret = await db.Ado.ExecuteScalarAsync("SELECT 1"); ok = Convert.ToString(ret) == "1"; } catch { ok = false; } SafeSetStatus(tslSqlConnection, ok, "服务连接状态"); } private async Task UpdateNasStatusAsync() { bool ok = false; try { var path = HkCameraClient?.NVRVideoSavePath; if (!string.IsNullOrWhiteSpace(path)) { ok = await Task.Run(() => System.IO.Directory.Exists(path)); } } catch { ok = false; } SafeSetStatus(tslNasConnection, ok, "NAS连接状态"); } private async Task UpdateNvrStatusAsync() { bool ok = false; try { ok = HkCameraClient != null && HkCameraClient.NVRLoginState && HkCameraClient.IsOnline(); // NVR未登录时自动尝试重连 if (!ok && HkCameraClient != null && !HkCameraClient.NVRLoginState) { LogInfo("NVR未连接,尝试自动重连..."); var result = HkCameraClient.Sdk_NET_DVR_Login_V30( HkCameraClient.NVR_IP, HkCameraClient.NVR_Port, HkCameraClient.NVR_UserName, HkCameraClient.NVR_Pw); if (result) { ok = true; LogInfo("NVR 自动重连成功"); } else { LogError($"NVR 自动重连失败: {HkCameraClient.LastMsgErr}"); } } } catch { ok = false; } await Task.Yield(); SafeSetStatus(tslNVRConnection, ok, "NVR连接状态"); } #endregion private void MainApp_FormClosing(object sender, FormClosingEventArgs e) { if (!_closeConfirmed && e.CloseReason == CloseReason.UserClosing) { try { var result = MessageBox.Show( "确认退出系统?", "退出确认", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2); if (result != DialogResult.Yes) { e.Cancel = true; LogInfo("已取消关闭"); return; } _closeConfirmed = true; LogInfo("用户确认关闭,开始退出"); } catch (Exception ex) { // 弹窗异常时,为保证可关闭,默认继续退出,但记录日志 LogError($"关闭确认弹窗异常: {ex.Message}"); _closeConfirmed = true; } } try { DownloadTaskWorker.Instance.DownloadFileNameChanged -= OnDownloadFileNameChanged; } catch { } try { DownloadTaskWorker.Instance.TaskStarted -= OnTaskStatusChanged; } catch { } try { DownloadTaskWorker.Instance.TaskCompleted -= OnTaskStatusChanged; } catch { } try { DownloadTaskWorker.Instance.TaskFailed -= OnTaskFailed; } catch { } try { _statusTimer?.Stop(); _statusTimer?.Dispose(); _statusTimer = null; } catch { } //try { TimeClearService?.Stop(); } catch { } try { if (TouchSocketServer != null && TouchSocketServer.IsRunning) { //await TouchSocketServer.cl(); LogInfo("TouchSocket Server 已停止"); } } catch (Exception ex) { LogError($"TouchSocket Server 停止失败: {ex.Message}"); } } } }