初步版本251204

This commit is contained in:
2025-12-04 18:39:34 +08:00
parent cd1ec78a11
commit 9dd458ae8b
17 changed files with 1089 additions and 522 deletions

View File

@@ -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
/// </summary>
/// <param name="configKey"></param>
/// <returns></returns>
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";
}
/// <summary>
/// 获取视频名称
/// </summary>
/// <returns></returns>
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";
}
/// <summary>
/// 解析条码
/// </summary>
/// <returns></returns>
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.21234 => 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;
}
}
}

View File

@@ -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
{
/// <summary>
/// 直接的条码
/// 从这个条码中解析出其他信息
/// </summary>
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; }
}
}

View File

@@ -3,8 +3,8 @@ using FreeSql.DataAnnotations;
namespace FATrace.Model
{
/// <summary>
/// 下载任务实体(持久化队列项)
/// 表示一条从海康 NVR 下载视频的计划与执行状态
/// 下载任务实体(持久化队列项)
/// 表示一条从海康 NVR 下载视频的计划与执行状态
/// </summary>
[Table(Name = "DownloadTask")]
public class DownloadTask
@@ -25,8 +25,8 @@ namespace FATrace.Model
/// <summary>
/// 原料名称
/// </summary>
[Column(StringLength = 100, IsNullable = false)]
public string? RawName { get; set; }
[Column(StringLength = 100, IsNullable = true)]
public string? RawName { get; set; }="";
/// <summary>
/// 原料条码
@@ -62,7 +62,7 @@ namespace FATrace.Model
/// 失败时的错误信息
/// </summary>
[Column(StringLength = 500)]
public string? Error { get; set; }
public string? Error { get; set; }="";
/// <summary>
/// 已尝试次数(每次 Running 前加一)

View File

@@ -33,10 +33,34 @@ namespace FATrace.Model
public string? RawCode { get; set; }
/// <summary>
/// 视频链接
/// Video 开始时间(默认当前时间 - 30秒
/// </summary>
[Column(Name = "VideoUrl", IsNullable = false, StringLength = 500)]
public string? VideoUrl{ get; set; }
[Column(IsNullable = false)]
public DateTime VideoStartTime { get; set; }
/// <summary>
/// Video 结束时间(默认当前时间)
/// </summary>
[Column(IsNullable = false)]
public DateTime VideoEndTime { get; set; }
/// <summary>
/// 视频本地保存路径
/// </summary>
[Column(Name = "VideoFilePath", IsNullable = false, StringLength = 500)]
public string? VideoFilePath { get; set; }
/// <summary>
/// 视频名称
/// </summary>
[Column(Name = "VideoName", IsNullable = false, StringLength = 100)]
public string? VideoName { get; set; }
///// <summary>
///// 视频链接
///// </summary>
//[Column(Name = "VideoUrl", IsNullable = false, StringLength = 500)]
//public string? VideoUrl{ get; set; }
/// <summary>
/// 用户信息
@@ -44,11 +68,11 @@ namespace FATrace.Model
[Column(Name = "User", IsNullable = false, StringLength = 100)]
public string? User { get; set; }
/// <summary>
/// URl状态
/// </summary>
[Column(Name = "UrlState", IsNullable = false)]
public bool UrlState { get; set; }
///// <summary>
///// URl状态
///// </summary>
//[Column(Name = "UrlState", IsNullable = false)]
//public bool UrlState { get; set; }
///// <summary>
///// 视频信息
@@ -62,15 +86,15 @@ namespace FATrace.Model
[Column(ServerTime = DateTimeKind.Local, CanUpdate = true)]
public DateTime CreateTime { get; set; }
/// <summary>
/// ///////////////////////////////////////////导航属性 LIN 一对一///////////////////////////////////////////////////////
/// </summary>
public long VideoActionId { get; set; } // 外键字段,必要
///// <summary>
///// ///////////////////////////////////////////导航属性 LIN 一对一///////////////////////////////////////////////////////
///// </summary>
//public long VideoActionId { get; set; } // 外键字段,必要
/// <summary>
/// 视频信息
/// </summary>
public VideoAction? VideoAction { get; set; }
///// <summary>
///// 视频信息
///// </summary>
//public VideoAction? VideoAction { get; set; }
}
}

