From 9dd458ae8bbac6db1ae6425a9e5ab47fba4f0b22 Mon Sep 17 00:00:00 2001 From: Tyrone CT Date: Thu, 4 Dec 2025 18:39:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E7=89=88=E6=9C=AC251204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FATrace.Com/NVRCom.cs | 152 +++++- FATrace.Com/ParsedCodeInfo.cs | 26 + FATrace.Model/DownloadTask.cs | 10 +- FATrace.Model/OEMRawUse.cs | 56 ++- FATrace.OEMApp/App.config | 6 +- FATrace.OEMApp/FATrace.OEMApp.csproj | 1 + FATrace.OEMApp/MainApp.Designer.cs | 370 +++++--------- FATrace.OEMApp/MainApp.cs | 458 ++++++++++++------ FATrace.OEMApp/MainApp.resx | 4 +- FATrace.OEMApp/Model/RawUseCsvDto.cs | 49 ++ FATrace.OEMApp/Model/RawUseCsvDtoMap.cs | 23 + FATrace.OEMApp/Services/CsvService.cs | 92 ++++ FATrace.OEMApp/Services/DownloadTaskWorker.cs | 252 ++++++---- .../Services/JellyfinMonitorQueueService.cs | 6 +- .../Services/JellyfinMonitorService.cs | 6 +- FATrace.OEMApp/Services/PLCDataService.cs | 8 +- .../Services/TimeClearDataService.cs | 92 +++- 17 files changed, 1089 insertions(+), 522 deletions(-) create mode 100644 FATrace.Com/ParsedCodeInfo.cs create mode 100644 FATrace.OEMApp/Model/RawUseCsvDto.cs create mode 100644 FATrace.OEMApp/Model/RawUseCsvDtoMap.cs create mode 100644 FATrace.OEMApp/Services/CsvService.cs diff --git a/FATrace.Com/NVRCom.cs b/FATrace.Com/NVRCom.cs index 0f69804..825da5c 100644 --- a/FATrace.Com/NVRCom.cs +++ b/FATrace.Com/NVRCom.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; +using System.Globalization; namespace FATrace.Com { @@ -17,7 +18,7 @@ namespace FATrace.Com /// /// /// - public static string GetVideoName(string basePath,string Code) + public static string GetVideoPathName(string basePath, string Code) { // 清洗非法文件名字符,避免保存失败 string safeCode = Code; @@ -25,11 +26,156 @@ namespace FATrace.Com { var invalid = System.IO.Path.GetInvalidFileNameChars(); safeCode = new string(Code.Where(c => !invalid.Contains(c)).ToArray()); - if (string.IsNullOrWhiteSpace(safeCode)) safeCode = "CODE"; + if (string.IsNullOrWhiteSpace(safeCode)) safeCode = DateTime.Now.ToString("yyyy-MM-dd HHmmss"); + } catch { } - return $"{basePath}\\{DateTime.Now.ToString("yyyy-MM-dd HHmmss")} {safeCode}.mp4"; + return $"{basePath}\\{safeCode}.mp4"; + } + + /// + /// 获取视频名称 + /// + /// + public static string GetVideoName(string Code) + { + string safeCode = Code; + try + { + var invalid = System.IO.Path.GetInvalidFileNameChars(); + safeCode = new string(Code.Where(c => !invalid.Contains(c)).ToArray()); + if (string.IsNullOrWhiteSpace(safeCode)) safeCode = DateTime.Now.ToString("yyyy-MM-dd HHmmss"); + + } + catch { } + return $"{safeCode}.mp4"; + } + + /// + /// 解析条码 + /// + /// + public static (string RawCode, string Batch, string Weight) ParseCode(string Code) + { + if (string.IsNullOrWhiteSpace(Code)) + { + return (string.Empty, string.Empty, string.Empty); + } + + try + { + // 标准化分隔符并切分:支持 英文逗号/中文逗号/分号/空格 + var normalized = Code.Trim() + .Replace(',', ',') + .Replace(';', ',') + .Replace(';', ','); + var parts = normalized + .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(p => p.Trim()) + .ToArray(); + + string rawCode = parts.Length > 0 ? parts[0] : string.Empty; + + // 批号:通常为 8 位日期 yyyyMMdd;保留前 8 位数字 + string batch = string.Empty; + if (parts.Length > 1) + { + var digits = new string(parts[1].Where(char.IsDigit).ToArray()); + if (digits.Length >= 8) batch = digits.Substring(0, 8); + else batch = digits; // 若不足 8 位,原样返回可供上层判定 + } + + // 重量:3/4 位数字,最后一位为小数位(例:802 => 80.2g)。 + string weight = string.Empty; + if (parts.Length > 2) + { + var digits = new string(parts[2].Where(char.IsDigit).ToArray()); + if (digits.Length >= 2) + { + // 在最后一位前插入小数点,如 802 => 80.2,1234 => 123.4 + weight = string.Concat(digits.AsSpan(0, digits.Length - 1), ".", digits.AsSpan(digits.Length - 1)); + } + else if (digits.Length == 1) + { + weight = "0." + digits; // 兜底:1 位数字视为 0.x + } + } + + return (rawCode, batch, weight); + } + catch + { + return (string.Empty, string.Empty, string.Empty); + } + } + + public static ParsedCodeInfo ParseCodeFull(string code) + { + var result = new ParsedCodeInfo(); + if (string.IsNullOrWhiteSpace(code)) return result; + result.Code = code; + + try + { + var normalized = code.Trim() + .Replace(',', ',') + .Replace(';', ',') + .Replace(';', ','); + var parts = normalized + .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(p => p.Trim()) + .ToArray(); + + result.RawCode = parts.Length > 0 ? parts[0] : string.Empty; + + if (parts.Length > 1) + { + var digits = new string(parts[1].Where(char.IsDigit).ToArray()); + result.Batch = digits.Length >= 8 ? digits.Substring(0, 8) : digits; + } + + if (parts.Length > 2) + { + var digits = new string(parts[2].Where(char.IsDigit).ToArray()); + string weightStr = string.Empty; + if (digits.Length >= 2) + { + weightStr = string.Concat(digits.AsSpan(0, digits.Length - 1), ".", digits.AsSpan(digits.Length - 1)); + } + else if (digits.Length == 1) + { + weightStr = "0." + digits; + } + if (!string.IsNullOrWhiteSpace(weightStr)) + { + if (decimal.TryParse(weightStr, NumberStyles.Any, CultureInfo.InvariantCulture, out var w)) + result.Weight = w; + } + } + + if (parts.Length > 3) + { + var digits = new string(parts[3].Where(char.IsDigit).ToArray()); + if (int.TryParse(digits, out var m)) result.ShelfLifeMonths = m; + } + + if (parts.Length > 4) + { + var digits = new string(parts[4].Where(char.IsDigit).ToArray()); + result.RegionCode = digits; + result.RegionName = digits == "01" ? "国内" : (digits == "02" ? "日本" : "未知"); + } + + if (parts.Length > 5) + { + var digits = new string(parts[5].Where(char.IsDigit).ToArray()); + if (int.TryParse(digits, out var c)) result.Count = c; + } + } + catch { } + + return result; } } } diff --git a/FATrace.Com/ParsedCodeInfo.cs b/FATrace.Com/ParsedCodeInfo.cs new file mode 100644 index 0000000..04c683b --- /dev/null +++ b/FATrace.Com/ParsedCodeInfo.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FATrace.Com +{ + public class ParsedCodeInfo + { + /// + /// 直接的条码 + /// 从这个条码中解析出其他信息 + /// + public string Code { get; set; } = string.Empty; + + public string RawCode { get; set; } = string.Empty; + public string RawName { get; set; } = string.Empty; + public string Batch { get; set; } = string.Empty; + public decimal Weight { get; set; } + public int ShelfLifeMonths { get; set; } + public string RegionCode { get; set; } = string.Empty; + public string RegionName { get; set; } = string.Empty; + public int Count { get; set; } + } +} diff --git a/FATrace.Model/DownloadTask.cs b/FATrace.Model/DownloadTask.cs index df006fb..047f82f 100644 --- a/FATrace.Model/DownloadTask.cs +++ b/FATrace.Model/DownloadTask.cs @@ -3,8 +3,8 @@ using FreeSql.DataAnnotations; namespace FATrace.Model { /// - /// 下载任务实体(持久化队列项)。 - /// 表示一条从海康 NVR 下载视频的计划与执行状态。 + /// 下载任务实体(持久化队列项) + /// 表示一条从海康 NVR 下载视频的计划与执行状态 /// [Table(Name = "DownloadTask")] public class DownloadTask @@ -25,8 +25,8 @@ namespace FATrace.Model /// /// 原料名称 /// - [Column(StringLength = 100, IsNullable = false)] - public string? RawName { get; set; } + [Column(StringLength = 100, IsNullable = true)] + public string? RawName { get; set; }=""; /// /// 原料条码 @@ -62,7 +62,7 @@ namespace FATrace.Model /// 失败时的错误信息 /// [Column(StringLength = 500)] - public string? Error { get; set; } + public string? Error { get; set; }=""; /// /// 已尝试次数(每次 Running 前加一) diff --git a/FATrace.Model/OEMRawUse.cs b/FATrace.Model/OEMRawUse.cs index f4ef48d..dd1ffac 100644 --- a/FATrace.Model/OEMRawUse.cs +++ b/FATrace.Model/OEMRawUse.cs @@ -33,10 +33,34 @@ namespace FATrace.Model public string? RawCode { get; set; } /// - /// 视频链接 + /// Video 开始时间(默认当前时间 - 30秒) /// - [Column(Name = "VideoUrl", IsNullable = false, StringLength = 500)] - public string? VideoUrl{ get; set; } + [Column(IsNullable = false)] + public DateTime VideoStartTime { get; set; } + + /// + /// Video 结束时间(默认当前时间) + /// + [Column(IsNullable = false)] + public DateTime VideoEndTime { get; set; } + + /// + /// 视频本地保存路径 + /// + [Column(Name = "VideoFilePath", IsNullable = false, StringLength = 500)] + public string? VideoFilePath { get; set; } + + /// + /// 视频名称 + /// + [Column(Name = "VideoName", IsNullable = false, StringLength = 100)] + public string? VideoName { get; set; } + + ///// + ///// 视频链接 + ///// + //[Column(Name = "VideoUrl", IsNullable = false, StringLength = 500)] + //public string? VideoUrl{ get; set; } /// /// 用户信息 @@ -44,11 +68,11 @@ namespace FATrace.Model [Column(Name = "User", IsNullable = false, StringLength = 100)] public string? User { get; set; } - /// - /// URl状态 - /// - [Column(Name = "UrlState", IsNullable = false)] - public bool UrlState { get; set; } + ///// + ///// URl状态 + ///// + //[Column(Name = "UrlState", IsNullable = false)] + //public bool UrlState { get; set; } ///// ///// 视频信息 @@ -62,15 +86,15 @@ namespace FATrace.Model [Column(ServerTime = DateTimeKind.Local, CanUpdate = true)] public DateTime CreateTime { get; set; } - /// - /// ///////////////////////////////////////////导航属性 LIN 一对一/////////////////////////////////////////////////////// - /// - public long VideoActionId { get; set; } // 外键字段,必要 + ///// + ///// ///////////////////////////////////////////导航属性 LIN 一对一/////////////////////////////////////////////////////// + ///// + //public long VideoActionId { get; set; } // 外键字段,必要 - /// - /// 视频信息 - /// - public VideoAction? VideoAction { get; set; } + ///// + ///// 视频信息 + ///// + //public VideoAction? VideoAction { get; set; } } } diff --git a/FATrace.OEMApp/App.config b/FATrace.OEMApp/App.config index be2ccbf..34ee6f8 100644 --- a/FATrace.OEMApp/App.config +++ b/FATrace.OEMApp/App.config @@ -10,13 +10,17 @@ + + + - + + diff --git a/FATrace.OEMApp/FATrace.OEMApp.csproj b/FATrace.OEMApp/FATrace.OEMApp.csproj index fcff596..e0d72f3 100644 --- a/FATrace.OEMApp/FATrace.OEMApp.csproj +++ b/FATrace.OEMApp/FATrace.OEMApp.csproj @@ -422,6 +422,7 @@ + diff --git a/FATrace.OEMApp/MainApp.Designer.cs b/FATrace.OEMApp/MainApp.Designer.cs index fd89ead..0cc57b8 100644 --- a/FATrace.OEMApp/MainApp.Designer.cs +++ b/FATrace.OEMApp/MainApp.Designer.cs @@ -31,34 +31,29 @@ namespace FATrace.OEMApp components = new System.ComponentModel.Container(); System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainApp)); statusStrip1 = new StatusStrip(); + tslPlcConnection = new ToolStripStatusLabel(); + tslSqlConnection = new ToolStripStatusLabel(); + tslNasConnection = new ToolStripStatusLabel(); + tslNVRConnection = new ToolStripStatusLabel(); imageList1 = new ImageList(components); materialTabControl1 = new ReaLTaiizor.Controls.MaterialTabControl(); tabPage1 = new TabPage(); materialCard4 = new ReaLTaiizor.Controls.MaterialCard(); - label18 = new Label(); - txtIOOutputTime = new TextBox(); - label17 = new Label(); - txtIOInputTime = new TextBox(); - label16 = new Label(); - txtIOInBagCode = new TextBox(); - txtRemainBoxCount = new TextBox(); - label15 = new Label(); - txtOutBoxCount = new TextBox(); - label14 = new Label(); - txtInBoxCount = new TextBox(); - label13 = new Label(); + txtCsvSaveState = new TextBox(); label12 = new Label(); - label9 = new Label(); - txtIORawName = new TextBox(); + DownloadFileName = new TextBox(); label10 = new Label(); - txtIOOutBagCode = new TextBox(); + label9 = new Label(); label11 = new Label(); + DownloadProgressBarMain = new ProgressBar(); materialCard3 = new ReaLTaiizor.Controls.MaterialCard(); + LvLog = new ListView(); label8 = new Label(); materialCard2 = new ReaLTaiizor.Controls.MaterialCard(); + label19 = new Label(); + txtRURawCode = new TextBox(); gridRULog = new DataGridView(); btnRawStopLoadVideo = new Button(); - DownloadProgressBarMain = new ProgressBar(); btnTestAction = new Button(); label7 = new Label(); txtRURawName = new TextBox(); @@ -83,9 +78,8 @@ namespace FATrace.OEMApp metroProgressBar1 = new ReaLTaiizor.Controls.MetroProgressBar(); DownloadProgressBar = new ProgressBar(); btnStopLoadVideo = new Button(); - btnLoadVideo = new Button(); btnNVRLogin = new Button(); - LvLog = new ListView(); + statusStrip1.SuspendLayout(); materialTabControl1.SuspendLayout(); tabPage1.SuspendLayout(); materialCard4.SuspendLayout(); @@ -101,12 +95,41 @@ namespace FATrace.OEMApp // // statusStrip1 // + statusStrip1.Items.AddRange(new ToolStripItem[] { tslPlcConnection, tslSqlConnection, tslNasConnection, tslNVRConnection }); statusStrip1.Location = new Point(3, 1005); statusStrip1.Name = "statusStrip1"; statusStrip1.Size = new Size(1914, 22); statusStrip1.TabIndex = 1; statusStrip1.Text = "statusStrip1"; // + // tslPlcConnection + // + tslPlcConnection.Name = "tslPlcConnection"; + tslPlcConnection.Size = new Size(474, 17); + tslPlcConnection.Spring = true; + tslPlcConnection.Text = "Plc连接状态"; + // + // tslSqlConnection + // + tslSqlConnection.Name = "tslSqlConnection"; + tslSqlConnection.Size = new Size(474, 17); + tslSqlConnection.Spring = true; + tslSqlConnection.Text = "服务连接状态"; + // + // tslNasConnection + // + tslNasConnection.Name = "tslNasConnection"; + tslNasConnection.Size = new Size(474, 17); + tslNasConnection.Spring = true; + tslNasConnection.Text = "NAS连接状态"; + // + // tslNVRConnection + // + tslNVRConnection.Name = "tslNVRConnection"; + tslNVRConnection.Size = new Size(474, 17); + tslNVRConnection.Spring = true; + tslNVRConnection.Text = "NVR连接状态"; + // // imageList1 // imageList1.ColorDepth = ColorDepth.Depth32Bit; @@ -155,24 +178,13 @@ namespace FATrace.OEMApp // materialCard4 // materialCard4.BackColor = Color.FromArgb(255, 255, 255); - materialCard4.Controls.Add(label18); - materialCard4.Controls.Add(txtIOOutputTime); - materialCard4.Controls.Add(label17); - materialCard4.Controls.Add(txtIOInputTime); - materialCard4.Controls.Add(label16); - materialCard4.Controls.Add(txtIOInBagCode); - materialCard4.Controls.Add(txtRemainBoxCount); - materialCard4.Controls.Add(label15); - materialCard4.Controls.Add(txtOutBoxCount); - materialCard4.Controls.Add(label14); - materialCard4.Controls.Add(txtInBoxCount); - materialCard4.Controls.Add(label13); + materialCard4.Controls.Add(txtCsvSaveState); materialCard4.Controls.Add(label12); - materialCard4.Controls.Add(label9); - materialCard4.Controls.Add(txtIORawName); + materialCard4.Controls.Add(DownloadFileName); materialCard4.Controls.Add(label10); - materialCard4.Controls.Add(txtIOOutBagCode); + materialCard4.Controls.Add(label9); materialCard4.Controls.Add(label11); + materialCard4.Controls.Add(DownloadProgressBarMain); materialCard4.Depth = 0; materialCard4.ForeColor = Color.FromArgb(222, 0, 0, 0); materialCard4.Location = new Point(14, 373); @@ -180,187 +192,72 @@ namespace FATrace.OEMApp materialCard4.MouseState = ReaLTaiizor.Helper.MaterialDrawHelper.MaterialMouseState.HOVER; materialCard4.Name = "materialCard4"; materialCard4.Padding = new Padding(14); - materialCard4.Size = new Size(864, 535); + materialCard4.Size = new Size(561, 535); materialCard4.TabIndex = 5; // - // label18 + // txtCsvSaveState // - label18.AutoSize = true; - label18.ForeColor = Color.DimGray; - label18.Location = new Point(41, 260); - label18.Name = "label18"; - label18.Size = new Size(88, 26); - label18.TabIndex = 17; - label18.Text = "出库时间"; - // - // txtIOOutputTime - // - txtIOOutputTime.Location = new Point(154, 257); - txtIOOutputTime.Name = "txtIOOutputTime"; - txtIOOutputTime.Size = new Size(446, 32); - txtIOOutputTime.TabIndex = 16; - // - // label17 - // - label17.AutoSize = true; - label17.ForeColor = Color.DimGray; - label17.Location = new Point(41, 211); - label17.Name = "label17"; - label17.Size = new Size(88, 26); - label17.TabIndex = 15; - label17.Text = "入库时间"; - // - // txtIOInputTime - // - txtIOInputTime.Location = new Point(154, 208); - txtIOInputTime.Name = "txtIOInputTime"; - txtIOInputTime.Size = new Size(446, 32); - txtIOInputTime.TabIndex = 14; - // - // label16 - // - label16.AutoSize = true; - label16.ForeColor = Color.DimGray; - label16.Location = new Point(41, 109); - label16.Name = "label16"; - label16.Size = new Size(107, 26); - label16.TabIndex = 13; - label16.Text = "内袋二维码"; - // - // txtIOInBagCode - // - txtIOInBagCode.Location = new Point(154, 106); - txtIOInBagCode.Name = "txtIOInBagCode"; - txtIOInBagCode.Size = new Size(446, 32); - txtIOInBagCode.TabIndex = 12; - // - // txtRemainBoxCount - // - txtRemainBoxCount.BackColor = SystemColors.ButtonHighlight; - txtRemainBoxCount.BorderStyle = BorderStyle.None; - txtRemainBoxCount.Font = new Font("Microsoft YaHei UI", 21.75F, FontStyle.Bold, GraphicsUnit.Point, 134); - txtRemainBoxCount.Location = new Point(507, 417); - txtRemainBoxCount.Name = "txtRemainBoxCount"; - txtRemainBoxCount.ReadOnly = true; - txtRemainBoxCount.Size = new Size(100, 37); - txtRemainBoxCount.TabIndex = 11; - txtRemainBoxCount.Text = "100"; - txtRemainBoxCount.TextAlign = HorizontalAlignment.Center; - // - // label15 - // - label15.AutoSize = true; - label15.Font = new Font("Microsoft YaHei UI", 15.75F, FontStyle.Bold, GraphicsUnit.Point, 134); - label15.ForeColor = Color.DimGray; - label15.Location = new Point(496, 475); - label15.Name = "label15"; - label15.Size = new Size(117, 28); - label15.TabIndex = 10; - label15.Text = "剩余总箱数"; - // - // txtOutBoxCount - // - txtOutBoxCount.BackColor = SystemColors.ButtonHighlight; - txtOutBoxCount.BorderStyle = BorderStyle.None; - txtOutBoxCount.Font = new Font("Microsoft YaHei UI", 21.75F, FontStyle.Bold, GraphicsUnit.Point, 134); - txtOutBoxCount.Location = new Point(317, 417); - txtOutBoxCount.Name = "txtOutBoxCount"; - txtOutBoxCount.ReadOnly = true; - txtOutBoxCount.Size = new Size(100, 37); - txtOutBoxCount.TabIndex = 9; - txtOutBoxCount.Text = "100"; - txtOutBoxCount.TextAlign = HorizontalAlignment.Center; - // - // label14 - // - label14.AutoSize = true; - label14.Font = new Font("Microsoft YaHei UI", 15.75F, FontStyle.Bold, GraphicsUnit.Point, 134); - label14.ForeColor = Color.DimGray; - label14.Location = new Point(306, 475); - label14.Name = "label14"; - label14.Size = new Size(117, 28); - label14.TabIndex = 8; - label14.Text = "出库总箱数"; - // - // txtInBoxCount - // - txtInBoxCount.BackColor = SystemColors.ButtonHighlight; - txtInBoxCount.BorderStyle = BorderStyle.None; - txtInBoxCount.Font = new Font("Microsoft YaHei UI", 21.75F, FontStyle.Bold, GraphicsUnit.Point, 134); - txtInBoxCount.Location = new Point(103, 417); - txtInBoxCount.Name = "txtInBoxCount"; - txtInBoxCount.ReadOnly = true; - txtInBoxCount.Size = new Size(100, 37); - txtInBoxCount.TabIndex = 7; - txtInBoxCount.Text = "100"; - txtInBoxCount.TextAlign = HorizontalAlignment.Center; - // - // label13 - // - label13.AutoSize = true; - label13.Font = new Font("Microsoft YaHei UI", 15.75F, FontStyle.Bold, GraphicsUnit.Point, 134); - label13.ForeColor = Color.DimGray; - label13.Location = new Point(92, 475); - label13.Name = "label13"; - label13.Size = new Size(117, 28); - label13.TabIndex = 6; - label13.Text = "入库总箱数"; + txtCsvSaveState.Location = new Point(27, 264); + txtCsvSaveState.Name = "txtCsvSaveState"; + txtCsvSaveState.ReadOnly = true; + txtCsvSaveState.Size = new Size(493, 32); + txtCsvSaveState.TabIndex = 15; // // label12 // label12.AutoSize = true; - label12.Font = new Font("Microsoft YaHei UI", 15.75F, FontStyle.Bold, GraphicsUnit.Point, 134); - label12.ForeColor = Color.FromArgb(64, 64, 64); - label12.Location = new Point(10, 319); + label12.ForeColor = Color.DimGray; + label12.Location = new Point(26, 229); label12.Name = "label12"; - label12.Size = new Size(159, 28); - label12.TabIndex = 5; - label12.Text = "成品出入库统计"; + label12.Size = new Size(164, 26); + label12.TabIndex = 14; + label12.Text = "CSV文件生成状态"; // - // label9 + // DownloadFileName // - label9.AutoSize = true; - label9.ForeColor = Color.DimGray; - label9.Location = new Point(41, 160); - label9.Name = "label9"; - label9.Size = new Size(88, 26); - label9.TabIndex = 4; - label9.Text = "原料名称"; - // - // txtIORawName - // - txtIORawName.Location = new Point(154, 157); - txtIORawName.Name = "txtIORawName"; - txtIORawName.Size = new Size(446, 32); - txtIORawName.TabIndex = 3; + DownloadFileName.Location = new Point(27, 96); + DownloadFileName.Name = "DownloadFileName"; + DownloadFileName.ReadOnly = true; + DownloadFileName.Size = new Size(493, 32); + DownloadFileName.TabIndex = 11; // // label10 // label10.AutoSize = true; label10.ForeColor = Color.DimGray; - label10.Location = new Point(41, 61); + label10.Location = new Point(27, 58); label10.Name = "label10"; - label10.Size = new Size(107, 26); - label10.TabIndex = 2; - label10.Text = "外箱二维码"; + label10.Size = new Size(126, 26); + label10.TabIndex = 12; + label10.Text = "视频文件名称"; // - // txtIOOutBagCode + // label9 // - txtIOOutBagCode.Location = new Point(154, 58); - txtIOOutBagCode.Name = "txtIOOutBagCode"; - txtIOOutBagCode.Size = new Size(446, 32); - txtIOOutBagCode.TabIndex = 1; + label9.AutoSize = true; + label9.ForeColor = Color.DimGray; + label9.Location = new Point(27, 144); + label9.Name = "label9"; + label9.Size = new Size(88, 26); + label9.TabIndex = 11; + label9.Text = "下载进度"; // // label11 // label11.AutoSize = true; - label11.Font = new Font("Microsoft YaHei UI", 15.75F, FontStyle.Bold, GraphicsUnit.Point, 134); + label11.Font = new Font("Microsoft YaHei UI", 16F, FontStyle.Bold); label11.ForeColor = Color.FromArgb(64, 64, 64); label11.Location = new Point(10, 9); label11.Name = "label11"; - label11.Size = new Size(159, 28); + label11.Size = new Size(145, 30); label11.TabIndex = 0; - label11.Text = "成品出入库信息"; + label11.Text = "视频下载状态"; + // + // DownloadProgressBarMain + // + DownloadProgressBarMain.Location = new Point(27, 178); + DownloadProgressBarMain.Name = "DownloadProgressBarMain"; + DownloadProgressBarMain.Size = new Size(493, 32); + DownloadProgressBarMain.TabIndex = 6; // // materialCard3 // @@ -369,14 +266,22 @@ namespace FATrace.OEMApp materialCard3.Controls.Add(label8); materialCard3.Depth = 0; materialCard3.ForeColor = Color.FromArgb(222, 0, 0, 0); - materialCard3.Location = new Point(897, 373); + materialCard3.Location = new Point(590, 373); materialCard3.Margin = new Padding(14); materialCard3.MouseState = ReaLTaiizor.Helper.MaterialDrawHelper.MaterialMouseState.HOVER; materialCard3.Name = "materialCard3"; materialCard3.Padding = new Padding(14); - materialCard3.Size = new Size(1000, 535); + materialCard3.Size = new Size(1307, 535); materialCard3.TabIndex = 1; // + // LvLog + // + LvLog.Location = new Point(17, 58); + LvLog.Name = "LvLog"; + LvLog.Size = new Size(1281, 460); + LvLog.TabIndex = 6; + LvLog.UseCompatibleStateImageBehavior = false; + // // label8 // label8.AutoSize = true; @@ -391,9 +296,10 @@ namespace FATrace.OEMApp // materialCard2 // materialCard2.BackColor = Color.FromArgb(255, 255, 255); + materialCard2.Controls.Add(label19); + materialCard2.Controls.Add(txtRURawCode); materialCard2.Controls.Add(gridRULog); materialCard2.Controls.Add(btnRawStopLoadVideo); - materialCard2.Controls.Add(DownloadProgressBarMain); materialCard2.Controls.Add(btnTestAction); materialCard2.Controls.Add(label7); materialCard2.Controls.Add(txtRURawName); @@ -410,18 +316,35 @@ namespace FATrace.OEMApp materialCard2.Size = new Size(1883, 348); materialCard2.TabIndex = 0; // + // label19 + // + label19.AutoSize = true; + label19.ForeColor = Color.DimGray; + label19.Location = new Point(41, 180); + label19.Name = "label19"; + label19.Size = new Size(88, 26); + label19.TabIndex = 10; + label19.Text = "原料条码"; + // + // txtRURawCode + // + txtRURawCode.Location = new Point(154, 177); + txtRURawCode.Name = "txtRURawCode"; + txtRURawCode.Size = new Size(387, 32); + txtRURawCode.TabIndex = 9; + // // gridRULog // gridRULog.AllowUserToAddRows = false; gridRULog.AllowUserToDeleteRows = false; gridRULog.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; gridRULog.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; - gridRULog.Location = new Point(557, 9); + gridRULog.Location = new Point(585, 7); gridRULog.Name = "gridRULog"; gridRULog.ReadOnly = true; gridRULog.RowHeadersVisible = false; gridRULog.SelectionMode = DataGridViewSelectionMode.FullRowSelect; - gridRULog.Size = new Size(1309, 338); + gridRULog.Size = new Size(1281, 338); gridRULog.TabIndex = 8; // // btnRawStopLoadVideo @@ -434,13 +357,6 @@ namespace FATrace.OEMApp btnRawStopLoadVideo.UseVisualStyleBackColor = true; btnRawStopLoadVideo.Click += btnRawStopLoadVideo_Click; // - // DownloadProgressBarMain - // - DownloadProgressBarMain.Location = new Point(7, 321); - DownloadProgressBarMain.Name = "DownloadProgressBarMain"; - DownloadProgressBarMain.Size = new Size(540, 23); - DownloadProgressBarMain.TabIndex = 6; - // // btnTestAction // btnTestAction.Location = new Point(144, 271); @@ -492,9 +408,9 @@ namespace FATrace.OEMApp label5.ForeColor = Color.FromArgb(64, 64, 64); label5.Location = new Point(10, 9); label5.Name = "label5"; - label5.Size = new Size(138, 28); + label5.Size = new Size(180, 28); label5.TabIndex = 0; - label5.Text = "原料使用信息"; + label5.Text = "当前原料使用信息"; // // tabPage2 // @@ -654,7 +570,6 @@ namespace FATrace.OEMApp tabPage3.Controls.Add(metroProgressBar1); tabPage3.Controls.Add(DownloadProgressBar); tabPage3.Controls.Add(btnStopLoadVideo); - tabPage3.Controls.Add(btnLoadVideo); tabPage3.Controls.Add(btnNVRLogin); tabPage3.ImageKey = "set3.png"; tabPage3.Location = new Point(4, 26); @@ -704,16 +619,6 @@ namespace FATrace.OEMApp btnStopLoadVideo.UseVisualStyleBackColor = true; btnStopLoadVideo.Click += btnStopLoadVideo_Click; // - // btnLoadVideo - // - btnLoadVideo.Location = new Point(247, 54); - btnLoadVideo.Name = "btnLoadVideo"; - btnLoadVideo.Size = new Size(130, 58); - btnLoadVideo.TabIndex = 1; - btnLoadVideo.Text = "下载视频"; - btnLoadVideo.UseVisualStyleBackColor = true; - btnLoadVideo.Click += btnLoadVideo_Click; - // // btnNVRLogin // btnNVRLogin.Location = new Point(84, 54); @@ -724,14 +629,6 @@ namespace FATrace.OEMApp btnNVRLogin.UseVisualStyleBackColor = true; btnNVRLogin.Click += btnNVRLogin_Click; // - // LvLog - // - LvLog.Location = new Point(17, 58); - LvLog.Name = "LvLog"; - LvLog.Size = new Size(966, 460); - LvLog.TabIndex = 6; - LvLog.UseCompatibleStateImageBehavior = false; - // // MainApp // AutoScaleDimensions = new SizeF(8F, 17F); @@ -747,6 +644,8 @@ namespace FATrace.OEMApp Text = "添加剂追溯系统"; WindowState = FormWindowState.Maximized; Load += MainApp_Load; + statusStrip1.ResumeLayout(false); + statusStrip1.PerformLayout(); materialTabControl1.ResumeLayout(false); tabPage1.ResumeLayout(false); materialCard4.ResumeLayout(false); @@ -778,7 +677,6 @@ namespace FATrace.OEMApp private TabPage tabPage2; private TabPage tabPage3; private Button btnNVRLogin; - private Button btnLoadVideo; private Button btnStopLoadVideo; private ProgressBar DownloadProgressBar; private ReaLTaiizor.Controls.MetroProgressBar metroProgressBar1; @@ -804,28 +702,22 @@ namespace FATrace.OEMApp private ReaLTaiizor.Controls.MaterialCard materialCard3; private Label label8; private ReaLTaiizor.Controls.MaterialCard materialCard4; - private Label label9; - private TextBox txtIORawName; - private Label label10; - private TextBox txtIOOutBagCode; private Label label11; - private Label label12; - private Label label13; - private TextBox txtInBoxCount; - private TextBox txtOutBoxCount; - private Label label14; - private TextBox txtRemainBoxCount; - private Label label15; - private Label label16; - private TextBox txtIOInBagCode; - private Label label18; - private TextBox txtIOOutputTime; - private Label label17; - private TextBox txtIOInputTime; private Button btnTestAction; private ProgressBar DownloadProgressBarMain; private Button btnRawStopLoadVideo; private DataGridView gridRULog; private ListView LvLog; + private ToolStripStatusLabel tslPlcConnection; + private ToolStripStatusLabel tslSqlConnection; + private ToolStripStatusLabel tslNasConnection; + private ToolStripStatusLabel tslNVRConnection; + private Label label19; + private TextBox txtRURawCode; + private TextBox DownloadFileName; + private Label label10; + private Label label9; + private Label label12; + private TextBox txtCsvSaveState; } } \ No newline at end of file diff --git a/FATrace.OEMApp/MainApp.cs b/FATrace.OEMApp/MainApp.cs index 1757625..effe215 100644 --- a/FATrace.OEMApp/MainApp.cs +++ b/FATrace.OEMApp/MainApp.cs @@ -3,6 +3,8 @@ using FATrace.HKNetLib.Hardware; using FATrace.HKNetLib.Wrapper; using FATrace.Model; using FATrace.OEMApp.Services; +using FATrace.OEMApp.Model; +using System.Threading.Tasks; using LibVLCSharp.Shared; using NLog; using ReaLTaiizor.Forms; @@ -51,8 +53,8 @@ namespace FATrace.OEMApp LvLog.GridLines = true; LvLog.HeaderStyle = ColumnHeaderStyle.Nonclickable; LvLog.Columns.Add("时间", 150); - LvLog.Columns.Add("级别", 60); - LvLog.Columns.Add("消息", 720); + LvLog.Columns.Add("级别", 100); + LvLog.Columns.Add("消息", 800); } finally { @@ -98,6 +100,7 @@ namespace FATrace.OEMApp /// private PLCDataService PLCDataService { get; set; } private TimeClearDataService TimeClearService { get; set; } + private System.Windows.Forms.Timer _statusTimer; /// /// 主窗体加载: @@ -143,8 +146,13 @@ namespace FATrace.OEMApp { DownloadTaskWorker.Instance.Start(HkCameraClient); LogInfo("下载队列服务已启动"); - JellyfinMonitorQueueService.Instance.Start(); - LogInfo("Jellyfin 监控服务已启动"); + 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(); @@ -158,9 +166,15 @@ namespace FATrace.OEMApp // 初始化 gridRULog 并启动 UI 定时刷新 InitRuLogGrid(); - // 立即刷新一次,避免首次 1s 空白 + // 仅初始化一次,当天数据 RefreshRuLogGrid(); - StartTaskUiTimer(); + + // 初始化底部连接状态(立即检测一次 + 定时轻量检测) + SafeSetStatus(tslPlcConnection, PLCDataService?.PlcConnected == true, "Plc连接状态"); + _ = UpdateDbStatusAsync(); + _ = UpdateNasStatusAsync(); + _ = UpdateNvrStatusAsync(); + StartStatusTimer(); //materialListView1.DataBindings try @@ -194,17 +208,30 @@ namespace FATrace.OEMApp /// private void PLCDataService_ScanCodeEventHandler(object? sender, string Code) { + //解析Code条码数据,内包条码数据 + CurParsedCodeInfo = NVRCom.ParseCodeFull(Code); + + var taskId = DownloadTaskWorker.Instance.Enqueue( + CurParsedCodeInfo, + user: CurUserName, + start: DateTime.Now, + end: DateTime.Now.AddSeconds(DownloadTaskWorker.VideoTime+2) + ); + //MessageBox.Show($"[Test] 已入队下载任务,Id={taskId}"); + LogInfo($"扫码: {Code}"); } - /// - /// PLC数据服务:PLC连接 - /// - /// - /// + /// + /// 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() @@ -220,10 +247,12 @@ namespace FATrace.OEMApp { MessageBox.Show("登录成功"); LogInfo("NVR 登录成功"); + _ = UpdateNvrStatusAsync(); return; } MessageBox.Show($"登录失败:{HkCameraClient.LastMsgErr}"); LogError($"NVR 登录失败: {HkCameraClient.LastMsgErr}"); + SafeSetStatus(tslNVRConnection, false, "NVR连接状态"); return; } @@ -250,15 +279,24 @@ namespace FATrace.OEMApp #region 内包信息 - /// - /// 内包条码 - /// - public string CurInBagCode { get; set; } = "AAAAAA"; + ///// + ///// 内包条码 + ///// + //public string CurInBagCode { get; set; } = "AAAAAA"; + + ///// + ///// 内包条码 原料名称 + ///// + //public string CurRawName { get; set; } = "添加剂Test"; + ///// + ///// 内包条码 原料代码 + ///// + //public string CurRawCode { get; set; } = "ASASASS"; /// - /// 内包条码 原料名称 + /// 内包条码 解析结果 /// - public string CurInBagRawName { get; set; } = "添加剂Test"; + public ParsedCodeInfo CurParsedCodeInfo { get; set; } #endregion @@ -282,25 +320,6 @@ namespace FATrace.OEMApp return; } - - /// - /// 下载按钮:仅将请求入队为 DownloadTask,由后台 DownloadTaskWorker 顺序处理。 - /// 注意:不会在此处直接调用 SDK 下载,也不会订阅 SDK 事件,避免并发与重复。 - /// - private void btnLoadVideo_Click(object sender, EventArgs e) - { - // 仅入队下载任务,由后台 DownloadTaskWorker 顺序处理 - var taskId = DownloadTaskWorker.Instance.Enqueue( - code: CurInBagCode, - rawName: CurInBagRawName, - user: CurUserName, - start: DateTime.Now.AddSeconds(-30), - end: DateTime.Now - ); - MessageBox.Show($"已入队下载任务,Id={taskId}"); - LogInfo($"下载任务入队 Id={taskId} Code={CurInBagCode}"); - } - private void HkCameraClient_NVRLoadVideoProcessEventHandler(object? sender, short value) { this.BeginInvoke(new Action(() => @@ -310,7 +329,7 @@ namespace FATrace.OEMApp if (value >= 100 && _lastProgressLogged != 100) { _lastProgressLogged = 100; - LogInfo("下载进度 100%"); + LogInfo($"{DownloadTaskWorker.Instance.CurDownloadTask.Code} - 下载进度 100%"); } } @@ -592,13 +611,12 @@ namespace FATrace.OEMApp private void btnTestAction_Click(object sender, EventArgs e) { var taskId = DownloadTaskWorker.Instance.Enqueue( - code: CurInBagCode, - rawName: CurInBagRawName, + CurParsedCodeInfo, user: CurUserName, - start: DateTime.Now.AddSeconds(-100), - end: DateTime.Now + start: DateTime.Now, + end: DateTime.Now.AddSeconds(DownloadTaskWorker.VideoTime) ); - MessageBox.Show($"[Test] 已入队下载任务,Id={taskId}"); + //MessageBox.Show($"[Test] 已入队下载任务,Id={taskId}"); } /// @@ -628,59 +646,12 @@ namespace FATrace.OEMApp } LogInfo($"下载完成: {localNameOrPath}"); - - // 先保存当前的信息,记录主键 Id - var rawUse = new OEMRawUse() - { - InBagCode = CurInBagCode, - RawName = CurInBagRawName, - User = CurUserName, - UrlState = false, - VideoUrl = string.Empty - }; - long rawUseId = 0; - try - { - rawUseId = FSqlContext.FDb.Insert(rawUse).ExecuteIdentity(); - _downloadProcessingKeys[key] = rawUseId; - } - catch (Exception ex) - { - _downloadProcessingKeys.TryRemove(key, out _); - System.Diagnostics.Debug.WriteLine($"[NVRLoadVideoComplete] 插入 OEMRawUse 失败: {ex.Message}"); - return; - } - - // 后台执行,避免阻塞 UI 线程 - Task.Run(async () => - { - try - { - var monitor = new JellyfinMonitorService(); - await monitor.MonitorAndUpdateAfterDownloadAsync( - oemRawUseId: rawUseId, - videoLocalPathOrName: localNameOrPath, - code: CurInBagCode, - rawName: CurInBagRawName, - userName: CurUserName, - cancellationToken: CancellationToken.None - ).ConfigureAwait(false); - } - catch (Exception ex1) - { - System.Diagnostics.Debug.WriteLine($"[JellyfinMonitor] 异常: {ex1.Message}"); - } - finally - { - // 移除去重 key,允许后续相同文件再次处理(如需重试) - _downloadProcessingKeys.TryRemove(key, out _); - } - }); + // Jellyfin 监控已停用:仅移除去重 key + _downloadProcessingKeys.TryRemove(key, out _); } #region 任务监控(gridRULog / DataGridView) - private System.Windows.Forms.Timer _taskUiTimer; private BindingSource ruLogBindingSource; private BindingList ruLogBindingList; @@ -691,6 +662,7 @@ namespace FATrace.OEMApp { 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; } @@ -726,6 +698,14 @@ namespace FATrace.OEMApp 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", @@ -772,7 +752,6 @@ namespace FATrace.OEMApp gridRULog.ColumnHeadersDefaultCellStyle.ForeColor = Color.Black; gridRULog.RowHeadersVisible = false; // 使用 Designer 中的 Location/Size 与 Anchor,不额外 Dock - gridRULog.BringToFront(); // 跟随父容器尺寸变化进行定位与大小调整 materialCard2.Resize -= MaterialCard2_Resize; @@ -810,7 +789,6 @@ namespace FATrace.OEMApp var height = Math.Max(120, client.Height - top - 9); gridRULog.Location = new Point(left, top); gridRULog.Size = new Size(width, height); - gridRULog.BringToFront(); } catch { } } @@ -820,40 +798,22 @@ namespace FATrace.OEMApp LayoutRuLogGrid(); } - /// - /// 启动 UI 定时器,定时刷新下载与 Jellyfin 监听任务状态 - /// - private void StartTaskUiTimer() - { - _taskUiTimer = new System.Windows.Forms.Timer(); - _taskUiTimer.Interval = 1000; // 1s 刷新 - _taskUiTimer.Tick += TaskUiTimer_Tick; - _taskUiTimer.Start(); - } - - private void TaskUiTimer_Tick(object? sender, EventArgs e) - { - RefreshRuLogGrid(); - } + // 定时刷新已移除:仅在下载开始/完成/失败时按需更新 /// - /// 刷新 gridRULog(下载 + 监听) + /// 刷新 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) - .Limit(50) .ToList(); - var monitors = db.Select() - .OrderByDescending(a => a.UpdateTime) - .Limit(50) - .ToList(); - - var rows = new List(downloads.Count + monitors.Count); + var rows = new List(downloads.Count); // 运行中优先 foreach (var t in downloads.Where(x => x.Status == TaskStatus.Running)) @@ -862,24 +822,14 @@ namespace FATrace.OEMApp { 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 monitors.Where(x => x.Status == TaskStatus.Running)) - { - rows.Add(new RuLogRow - { - TimeText = t.UpdateTime.ToString("HH:mm:ss"), - Type = "监听", - TaskId = t.Id, - StatusText = t.Status.ToString(), - ProgressText = string.Empty, - Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.FoundItemId ?? t.LocalFileNameOrPath ?? string.Empty) : t.Error! - }); - } + // 监听任务展示已移除 // 其余 foreach (var t in downloads.Where(x => x.Status != TaskStatus.Running)) @@ -888,26 +838,16 @@ namespace FATrace.OEMApp { 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 monitors.Where(x => x.Status != TaskStatus.Running)) - { - rows.Add(new RuLogRow - { - TimeText = t.UpdateTime.ToString("HH:mm:ss"), - Type = "监听", - TaskId = t.Id, - StatusText = t.Status.ToString(), - ProgressText = string.Empty, - Remark = string.IsNullOrWhiteSpace(t.Error) ? (t.FoundItemId ?? t.LocalFileNameOrPath ?? string.Empty) : t.Error! - }); - } + // 监听任务展示已移除 - // 批量更新绑定列表,尽量减少闪烁 + // 批量更新绑定列表 ruLogBindingList.RaiseListChangedEvents = false; ruLogBindingList.Clear(); foreach (var r in rows) @@ -923,6 +863,138 @@ namespace FATrace.OEMApp } } + 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 { } + } + + 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 { } + } + + 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 + { + 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 { } + } + private static void SetDataGridViewDoubleBuffered(DataGridView dgv) { try @@ -947,8 +1019,112 @@ namespace FATrace.OEMApp protected override void OnFormClosing(FormClosingEventArgs e) { + 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 { } base.OnFormClosing(e); } + + /// + /// 当前下载文件的名称改变 + /// + /// + 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 { } + } + + #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 { } + } + + 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 { } + } + + 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 } } diff --git a/FATrace.OEMApp/MainApp.resx b/FATrace.OEMApp/MainApp.resx index 5417aa4..31552f1 100644 --- a/FATrace.OEMApp/MainApp.resx +++ b/FATrace.OEMApp/MainApp.resx @@ -128,7 +128,7 @@ AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAaCEAAAJNU0Z0AUkBTAIBAQgB - AAFYAQEBWAEBARABAAEQAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABQAMAATADAAEBAQABIAYAATD/ + AAGIAQEBiAEBARABAAEQAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABQAMAATADAAEBAQABIAYAATD/ AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AFIAAdsBlgESAf8B2wGWARIB/wQAAxIBGANJAYgB 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wQAAdsB lgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8UAAHbAZYB @@ -217,7 +217,7 @@ 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wMqAUAY AAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wgAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B 2wGWARIB/xgAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsB - lgESAf8B2wGWARIB/wJ8AVsB+BAAAm8BUQH3AdsBlgESAf8MAAMzAVAB2wGWARIB/wQAAdsBlgESAf8Q + lgESAf8B2wGWARIB/wJ8AVwB+BAAAm8BUQH3AdsBlgESAf8MAAMzAVAB2wGWARIB/wQAAdsBlgESAf8Q AAHbAZYBEgH/EAACWwFZAcAgAAJHAUYBgAMqAUAUAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYB EgH/DAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/CAAB2wGWARIB/wHbAZYB EgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B diff --git a/FATrace.OEMApp/Model/RawUseCsvDto.cs b/FATrace.OEMApp/Model/RawUseCsvDto.cs new file mode 100644 index 0000000..d520438 --- /dev/null +++ b/FATrace.OEMApp/Model/RawUseCsvDto.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FATrace.OEMApp.Model +{ + /// + /// 原材料使用CSV保存信息 + /// + public class RawUseCsvDto + { + /// + /// 原料编号 + /// + public string? RawCode { get; set; } + + /// + /// 原料名称 + /// + public string? RawName { get; set; } + + /// + /// 内袋二维码 + /// + public string? InBagCode { get; set; } + + ///// + ///// 外箱二维码 + ///// + //public string? BoxCode { get; set; } + + /// + /// 操作者 + /// + public string? OpUser { get; set; } + + /// + /// 视频保存路径 + /// + public string? VideoSavePath { get; set; } + + /// + /// 原料使用时间 + /// + public DateTime UseTime { get; set; } + } +} diff --git a/FATrace.OEMApp/Model/RawUseCsvDtoMap.cs b/FATrace.OEMApp/Model/RawUseCsvDtoMap.cs new file mode 100644 index 0000000..129757f --- /dev/null +++ b/FATrace.OEMApp/Model/RawUseCsvDtoMap.cs @@ -0,0 +1,23 @@ +using CsvHelper.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FATrace.OEMApp.Model +{ + public class RawUseCsvDtoMap : ClassMap + { + public RawUseCsvDtoMap() + { + Map(x => x.RawCode).Index(0).Name("原料编号"); + Map(x => x.RawName).Index(1).Name("原料名称"); + Map(x => x.InBagCode).Index(2).Name("内袋二维码"); + Map(x => x.OpUser).Index(3).Name("操作者"); + Map(x => x.VideoSavePath).Index(4).Name("视频保存路径"); + Map(x => x.UseTime).Index(5).Name("使用时间").TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss"); + + } + } +} diff --git a/FATrace.OEMApp/Services/CsvService.cs b/FATrace.OEMApp/Services/CsvService.cs new file mode 100644 index 0000000..346a34a --- /dev/null +++ b/FATrace.OEMApp/Services/CsvService.cs @@ -0,0 +1,92 @@ +using CsvHelper; +using CsvHelper.Configuration; +using FATrace.Com; +using FATrace.OEMApp.Model; +using NLog; +using System.Globalization; +using System.Text; + +namespace FATrace.OEMApp.Services +{ + public class CsvService + { + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); + public CsvService() + { + RawUseCsvPath = ConfigHelper.GetValue("RawUseCsvPath"); + } + + /// + /// 原料使用信息CSV文件路径 + /// + public string RawUseCsvPath { get; set; } + + /// + /// 将一条原料使用记录导出为单独CSV文件(包含表头) + /// 文件保存目录来自 RawUseCsvPath;文件名包含时间戳与内袋二维码(若有) + /// + /// 原料使用记录 + /// 生成的CSV完整路径 + /// 当 data 为空时抛出 + /// 当 RawUseCsvPath 未配置时抛出 + /// 当写入文件失败时抛出 + public string ExportSingle(RawUseCsvDto data) + { + if (data == null) throw new ArgumentNullException(nameof(data)); + + if (string.IsNullOrWhiteSpace(RawUseCsvPath)) + { + const string msg = "RawUseCsvPath 未配置,无法导出CSV。"; + _logger.Error(msg); + throw new InvalidOperationException(msg); + } + + try + { + // 确保目录存在 + Directory.CreateDirectory(RawUseCsvPath); + + // 构建安全文件名:时间戳_内袋二维码.csv(若二维码为空则用RawUse代替) + var safeCode = SanitizeFileName(string.IsNullOrWhiteSpace(data.InBagCode) ? "RawUse" : data.InBagCode!.Trim()); + var fileName = $"{safeCode}.csv"; + var fullPath = Path.Combine(RawUseCsvPath, fileName); + + var config = new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = true, + }; + + using (var writer = new StreamWriter(fullPath, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true))) + using (var csv = new CsvWriter(writer, config)) + { + csv.Context.RegisterClassMap(); + + csv.WriteHeader(); + csv.NextRecord(); + csv.WriteRecord(data); + csv.NextRecord(); + } + + _logger.Info($"CSV 导出成功: {fullPath}"); + return fullPath; + } + catch (Exception ex) + { + _logger.Error(ex, "导出原料使用CSV失败。"); + throw new IOException("导出原料使用CSV失败。", ex); + } + } + + private static string SanitizeFileName(string name) + { + var invalid = Path.GetInvalidFileNameChars(); + var sb = new StringBuilder(name.Length); + foreach (var ch in name) + { + sb.Append(invalid.Contains(ch) ? '_' : ch); + } + var result = sb.ToString(); + return string.IsNullOrWhiteSpace(result) ? "RawUse" : result; + } + } +} diff --git a/FATrace.OEMApp/Services/DownloadTaskWorker.cs b/FATrace.OEMApp/Services/DownloadTaskWorker.cs index 21e5728..b55ff81 100644 --- a/FATrace.OEMApp/Services/DownloadTaskWorker.cs +++ b/FATrace.OEMApp/Services/DownloadTaskWorker.cs @@ -33,7 +33,30 @@ namespace FATrace.OEMApp.Services private HkCamera? _hk; // 供下载与事件回调使用的海康客户端(由 UI 注入) - private DownloadTaskWorker() { } + // 当前下载文件名变更事件(用于 UI 显示) + public event Action? DownloadFileNameChanged; + private void RaiseDownloadFileName(string name) + { + try { DownloadFileNameChanged?.Invoke(name); } catch { } + } + + // 任务状态事件(用于按需刷新 gridRULog) + public event Action? TaskStarted; + public event Action? TaskCompleted; + public event Action? TaskFailed; + private void RaiseTaskStarted(DownloadTask t) { try { TaskStarted?.Invoke(t); } catch { } } + private void RaiseTaskCompleted(DownloadTask t) { try { TaskCompleted?.Invoke(t); } catch { } } + private void RaiseTaskFailed(DownloadTask t, string? err) { try { TaskFailed?.Invoke(t, err); } catch { } } + + /// + /// 下载的视频时间 + /// + public static int VideoTime { get; set; } + + private DownloadTaskWorker() + { + VideoTime = ConfigHelper.GetIntOrDefault("VideoTime", 30); + } /// /// 启动下载队列后台循环。 @@ -105,10 +128,10 @@ namespace FATrace.OEMApp.Services /// NVR 下载结束时间(默认当前时间)。 /// 原料条码(可选)。未提供时默认等于 code。 /// 新增 DownloadTask 的自增 Id。 - public long Enqueue(string code, string rawName, string user, DateTime? start = null, DateTime? end = null, string? rawCode = null) + public long Enqueue(ParsedCodeInfo parsedCodeInfo, string user, DateTime? start = null, DateTime? end = null) { - if (string.IsNullOrWhiteSpace(code)) throw new ArgumentException("code 不能为空"); - if (string.IsNullOrWhiteSpace(rawName)) throw new ArgumentException("rawName 不能为空"); + if (parsedCodeInfo == null) throw new ArgumentException("code 不能为空"); + if (string.IsNullOrWhiteSpace(parsedCodeInfo.Code)) throw new ArgumentException("code 不能为空"); if (string.IsNullOrWhiteSpace(user)) throw new ArgumentException("user 不能为空"); var now = DateTime.Now; @@ -117,13 +140,13 @@ namespace FATrace.OEMApp.Services // - rawCode 未提供时回落到 code,避免非空列插入失败 var task = new DownloadTask { - Code = code, - RawName = rawName, - RawCode = rawCode ?? code, + Code = parsedCodeInfo.Code, + RawName = parsedCodeInfo.RawName, + RawCode = parsedCodeInfo.RawCode, User = user, Status = TaskStatus.Pending, Progress = 0, - NvrStartTime = start ?? now.AddSeconds(-30), + NvrStartTime = start ?? now.AddSeconds(-VideoTime), NvrEndTime = end ?? now, CreateTime = now, UpdateTime = now @@ -131,7 +154,7 @@ namespace FATrace.OEMApp.Services // 入库返回自增 Id,便于 UI 提示与后续跟踪 var id = FSqlContext.FDb.Insert(task).ExecuteIdentity(); - _logger.Info("[DownloadTaskWorker] 入队 DownloadTask: Id={Id}, Code={Code}", id, code); + _logger.Info("[DownloadTaskWorker] 入队 DownloadTask: Id={Id}, Code={Code}", id, parsedCodeInfo.Code); return id; } @@ -143,26 +166,27 @@ namespace FATrace.OEMApp.Services { while (!token.IsCancellationRequested) { + DownloadTask? next = null; try { - // 查询最早入队但未处理的任务 - var db = FSqlContext.FDb; - var next = await db.Select() - .Where(t => t.Status == TaskStatus.Pending) - .OrderBy(t => t.Id) - .FirstAsync(); - - if (next == null) - { - // 暂无任务,稍候再查 - await Task.Delay(5000, token); - continue; - } - - // 使用信号量确保同一时间仅有一个任务进入下载处理 await _singleRunner.WaitAsync(token); - // 通过 ContinueWith 在任务结束时释放信号量,避免阻塞主循环 - _ = ProcessTaskAsync(next, token).ContinueWith(_ => _singleRunner.Release()); + try + { + var db = FSqlContext.FDb; + next = await db.Select() + .Where(t => t.Status == TaskStatus.Pending && t.NvrEndTime < DateTime.Now.AddSeconds(5)) + .OrderBy(t => t.Id) + .FirstAsync(); + + if (next != null) + { + await ProcessTaskAsync(next, token); + } + } + finally + { + _singleRunner.Release(); + } } catch (OperationCanceledException) { @@ -171,12 +195,21 @@ namespace FATrace.OEMApp.Services catch (Exception ex) { _logger.Error(ex, "[DownloadTaskWorker] 主循环异常"); - try { await Task.Delay(2000, token); } catch { } + } + + if (next == null) + { + try { await Task.Delay(5000, token); } catch { } } } _logger.Info("[DownloadTaskWorker] 已停止"); } + /// + /// 当前处理中的 DownloadTask + /// + public DownloadTask CurDownloadTask { get; set; } + /// /// 实际执行单个下载任务: /// 1) 标记 Running,生成保存路径 @@ -186,39 +219,46 @@ namespace FATrace.OEMApp.Services /// /// 待处理的 DownloadTask。 /// 取消令牌。 - private async Task ProcessTaskAsync(DownloadTask t, CancellationToken token) + private async Task ProcessTaskAsync(DownloadTask downloadTask, CancellationToken token) { if (_hk == null) { - _logger.Warn("[DownloadTaskWorker] HkCamera 未初始化,跳过任务 Id={Id}", t.Id); + _logger.Warn("[DownloadTaskWorker] HkCamera 未初始化,跳过任务 Id={Id}", downloadTask.Id); return; } + CurDownloadTask = downloadTask; + var db = FSqlContext.FDb; // 步骤1:状态入库(Running/TryCount/Progress/UpdateTime) // 标记运行中 - t.Status = TaskStatus.Running; - t.TryCount += 1; - t.Progress = 0; - t.UpdateTime = DateTime.Now; + downloadTask.Status = TaskStatus.Running; + downloadTask.TryCount += 1; + downloadTask.Progress = 0; + downloadTask.UpdateTime = DateTime.Now; await db.Update() - .SetSource(t) + .SetSource(downloadTask) .UpdateColumns(a => new { a.Status, a.TryCount, a.Progress, a.UpdateTime }) - .Where(a => a.Id == t.Id) + .Where(a => a.Id == downloadTask.Id) .ExecuteAffrowsAsync(); + // 通知任务开始 + RaiseTaskStarted(downloadTask); + // 步骤2:生成保存路径(含安全文件名),并确保保存目录存在 // 生成本地文件名/路径 var saveBase = _hk.NVRVideoSavePath; if (string.IsNullOrWhiteSpace(saveBase)) { - _logger.Error("[DownloadTaskWorker] NVRVideoSavePath 为空,无法下载。任务 Id={Id}", t.Id); - await MarkFailedAsync(t, "NVRVideoSavePath 未配置"); + _logger.Error("[DownloadTaskWorker] NVRVideoSavePath 为空,无法下载。任务 Id={Id}", downloadTask.Id); + await MarkFailedAsync(downloadTask, "NVRVideoSavePath 未配置"); return; } - var filePath = NVRCom.GetVideoName(saveBase, t.Code ?? "CODE"); + var filePath = NVRCom.GetVideoPathName(saveBase, downloadTask.Code!); // 确保保存目录存在,避免 SDK 写文件失败 + + try { var dir = Path.GetDirectoryName(filePath); @@ -231,18 +271,26 @@ namespace FATrace.OEMApp.Services await db.Update() .Set(a => a.VideoFilePath, filePath) .Set(a => a.UpdateTime, DateTime.Now) - .Where(a => a.Id == t.Id) + .Where(a => a.Id == downloadTask.Id) .ExecuteAffrowsAsync(); + //当前的下载路径 + downloadTask.VideoFilePath = filePath; + + // 通知 UI 当前下载文件名 + RaiseDownloadFileName(Path.GetFileName(filePath) ?? string.Empty); + // 步骤3:订阅 SDK 事件 -> TCS 转换 // 事件 -> TCS // 订阅两个事件:进度与完成。进度事件写回数据库;完成事件用于唤醒等待 var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - + void OnProgress(object? s, short p) { try { + //实时更新下载进度 + //db.Update() // .Set(a => a.Progress, Math.Max((short)0, Math.Min((short)100, p))) // .Set(a => a.UpdateTime, DateTime.Now) @@ -259,7 +307,7 @@ namespace FATrace.OEMApp.Services } catch { } } - + _hk.NVRLoadVideoProcessEventHandler += OnProgress; _hk.NVRLoadVideoCompleteEventHandler += OnComplete; @@ -268,114 +316,136 @@ namespace FATrace.OEMApp.Services try { // 发起下载(按时间范围) - var res = _hk.Sdk_NET_DVR_GetFileByTime_V40(t.NvrStartTime, t.NvrEndTime, filePath); + var res = _hk.Sdk_NET_DVR_GetFileByTime_V40(downloadTask.NvrStartTime, downloadTask.NvrEndTime, filePath); if (!res.Result) { - await MarkFailedAsync(t, res.Msg); + await MarkFailedAsync(downloadTask, res.Msg); return; } - // HkCamera 内部会启动进度监控:此处等待完成事件触发(带超时) - var timeWindowSec = Math.Max(1, (int)(t.NvrEndTime - t.NvrStartTime).TotalSeconds); - var timeoutSec = ConfigHelper.GetIntOrDefault("DownloadTaskTimeoutSeconds", Math.Max(120, Math.Min(1800, timeWindowSec * 3))); + //下载过程中的监控事件 + // 计算这次下载的时间窗口长度(秒),用于推导合理的超时 + var timeWindowSec = Math.Max(1, (int)(downloadTask.NvrEndTime - downloadTask.NvrStartTime).TotalSeconds); + + // 计算总超时秒数:优先读配置 DownloadTaskTimeoutSeconds; + // 如果未配置,用时间窗口 * 3 的经验值,并限制在 [120, 1800] 区间,防止过短/过长。 + var timeoutSec = ConfigHelper.GetIntOrDefault( + "DownloadTaskTimeoutSeconds", + Math.Max(120, Math.Min(1800, timeWindowSec * 3)) + ); + + // completed 代表“下载完成”这个事件(由 tcs.TrySetResult 在回调里触发) Task completed = tcs.Task; + + // timeoutTask 是“超时定时器”,超时时间到会完成;同时受 token 取消影响 Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(timeoutSec), token); + + // 当外部取消(Stop/退出)时,主动让 tcs 进入取消态,避免一直等待 completed using (token.Register(() => { try { tcs.TrySetCanceled(); } catch { } })) { + // 等待“完成事件”或“超时”二者之一先发生 var finished = await Task.WhenAny(completed, timeoutTask); + + // 如果先等到了 timeoutTask,说明超时发生 if (finished == timeoutTask) { + // 尽力停止 SDK 的下载(防止后台还在跑) try { _hk.Sdk_NET_DVR_StopGetFile(); } catch { } - await MarkFailedAsync(t, $"下载超时({timeoutSec}s)"); - return; + // 将任务标记为失败(原因是超时),写入数据库状态 + await MarkFailedAsync(downloadTask, $"下载超时({timeoutSec}s)"); + return; // 结束本次任务处理 } } + // 能走到这里,表示 completed 先发生(下载完成事件被触发) + // 读取回调携带的完成消息(有些 SDK 会返回提示文本,例如“下载完成”) var completeMsg = await ((Task)tcs.Task); - // 依据完成事件消息判断成功/失败(包含“完成”视为成功) + + // 简单的成功判断:消息非空,并包含“完成”字样就算成功 var succeed = !string.IsNullOrWhiteSpace(completeMsg) && completeMsg.Contains("完成"); + + // 若判断不通过,视为失败,尽力停止下载并入库失败原因 if (!succeed) { try { _hk.Sdk_NET_DVR_StopGetFile(); } catch { } - await MarkFailedAsync(t, string.IsNullOrWhiteSpace(completeMsg) ? "下载失败" : completeMsg); + await MarkFailedAsync(downloadTask, string.IsNullOrWhiteSpace(completeMsg) ? "下载失败" : completeMsg); return; } + // 走到这里即认为下载成功,后续会继续执行文件校验与入库逻辑 + // 步骤5:文件有效性检查(存在且大小>0) // 文件有效性检查 try { if (!File.Exists(filePath) || new FileInfo(filePath).Length <= 0) { - await MarkFailedAsync(t, "下载文件不存在或为空"); + await MarkFailedAsync(downloadTask, "下载文件不存在或为空"); return; } } catch { // 文件系统异常 - await MarkFailedAsync(t, "下载文件验证异常"); + await MarkFailedAsync(downloadTask, "下载文件验证异常"); return; } - // 步骤6:入库——写 OEMRawUse 并创建 Jellyfin 监听任务 - // 下载完成:写 OEMRawUse 并创建 JellyfinMonitorTask - // 1) 插入 OEMRawUse(UrlState=false, VideoUrl 空) + //// 步骤6:入库(不再使用 Jellyfin 监听): + //// 1) 插入 VideoAction(保存本地视频元数据) + //var va = new VideoAction + //{ + // Code = downloadTask.Code, + // User = downloadTask.User, + // VideoFilePath = filePath, + // VideoName = Path.GetFileName(filePath) ?? string.Empty, + // StartTime = downloadTask.NvrStartTime, + // EndTime = downloadTask.NvrEndTime, + // CreateTime = DateTime.Now + //}; + //var videoActionId = db.Insert(va).ExecuteIdentity(); + + // 2) 插入 OEMRawUse(直接填充本地文件路径作为 VideoUrl,并置 UrlState=true) var rawUse = new OEMRawUse { - InBagCode = t.Code, - RawName = t.RawName, - RawCode = t.RawCode, - User = t.User, - UrlState = false, - VideoUrl = string.Empty, - CreateTime = DateTime.Now, - VideoActionId = 0 + InBagCode = downloadTask.Code, + RawName = downloadTask.RawName, + RawCode = downloadTask.RawCode, + User = downloadTask.User, + VideoStartTime = downloadTask.NvrStartTime, + VideoEndTime = downloadTask.NvrEndTime, + VideoFilePath = downloadTask.VideoFilePath, + VideoName = NVRCom.GetVideoName(downloadTask.Code!), }; var rawUseId = db.Insert(rawUse).ExecuteIdentity(); - // 2) 创建 Jellyfin 监听任务,交由 JellyfinMonitorQueueService 批量匹配 - var jfTask = new JellyfinMonitorTask - { - OemRawUseId = rawUseId, - LocalFileNameOrPath = filePath, - Code = t.Code, - RawName = t.RawName, - User = t.User, - NvrStartTime = t.NvrStartTime, - NvrEndTime = t.NvrEndTime, - Status = TaskStatus.Pending, - TryCount = 0, - CreateTime = DateTime.Now, - UpdateTime = DateTime.Now - }; - db.Insert(jfTask).ExecuteAffrows(); - // 步骤7:收尾——标记下载完成并记录日志 // 标记下载完成 - t.Status = TaskStatus.Completed; - t.Progress = 100; - t.UpdateTime = DateTime.Now; + downloadTask.Status = TaskStatus.Completed; + downloadTask.Progress = 100; + downloadTask.UpdateTime = DateTime.Now; await db.Update() - .SetSource(t) + .SetSource(downloadTask) .UpdateColumns(a => new { a.Status, a.Progress, a.UpdateTime }) - .Where(a => a.Id == t.Id) + .Where(a => a.Id == downloadTask.Id) .ExecuteAffrowsAsync(); - _logger.Info("[DownloadTaskWorker] 下载完成,已创建 Jellyfin 监控任务 DownloadTaskId={Id}, OEMRawUseId={RawUseId}", t.Id, rawUseId); + _logger.Info("[DownloadTaskWorker] 下载完成并入库:DownloadTaskId={Id}, OEMRawUseId={RawUseId}", downloadTask.Id, rawUseId); + // 通知任务完成 + RaiseTaskCompleted(downloadTask); } catch (OperationCanceledException) { // 取消:停止当前 SDK 下载并标记失败 try { _hk.Sdk_NET_DVR_StopGetFile(); } catch { } - await MarkFailedAsync(t, "任务被取消"); + await MarkFailedAsync(downloadTask, "任务被取消"); } catch (Exception ex) { - await MarkFailedAsync(t, ex.Message); + await MarkFailedAsync(downloadTask, ex.Message); } finally { @@ -383,6 +453,8 @@ namespace FATrace.OEMApp.Services try { _hk.NVRLoadVideoProcessEventHandler -= OnProgress; } catch { } try { _hk.NVRLoadVideoCompleteEventHandler -= OnComplete; } catch { } //_singleRunner.Release(); + // 清空 UI 显示(可选) + RaiseDownloadFileName(string.Empty); } } @@ -400,11 +472,13 @@ namespace FATrace.OEMApp.Services t.Error = error; t.UpdateTime = DateTime.Now; _logger.Warn("[DownloadTaskWorker] 任务失败 Id={Id}, 错误={Err}", t.Id, error); - return db.Update() + var task = db.Update() .SetSource(t) .UpdateColumns(a => new { a.Status, a.Error, a.UpdateTime }) .Where(a => a.Id == t.Id) .ExecuteAffrowsAsync(); + try { RaiseTaskFailed(t, error); } catch { } + return task; } } } diff --git a/FATrace.OEMApp/Services/JellyfinMonitorQueueService.cs b/FATrace.OEMApp/Services/JellyfinMonitorQueueService.cs index efc42f0..d070482 100644 --- a/FATrace.OEMApp/Services/JellyfinMonitorQueueService.cs +++ b/FATrace.OEMApp/Services/JellyfinMonitorQueueService.cs @@ -211,9 +211,9 @@ namespace FATrace.OEMApp.Services db.Update() .Set(a => new OEMRawUse { - UrlState = true, - VideoUrl = playUrl, - VideoActionId = actionId, + //UrlState = true, + //VideoUrl = playUrl, + //VideoActionId = actionId, RawName = t.RawName }) .Where(a => a.Id == t.OemRawUseId) diff --git a/FATrace.OEMApp/Services/JellyfinMonitorService.cs b/FATrace.OEMApp/Services/JellyfinMonitorService.cs index 98f708a..195d39c 100644 --- a/FATrace.OEMApp/Services/JellyfinMonitorService.cs +++ b/FATrace.OEMApp/Services/JellyfinMonitorService.cs @@ -150,9 +150,9 @@ namespace FATrace.OEMApp.Services db.Update() .Set(a => new OEMRawUse { - UrlState = true, - VideoUrl = playUrl, - VideoActionId = actionId, + //UrlState = true, + //VideoUrl = playUrl, + //VideoActionId = actionId, RawName = rawName }) .Where(a => a.Id == oemRawUseId) diff --git a/FATrace.OEMApp/Services/PLCDataService.cs b/FATrace.OEMApp/Services/PLCDataService.cs index a99c0d8..8ca9229 100644 --- a/FATrace.OEMApp/Services/PLCDataService.cs +++ b/FATrace.OEMApp/Services/PLCDataService.cs @@ -215,7 +215,13 @@ namespace FATrace.OEMApp.Services OperateResultPDAScanCode = KeyencePlcMcNet!.ReadString(PadCodeAddress, 40); if (OperateResultPDAScanCode.IsSuccess) { - ScanCode = RevData(OperateResultPDAScanCode.Content).Replace("\r", "").Replace("\n", "").Trim(); + //ScanCode = RevData(OperateResultPDAScanCode.Content).Replace("\r", "").Replace("\n", "").Replace("\0", "").Trim(); + ScanCode = (OperateResultPDAScanCode.Content).Replace("\r", "").Replace("\n", "").Replace("\0", "").Trim(); + PlcConnected = true; + } + else + { + PlcConnected = false; } } catch (OperationCanceledException) diff --git a/FATrace.OEMApp/Services/TimeClearDataService.cs b/FATrace.OEMApp/Services/TimeClearDataService.cs index 3d64be9..e242395 100644 --- a/FATrace.OEMApp/Services/TimeClearDataService.cs +++ b/FATrace.OEMApp/Services/TimeClearDataService.cs @@ -1,7 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using FATrace.Com; +using FATrace.Com; using FATrace.Model; using NLog; @@ -13,13 +10,23 @@ namespace FATrace.OEMApp.Services private CancellationTokenSource? _cts; private Task? _loopTask; private TimeSpan _runAt = new TimeSpan(2, 0, 0); - private int _retentionDays = 365; + + /// + /// 文件暂存保存的天数 + /// + private int FileRetentionDays = 365; private bool _enabled = true; + /// + /// 数据库保存的信息天数 + /// + private int DbRetentionDays = 180; public event Action? Info; public void Start() { + FileRetentionDays=ConfigHelper.GetIntOrDefault("VideoFileSaveDay", 365); + DbRetentionDays = ConfigHelper.GetIntOrDefault("DbSaveDay", 180); if (_cts != null) return; try @@ -30,7 +37,6 @@ namespace FATrace.OEMApp.Services { _runAt = new TimeSpan(2, 0, 0); } - _retentionDays = Math.Max(1, ConfigHelper.GetIntOrDefault("DataRetentionDays", 365)); } catch { } @@ -42,8 +48,8 @@ namespace FATrace.OEMApp.Services _cts = new CancellationTokenSource(); _loopTask = Task.Run(() => RunAsync(_cts.Token)); - _logger.Info("[TimeClear] 服务已启动,时间点={RunAt}, 保留天数={Days}", _runAt, _retentionDays); - SafeInfo($"定时清理服务启动,时间点={_runAt}, 保留天数={_retentionDays}"); + _logger.Info("[TimeClear] 服务已启动,时间点={RunAt}, 文件保留天数={FileDays}, 数据库保留天数={DbDays}", _runAt, FileRetentionDays, DbRetentionDays); + SafeInfo($"定时清理服务启动,时间点={_runAt}, 文件保留天数={FileRetentionDays}, 数据库保留天数={DbRetentionDays}"); } public void Stop() @@ -87,23 +93,62 @@ namespace FATrace.OEMApp.Services private async Task CleanupOnceAsync(CancellationToken token) { - var cutoff = DateTime.Now.AddDays(-_retentionDays); - _logger.Info("[TimeClear] 开始清理,截止时间: {Cutoff}", cutoff); - SafeInfo($"开始清理,截止时间: {cutoff:yyyy-MM-dd HH:mm:ss}"); + var fileCutoff = DateTime.Now.AddDays(-FileRetentionDays); + var dbCutoff = DateTime.Now.AddDays(-DbRetentionDays); + _logger.Info("[TimeClear] 开始清理,文件截止: {FileCutoff}, 数据库截止: {DbCutoff}", fileCutoff, dbCutoff); + SafeInfo($"开始清理,文件截止: {fileCutoff:yyyy-MM-dd HH:mm:ss}, 数据库截止: {dbCutoff:yyyy-MM-dd HH:mm:ss}"); + + long delJf = 0, delDl = 0, delRaw = 0, delAct = 0, delDlFiles = 0; - long delJf = 0, delDl = 0, delRaw = 0, delAct = 0; try { - delJf = FSqlContext.FDb.Delete().Where(a => a.CreateTime < cutoff).ExecuteAffrows(); + var toDelFile = FSqlContext.FDb.Select() + .Where(a => a.CreateTime < fileCutoff && a.VideoFilePath != null && a.VideoFilePath != "") + .ToList(); + foreach (var t in toDelFile) + { + try + { + if (!string.IsNullOrWhiteSpace(t.VideoFilePath) && File.Exists(t.VideoFilePath)) + { + File.Delete(t.VideoFilePath); + delDlFiles++; + } + } + catch (Exception ex) + { + _logger.Warn(ex, "[TimeClear] 删除 DownloadTask 文件失败: {Path}", t.VideoFilePath); + } + } } catch (Exception ex) { - _logger.Warn(ex, "[TimeClear] 清理 JellyfinMonitorTask 失败"); + _logger.Warn(ex, "[TimeClear] 扫描 DownloadTask 待删文件失败"); } try { - delDl = FSqlContext.FDb.Delete().Where(a => a.CreateTime < cutoff).ExecuteAffrows(); + var dbOld = FSqlContext.FDb.Select() + .Where(a => a.CreateTime < dbCutoff) + .ToList(); + foreach (var t in dbOld) + { + try + { + if (!string.IsNullOrWhiteSpace(t.VideoFilePath) && File.Exists(t.VideoFilePath)) + { + File.Delete(t.VideoFilePath); + delDlFiles++; + } + } + catch (Exception ex) + { + _logger.Warn(ex, "[TimeClear] 删除 DownloadTask 文件失败(删库前): {Path}", t.VideoFilePath); + } + } + delDl = FSqlContext.FDb.Delete() + .Where(a => a.CreateTime < dbCutoff) + .ExecuteAffrows(); } catch (Exception ex) { @@ -112,7 +157,16 @@ namespace FATrace.OEMApp.Services try { - delRaw = FSqlContext.FDb.Delete().Where(a => a.CreateTime < cutoff).ExecuteAffrows(); + delJf = FSqlContext.FDb.Delete().Where(a => a.CreateTime < dbCutoff).ExecuteAffrows(); + } + catch (Exception ex) + { + _logger.Warn(ex, "[TimeClear] 清理 JellyfinMonitorTask 失败"); + } + + try + { + delRaw = FSqlContext.FDb.Delete().Where(a => a.CreateTime < dbCutoff).ExecuteAffrows(); } catch (Exception ex) { @@ -121,15 +175,15 @@ namespace FATrace.OEMApp.Services try { - delAct = FSqlContext.FDb.Delete().Where(a => a.CreateTime < cutoff).ExecuteAffrows(); + delAct = FSqlContext.FDb.Delete().Where(a => a.CreateTime < dbCutoff).ExecuteAffrows(); } catch (Exception ex) { _logger.Warn(ex, "[TimeClear] 清理 VideoAction 失败"); } - _logger.Info("[TimeClear] 清理完成: JellyfinMonitorTask={Jf}, DownloadTask={Dl}, OEMRawUse={Raw}, VideoAction={Act}", delJf, delDl, delRaw, delAct); - SafeInfo($"清理完成: Jf={delJf}, Dl={delDl}, Raw={delRaw}, Act={delAct}"); + _logger.Info("[TimeClear] 清理完成: JellyfinMonitorTask={Jf}, DownloadTask(删库)={Dl}, DownloadTask(删文件)={DlFiles}, OEMRawUse={Raw}, VideoAction={Act}", delJf, delDl, delDlFiles, delRaw, delAct); + SafeInfo($"清理完成: Jf={delJf}, Dl(库)={delDl}, Dl(文件)={delDlFiles}, Raw={delRaw}, Act={delAct}"); await Task.CompletedTask; }