Files
FATrace/FATrace.OEMApp/MainApp.cs
2026-03-05 10:20:43 +08:00

1398 lines
52 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 日志
/// </summary>
private Logger Logger { get; set; } = LogManager.GetCurrentClassLogger();
/// <summary>
/// 海康Camera 客户端
/// </summary>
public HkCamera HkCameraClient { get; set; }
// 历史表列头中文映射
private readonly Dictionary<string, string> _historyHeaderMap = new Dictionary<string, string>
{
{ 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;
/// <summary>
/// 关闭确认标记:用于避免重复弹窗(如关闭过程触发多次 OnFormClosing
/// </summary>
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();
}
/// <summary>
/// PLC数据服务
/// </summary>
private PLCDataService PLCDataService { get; set; }
//private TimeClearDataService TimeClearService { get; set; }
private System.Windows.Forms.Timer _statusTimer;
/// <summary>
/// TouchSocket Server
/// </summary>
private TouchSocketServer TouchSocketServer { get; set; }
/// <summary>
/// 主窗体加载:
/// - 初始化海康客户端与基础配置(日志目录与保存路径)
/// - 初始化播放器与历史记录网格绑定
/// - 启动后台任务服务(顺序下载队列 + Jellyfin 批量监听)
/// - 初始化并启动 gridRULogDataGridView定时刷新实时展示下载/监听任务状态
/// </summary>
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}");
}
}
/// <summary>
/// PLC数据服务扫描条码
/// </summary>
/// <param name="sender"></param>
/// <param name="Code"></param>
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}");
}
/// <summary>
/// PLC数据服务PLC连接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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)
{
MessageBox.Show("登录成功");
LogInfo("NVR 登录成功");
_ = UpdateNvrStatusAsync();
return;
}
MessageBox.Show($"登录失败:{HkCameraClient.LastMsgErr}");
LogError($"NVR 登录失败: {HkCameraClient.LastMsgErr}");
SafeSetStatus(tslNVRConnection, false, "NVR连接状态");
return;
}
/// <summary>
/// 当前登录用户
/// </summary>
public string CurUserName { get; set; } = "Admin";
#region
/// <summary>
/// 当前播放的视频
/// </summary>
public string CurrentVideoPath { get; set; }
/// <summary>
/// 按文件名/路径去重,避免同一下载完成事件被重复处理
/// key: 文件名或路径(小写) value: 对应插入的 OEMRawUse.Id0 表示尚未完成插入)
/// </summary>
private readonly ConcurrentDictionary<string, long> _downloadProcessingKeys = new();
#endregion
#region
///// <summary>
///// 内包条码
///// </summary>
//public string CurInBagCode { get; set; } = "AAAAAA";
///// <summary>
///// 内包条码 原料名称
///// </summary>
//public string CurRawName { get; set; } = "添加剂Test";
///// <summary>
///// 内包条码 原料代码
///// </summary>
//public string CurRawCode { get; set; } = "ASASASS";
/// <summary>
/// 内包条码 解析结果
/// </summary>
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)
{
MessageBox.Show("登录成功");
LogInfo("NVR 登录成功");
return;
}
MessageBox.Show($"登录失败:{HkCameraClient.LastMsgErr}");
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<bool> 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
/// <summary>
/// 采用 BindingList + BindingSource使数据变化自动触发 UI 刷新
/// </summary>
private BindingSource historyBindingSource { get; set; }
private BindingList<OEMRawUse> historyVideoBindingList { get; set; }
/// <summary>
/// 初始化历史记录表格的数据绑定
/// </summary>
private void InitHistoryGridBinding()
{
historyBindingSource = new BindingSource();
historyVideoBindingList = new BindingList<OEMRawUse>();
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<OEMRawUse>();
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}");
}
}
/// <summary>
/// 双击行,播放该行对应的视频文件
/// </summary>
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);
}
}
/// <summary>
/// 按照 _historyHeaderMap 配置列头、隐藏不需要的列,并做常用格式化
/// </summary>
private void ConfigureHistoryGridColumns()
{
if (dataGridView1 == null || dataGridView1.Columns.Count == 0) return;
var allowedProps = new HashSet<string>
{
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
/// <summary>
/// 测试/结束操作按钮:同样仅入队一条下载任务,方便快速验证下载->监听全流程。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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}");
}
/// <summary>
/// 下载完成事件:触发后入库并启动 Jellyfin 轮询匹配
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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<RuLogRow> ruLogBindingList;
/// <summary>
/// DataGridView 行模型
/// </summary>
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
}
/// <summary>
/// 初始化 gridRULog列定义/数据绑定/低闪烁
/// </summary>
private void InitRuLogGrid()
{
ruLogBindingSource = new BindingSource();
ruLogBindingList = new BindingList<RuLogRow>();
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();
}
/// <summary>
/// 根据左侧控件宽度动态计算 gridRULog 的 Left/Size确保位置与尺寸正确
/// </summary>
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();
}
// 定时刷新已移除:仅在下载开始/完成/失败时按需更新
/// <summary>
/// 刷新 gridRULog仅当天下载
/// </summary>
private void RefreshRuLogGrid()
{
try
{
var db = FSqlContext.FDb;
var today = DateTime.Today;
var downloads = db.Select<DownloadTask>()
.Where(a => a.UpdateTime >= today)
.OrderByDescending(a => a.UpdateTime)
.ToList();
var rows = new List<RuLogRow>(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();
}
/// <summary>
/// 当前下载文件的名称改变
/// </summary>
/// <param name="name"></param>
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();
}
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}");
}
}
}
}