View File

@@ -10,13 +10,17 @@
<add key="PLCScan" value="600" />
<add key="PDAScanCode" value="D1000" />
<add key="DownloadTaskMaxRetry" value="3" />
<add key="VideoTime" value="30" />
<add key="VideoFileSaveDay" value="365" />
<add key="DbSaveDay" value="180" />
<add key="NVRIP" value="192.168.0.15" />
<add key="NVRPort" value="8000" />
<add key="NVRUserName" value="admin" />
<add key="NVRPw" value="glico@2025" />
<add key="NVRVideoSavePath" value="Y:\" />
<add key="NVRVideoSavePath" value="Y:" />
<add key="InsCheckPort" value="COM10" />
<add key="InsCheckRate" value="9600" />
<add key="RawUseCsvPath" value="D:\TestData" />
<!-- Jellyfin 配置 -->
<add key="JellyfinBaseUrl" value="http://192.168.0.30:8096" />
<add key="JellyfinApiKey" value="f4285acda9f94efca46a6e4d0a0273c0" />

View File

@@ -422,6 +422,7 @@
<PackageReference Include="ReaLTaiizor" Version="3.8.1.3" />
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.21" />
<PackageReference Include="RestSharp" Version="110.2.0" />
<PackageReference Include="CsvHelper" Version="33.1.0" />
</ItemGroup>
<ItemGroup>

View File

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

View File

@@ -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
/// </summary>
private PLCDataService PLCDataService { get; set; }
private TimeClearDataService TimeClearService { get; set; }
private System.Windows.Forms.Timer _statusTimer;
/// <summary>
/// 主窗体加载:
@@ -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
/// <param name="Code"></param>
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}");
}
/// <summary>
/// PLC数据服务PLC连接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <summary>
/// PLC数据服务PLC连接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PLCDataService_PlcConnectedEventHandler(object? sender, string e)
{
LogInfo($"PLC连接: {e}");
var ok = string.Equals(e, "OK", StringComparison.OrdinalIgnoreCase);
SafeSetStatus(tslPlcConnection, ok, "Plc连接状态");
}
private void NVRLogin()
@@ -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
/// <summary>
/// 内包条码
/// </summary>
public string CurInBagCode { get; set; } = "AAAAAA";
///// <summary>
///// 内包条码
///// </summary>
//public string CurInBagCode { get; set; } = "AAAAAA";
///// <summary>
///// 内包条码 原料名称
///// </summary>
//public string CurRawName { get; set; } = "添加剂Test";
///// <summary>
///// 内包条码 原料代码
///// </summary>
//public string CurRawCode { get; set; } = "ASASASS";
/// <summary>
/// 内包条码 原料名称
/// 内包条码 解析结果
/// </summary>
public string CurInBagRawName { get; set; } = "添加剂Test";
public ParsedCodeInfo CurParsedCodeInfo { get; set; }
#endregion
@@ -282,25 +320,6 @@ namespace FATrace.OEMApp
return;
}
/// <summary>
/// 下载按钮:仅将请求入队为 DownloadTask由后台 DownloadTaskWorker 顺序处理。
/// 注意:不会在此处直接调用 SDK 下载,也不会订阅 SDK 事件,避免并发与重复。
/// </summary>
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}");
}
/// <summary>
@@ -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<OEMRawUse>(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<RuLogRow> 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();
}
/// <summary>
/// 启动 UI 定时器,定时刷新下载与 Jellyfin 监听任务状态
/// </summary>
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();
}
// 定时刷新已移除:仅在下载开始/完成/失败时按需更新
/// <summary>
/// 刷新 gridRULog下载 + 监听
/// 刷新 gridRULog仅当天下载)
/// </summary>
private void RefreshRuLogGrid()
{
try
{
var db = FSqlContext.FDb;
var today = DateTime.Today;
var downloads = db.Select<DownloadTask>()
.Where(a => a.UpdateTime >= today)
.OrderByDescending(a => a.UpdateTime)
.Limit(50)
.ToList();
var monitors = db.Select<JellyfinMonitorTask>()
.OrderByDescending(a => a.UpdateTime)
.Limit(50)
.ToList();
var rows = new List<RuLogRow>(downloads.Count + monitors.Count);
var rows = new List<RuLogRow>(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);
}
/// <summary>
/// 当前下载文件的名称改变
/// </summary>
/// <param name="name"></param>
private void OnDownloadFileNameChanged(string name)
{
try
{
if (DownloadFileName == null || DownloadFileName.IsDisposed) return;
if (InvokeRequired)
{
try { BeginInvoke(new Action(() => DownloadFileName.Text = name ?? string.Empty)); } catch { }
}
else
{
try { BeginInvoke(new Action(() => DownloadFileName.Text = name ?? string.Empty)); } catch { }
}
}
catch { }
}
#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
}
}

