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;
}