View File

@@ -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

View File

@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FATrace.OEMApp.Model
{
/// <summary>
/// 原材料使用CSV保存信息
/// </summary>
public class RawUseCsvDto
{
/// <summary>
/// 原料编号
/// </summary>
public string? RawCode { get; set; }
/// <summary>
/// 原料名称
/// </summary>
public string? RawName { get; set; }
/// <summary>
/// 内袋二维码
/// </summary>
public string? InBagCode { get; set; }
///// <summary>
///// 外箱二维码
///// </summary>
//public string? BoxCode { get; set; }
/// <summary>
/// 操作者
/// </summary>
public string? OpUser { get; set; }
/// <summary>
/// 视频保存路径
/// </summary>
public string? VideoSavePath { get; set; }
/// <summary>
/// 原料使用时间
/// </summary>
public DateTime UseTime { get; set; }
}
}

View File

@@ -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<RawUseCsvDto>
{
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");
}
}
}

View File

@@ -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");
}
/// <summary>
/// 原料使用信息CSV文件路径
/// </summary>
public string RawUseCsvPath { get; set; }
/// <summary>
/// 将一条原料使用记录导出为单独CSV文件包含表头
/// 文件保存目录来自 RawUseCsvPath文件名包含时间戳与内袋二维码若有
/// </summary>
/// <param name="data">原料使用记录</param>
/// <returns>生成的CSV完整路径</returns>
/// <exception cref="ArgumentNullException">当 data 为空时抛出</exception>
/// <exception cref="InvalidOperationException">当 RawUseCsvPath 未配置时抛出</exception>
/// <exception cref="IOException">当写入文件失败时抛出</exception>
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<RawUseCsvDtoMap>();
csv.WriteHeader<RawUseCsvDto>();
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;
}
}
}

View File

@@ -33,7 +33,30 @@ namespace FATrace.OEMApp.Services
private HkCamera? _hk; // 供下载与事件回调使用的海康客户端(由 UI 注入)
private DownloadTaskWorker() { }
// 当前下载文件名变更事件(用于 UI 显示)
public event Action<string>? DownloadFileNameChanged;
private void RaiseDownloadFileName(string name)
{
try { DownloadFileNameChanged?.Invoke(name); } catch { }
}
// 任务状态事件(用于按需刷新 gridRULog
public event Action<DownloadTask>? TaskStarted;
public event Action<DownloadTask>? TaskCompleted;
public event Action<DownloadTask, string?>? 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 { } }
/// <summary>
/// 下载的视频时间
/// </summary>
public static int VideoTime { get; set; }
private DownloadTaskWorker()
{
VideoTime = ConfigHelper.GetIntOrDefault("VideoTime", 30);
}
/// <summary>
/// 启动下载队列后台循环。
@@ -105,10 +128,10 @@ namespace FATrace.OEMApp.Services
/// <param name="end">NVR 下载结束时间(默认当前时间)。</param>
/// <param name="rawCode">原料条码(可选)。未提供时默认等于 code。</param>
/// <returns>新增 DownloadTask 的自增 Id。</returns>
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<DownloadTask>(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<DownloadTask>()
.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<DownloadTask>()
.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] 已停止");
}
/// <summary>
/// 当前处理中的 DownloadTask
/// </summary>
public DownloadTask CurDownloadTask { get; set; }
/// <summary>
/// 实际执行单个下载任务:
/// 1) 标记 Running生成保存路径
@@ -186,39 +219,46 @@ namespace FATrace.OEMApp.Services
/// </summary>
/// <param name="t">待处理的 DownloadTask。</param>
/// <param name="token">取消令牌。</param>
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<DownloadTask>()
.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,9 +271,15 @@ namespace FATrace.OEMApp.Services
await db.Update<DownloadTask>()
.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
// 订阅两个事件:进度与完成。进度事件写回数据库;完成事件用于唤醒等待
@@ -243,6 +289,8 @@ namespace FATrace.OEMApp.Services
{
try
{
//实时更新下载进度
//db.Update<DownloadTask>()
// .Set(a => a.Progress, Math.Max((short)0, Math.Min((short)100, p)))
// .Set(a => a.UpdateTime, DateTime.Now)
@@ -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<string>)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) 插入 OEMRawUseUrlState=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<VideoAction>(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<OEMRawUse>(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<JellyfinMonitorTask>(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<DownloadTask>()
.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<DownloadTask>()
var task = db.Update<DownloadTask>()
.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;
}
}
}

View File

@@ -211,9 +211,9 @@ namespace FATrace.OEMApp.Services
db.Update<OEMRawUse>()
.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)

View File

@@ -150,9 +150,9 @@ namespace FATrace.OEMApp.Services
db.Update<OEMRawUse>()
.Set(a => new OEMRawUse
{
UrlState = true,
VideoUrl = playUrl,
VideoActionId = actionId,
//UrlState = true,
//VideoUrl = playUrl,
//VideoActionId = actionId,
RawName = rawName
})
.Where(a => a.Id == oemRawUseId)

View File

@@ -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)

View File

@@ -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;
/// <summary>
/// 文件暂存保存的天数
/// </summary>
private int FileRetentionDays = 365;
private bool _enabled = true;
/// <summary>
/// 数据库保存的信息天数
/// </summary>
private int DbRetentionDays = 180;
public event Action<string>? 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<JellyfinMonitorTask>().Where(a => a.CreateTime < cutoff).ExecuteAffrows();
var toDelFile = FSqlContext.FDb.Select<DownloadTask>()
.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<DownloadTask>().Where(a => a.CreateTime < cutoff).ExecuteAffrows();
var dbOld = FSqlContext.FDb.Select<DownloadTask>()
.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<DownloadTask>()
.Where(a => a.CreateTime < dbCutoff)
.ExecuteAffrows();
}
catch (Exception ex)
{
@@ -112,7 +157,16 @@ namespace FATrace.OEMApp.Services
try
{
delRaw = FSqlContext.FDb.Delete<OEMRawUse>().Where(a => a.CreateTime < cutoff).ExecuteAffrows();
delJf = FSqlContext.FDb.Delete<JellyfinMonitorTask>().Where(a => a.CreateTime < dbCutoff).ExecuteAffrows();
}
catch (Exception ex)
{
_logger.Warn(ex, "[TimeClear] 清理 JellyfinMonitorTask 失败");
}
try
{
delRaw = FSqlContext.FDb.Delete<OEMRawUse>().Where(a => a.CreateTime < dbCutoff).ExecuteAffrows();
}
catch (Exception ex)
{
@@ -121,15 +175,15 @@ namespace FATrace.OEMApp.Services
try
{
delAct = FSqlContext.FDb.Delete<VideoAction>().Where(a => a.CreateTime < cutoff).ExecuteAffrows();
delAct = FSqlContext.FDb.Delete<VideoAction>().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;
}