From cd1ec78a11daa02df5038936dca6c2eb52173513 Mon Sep 17 00:00:00 2001 From: Tyrone CT Date: Wed, 3 Dec 2025 15:58:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E7=BA=A7=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FATrace.Com/NVRCom.cs | 15 +- FATrace.HKNetLib/Wrapper/HkCamera.cs | 5 +- FATrace.Model/DownloadTask.cs | 2 +- FATrace.OEMApp/MainApp.Designer.cs | 15 +- FATrace.OEMApp/MainApp.cs | 109 +++++++++++++- FATrace.OEMApp/MainApp.resx | 110 +++++++------- FATrace.OEMApp/Services/DownloadTaskWorker.cs | 114 +++++++++++--- .../Services/TimeClearDataService.cs | 142 ++++++++++++++++++ 8 files changed, 425 insertions(+), 87 deletions(-) create mode 100644 FATrace.OEMApp/Services/TimeClearDataService.cs diff --git a/FATrace.Com/NVRCom.cs b/FATrace.Com/NVRCom.cs index 325aacd..0f69804 100644 --- a/FATrace.Com/NVRCom.cs +++ b/FATrace.Com/NVRCom.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using System.IO; namespace FATrace.Com { @@ -16,9 +17,19 @@ namespace FATrace.Com /// /// /// - public static string GetVideoName(string Path,string Code) + public static string GetVideoName(string basePath,string Code) { - return $"{Path}\\{DateTime.Now.ToString("yyyy-MM-dd HHmmss")} {Code}.mp4"; + // 清洗非法文件名字符,避免保存失败 + 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 = "CODE"; + } + catch { } + + return $"{basePath}\\{DateTime.Now.ToString("yyyy-MM-dd HHmmss")} {safeCode}.mp4"; } } } diff --git a/FATrace.HKNetLib/Wrapper/HkCamera.cs b/FATrace.HKNetLib/Wrapper/HkCamera.cs index a3c9215..bc2f2d0 100644 --- a/FATrace.HKNetLib/Wrapper/HkCamera.cs +++ b/FATrace.HKNetLib/Wrapper/HkCamera.cs @@ -3,6 +3,7 @@ using FATrace.HKNetLib.Hardware; using FATrace.Model; using System.Reflection; using System.Runtime.InteropServices; +using System.IO; using static FATrace.HKNetLib.Hardware.CHCNetSDK; using System.Threading; using System.Threading.Tasks; @@ -445,15 +446,17 @@ namespace FATrace.HKNetLib.Wrapper { // 发生错误,尝试停止下载并退出 Sdk_NET_DVR_StopGetFile(); + try { NVRLoadVideoCompleteEventHandler?.Invoke(this, "下载失败"); } catch { } break; } try { await Task.Delay(_downloadPollIntervalMs, token); } catch { /* ignore */ } } } - catch + catch (Exception ex) { // 轮询线程异常吞掉,避免影响上层逻辑 + try { NVRLoadVideoCompleteEventHandler?.Invoke(this, $"下载失败:{ex.Message}"); } catch { } } }, token); } diff --git a/FATrace.Model/DownloadTask.cs b/FATrace.Model/DownloadTask.cs index b94e7c6..df006fb 100644 --- a/FATrace.Model/DownloadTask.cs +++ b/FATrace.Model/DownloadTask.cs @@ -59,7 +59,7 @@ namespace FATrace.Model public string? VideoFilePath { get; set; } /// - /// 失败时的错误信息。 + /// 失败时的错误信息 /// [Column(StringLength = 500)] public string? Error { get; set; } diff --git a/FATrace.OEMApp/MainApp.Designer.cs b/FATrace.OEMApp/MainApp.Designer.cs index 1684c00..fd89ead 100644 --- a/FATrace.OEMApp/MainApp.Designer.cs +++ b/FATrace.OEMApp/MainApp.Designer.cs @@ -85,6 +85,7 @@ namespace FATrace.OEMApp btnStopLoadVideo = new Button(); btnLoadVideo = new Button(); btnNVRLogin = new Button(); + LvLog = new ListView(); materialTabControl1.SuspendLayout(); tabPage1.SuspendLayout(); materialCard4.SuspendLayout(); @@ -364,6 +365,7 @@ namespace FATrace.OEMApp // materialCard3 // materialCard3.BackColor = Color.FromArgb(255, 255, 255); + materialCard3.Controls.Add(LvLog); materialCard3.Controls.Add(label8); materialCard3.Depth = 0; materialCard3.ForeColor = Color.FromArgb(222, 0, 0, 0); @@ -382,9 +384,9 @@ namespace FATrace.OEMApp label8.ForeColor = Color.FromArgb(64, 64, 64); label8.Location = new Point(17, 14); label8.Name = "label8"; - label8.Size = new Size(180, 28); + label8.Size = new Size(138, 28); label8.TabIndex = 5; - label8.Text = "原料使用历史信息"; + label8.Text = "系统运行日志"; // // materialCard2 // @@ -722,6 +724,14 @@ 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); @@ -816,5 +826,6 @@ namespace FATrace.OEMApp private ProgressBar DownloadProgressBarMain; private Button btnRawStopLoadVideo; private DataGridView gridRULog; + private ListView LvLog; } } \ No newline at end of file diff --git a/FATrace.OEMApp/MainApp.cs b/FATrace.OEMApp/MainApp.cs index 04c2fec..1757625 100644 --- a/FATrace.OEMApp/MainApp.cs +++ b/FATrace.OEMApp/MainApp.cs @@ -37,6 +37,57 @@ namespace FATrace.OEMApp { nameof(VideoAction.CreateTime), "创建时间" } }; + private int _lvLogMaxItems = 1000; + private int _lastProgressLogged = -1; + + private void InitLvLog() + { + try + { + LvLog.BeginUpdate(); + LvLog.Clear(); + LvLog.View = View.Details; + LvLog.FullRowSelect = true; + LvLog.GridLines = true; + LvLog.HeaderStyle = ColumnHeaderStyle.Nonclickable; + LvLog.Columns.Add("时间", 150); + LvLog.Columns.Add("级别", 60); + LvLog.Columns.Add("消息", 720); + } + finally + { + LvLog.EndUpdate(); + } + } + + private void AppendLog(string level, string message, Color? color = null) + { + if (LvLog == null || LvLog.IsDisposed) return; + if (InvokeRequired) + { + try { BeginInvoke(new Action(() => AppendLog(level, message, color))); } catch { } + return; + } + try + { + var item = new ListViewItem(DateTime.Now.ToString("HH:mm:ss.fff")); + item.SubItems.Add(level); + item.SubItems.Add(message ?? string.Empty); + if (color.HasValue) item.ForeColor = color.Value; + LvLog.Items.Add(item); + try { LvLog.EnsureVisible(LvLog.Items.Count - 1); } catch { } + if (LvLog.Items.Count > _lvLogMaxItems) + { + for (int i = 0; i < 200; i++) { if (LvLog.Items.Count == 0) break; LvLog.Items.RemoveAt(0); } + } + } + catch { } + } + + private void LogInfo(string msg) => AppendLog("INFO", msg, Color.FromArgb(33, 33, 33)); + private void LogWarn(string msg) => AppendLog("WARN", msg, Color.DarkOrange); + private void LogError(string msg) => AppendLog("ERROR", msg, Color.DarkRed); + public MainApp() { InitializeComponent(); @@ -46,6 +97,7 @@ namespace FATrace.OEMApp /// PLC数据服务 /// private PLCDataService PLCDataService { get; set; } + private TimeClearDataService TimeClearService { get; set; } /// /// 主窗体加载: @@ -56,10 +108,18 @@ namespace FATrace.OEMApp /// private void MainApp_Load(object sender, EventArgs e) { - + InitLvLog(); + LogInfo("主界面初始化"); HkCameraClient = new HkCamera(); //保存SDK日志 CHCNetSDK.NET_DVR_SetLogToFile(3, "C:\\SdkLog\\", true); + LogInfo("已启用海康SDK日志输出 C:\\SdkLog\\"); + + try + { + HkCameraClient.NVRLoadVideoProcessEventHandler += HkCameraClient_NVRLoadVideoProcessEventHandler; + } + catch { } //读取配置 //HkCameraClient.NVR_IP = ConfigHelper.GetValue("NVRIP"); @@ -71,6 +131,7 @@ namespace FATrace.OEMApp PLCDataService.ScanCodeEventHandler += PLCDataService_ScanCodeEventHandler; HkCameraClient.NVRVideoSavePath = ConfigHelper.GetValue("NVRVideoSavePath"); + //NVR登录 NVRLogin(); @@ -81,11 +142,18 @@ namespace FATrace.OEMApp try { DownloadTaskWorker.Instance.Start(HkCameraClient); + LogInfo("下载队列服务已启动"); JellyfinMonitorQueueService.Instance.Start(); + LogInfo("Jellyfin 监控服务已启动"); + TimeClearService = new TimeClearDataService(); + TimeClearService.Info += (m) => LogInfo($"[清理]{m}"); + TimeClearService.Start(); + LogInfo("定时清理服务已启动"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[TaskServices] 启动失败: {ex.Message}"); + LogError($"后台服务启动失败: {ex.Message}"); } // 初始化 gridRULog 并启动 UI 定时刷新 @@ -115,6 +183,7 @@ namespace FATrace.OEMApp catch (Exception ex) { MessageBox.Show($"初始化界面失败: {ex.Message}"); + LogError($"初始化界面失败: {ex.Message}"); } } @@ -125,7 +194,7 @@ namespace FATrace.OEMApp /// private void PLCDataService_ScanCodeEventHandler(object? sender, string Code) { - + LogInfo($"扫码: {Code}"); } /// @@ -135,7 +204,7 @@ namespace FATrace.OEMApp /// private void PLCDataService_PlcConnectedEventHandler(object? sender, string e) { - + LogInfo($"PLC连接: {e}"); } private void NVRLogin() @@ -145,13 +214,16 @@ namespace FATrace.OEMApp HkCameraClient.NVR_UserName = ConfigHelper.GetValue("NVRUserName"); HkCameraClient.NVR_Pw = ConfigHelper.GetValue("NVRPw"); + LogInfo($"尝试登录NVR {HkCameraClient.NVR_IP}:{HkCameraClient.NVR_Port} 用户 {HkCameraClient.NVR_UserName}"); var result = HkCameraClient.Sdk_NET_DVR_Login_V30(HkCameraClient.NVR_IP, HkCameraClient.NVR_Port, HkCameraClient.NVR_UserName, HkCameraClient.NVR_Pw); if (result) { MessageBox.Show("登录成功"); + LogInfo("NVR 登录成功"); return; } MessageBox.Show($"登录失败:{HkCameraClient.LastMsgErr}"); + LogError($"NVR 登录失败: {HkCameraClient.LastMsgErr}"); return; } @@ -197,13 +269,16 @@ namespace FATrace.OEMApp HkCameraClient.NVR_UserName = ConfigHelper.GetValue("NVRUserName"); HkCameraClient.NVR_Pw = ConfigHelper.GetValue("NVRPw"); + LogInfo($"尝试登录NVR {HkCameraClient.NVR_IP}:{HkCameraClient.NVR_Port} 用户 {HkCameraClient.NVR_UserName}"); var result = HkCameraClient.Sdk_NET_DVR_Login_V30(HkCameraClient.NVR_IP, HkCameraClient.NVR_Port, HkCameraClient.NVR_UserName, HkCameraClient.NVR_Pw); if (result) { MessageBox.Show("登录成功"); + LogInfo("NVR 登录成功"); return; } MessageBox.Show($"登录失败:{HkCameraClient.LastMsgErr}"); + LogError($"NVR 登录失败: {HkCameraClient.LastMsgErr}"); return; } @@ -219,10 +294,11 @@ namespace FATrace.OEMApp code: CurInBagCode, rawName: CurInBagRawName, user: CurUserName, - start: DateTime.Now.AddMinutes(-5), + 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) @@ -231,6 +307,11 @@ namespace FATrace.OEMApp { DownloadProgressBarMain.Value = value; })); + if (value >= 100 && _lastProgressLogged != 100) + { + _lastProgressLogged = 100; + LogInfo("下载进度 100%"); + } } private void btnDownloadName_Click(object sender, EventArgs e) @@ -287,10 +368,12 @@ namespace FATrace.OEMApp if (Result.Result) { MessageBox.Show($"[暂停成功] :{Result.Msg}"); + LogInfo($"下载暂停: {Result.Msg}"); } else { MessageBox.Show($"[暂停失败] :{Result.Msg}"); + LogWarn($"下载暂停失败: {Result.Msg}"); } } @@ -512,7 +595,7 @@ namespace FATrace.OEMApp code: CurInBagCode, rawName: CurInBagRawName, user: CurUserName, - start: DateTime.Now.AddMinutes(-5), + start: DateTime.Now.AddSeconds(-100), end: DateTime.Now ); MessageBox.Show($"[Test] 已入队下载任务,Id={taskId}"); @@ -526,10 +609,16 @@ namespace FATrace.OEMApp private void HkCameraClient_NVRLoadVideoCompleteEventHandler(object? sender, string e) { // 计算用于去重的 key(文件路径或文件名,小写) + if (string.IsNullOrWhiteSpace(e) && string.IsNullOrWhiteSpace(this.CurrentVideoPath)) + { + LogWarn("下载完成事件未提供文件名,忽略"); + return; + } var localNameOrPath = (!string.IsNullOrWhiteSpace(e) && (e.Contains("\\") || e.Contains("/") || e.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase))) ? e : this.CurrentVideoPath; - var key = (System.IO.Path.GetFileName(localNameOrPath) ?? localNameOrPath).ToLowerInvariant(); + var safePath = localNameOrPath ?? string.Empty; + var key = (System.IO.Path.GetFileName(safePath) ?? safePath).ToLowerInvariant(); // 若同一 key 已在处理中,则忽略本次回调 if (!_downloadProcessingKeys.TryAdd(key, 0)) @@ -538,6 +627,8 @@ namespace FATrace.OEMApp return; } + LogInfo($"下载完成: {localNameOrPath}"); + // 先保存当前的信息,记录主键 Id var rawUse = new OEMRawUse() { @@ -853,5 +944,11 @@ namespace FATrace.OEMApp { HkCameraClient.Sdk_NET_DVR_StopGetFile(); } + + protected override void OnFormClosing(FormClosingEventArgs e) + { + try { TimeClearService?.Stop(); } catch { } + base.OnFormClosing(e); + } } } diff --git a/FATrace.OEMApp/MainApp.resx b/FATrace.OEMApp/MainApp.resx index c83729e..5417aa4 100644 --- a/FATrace.OEMApp/MainApp.resx +++ b/FATrace.OEMApp/MainApp.resx @@ -127,8 +127,8 @@ AAEAAAD/////AQAAAAAAAAAMAgAAAEZTeXN0ZW0uV2luZG93cy5Gb3JtcywgQ3VsdHVyZT1uZXV0cmFs LCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5BQEAAAAmU3lzdGVtLldpbmRvd3MuRm9ybXMu - SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAaiEAAAJNU0Z0AUkBTAIBAQgB - AAFQAQEBUAEBARABAAEQAQAE/wEhAQAI/wFCAU0BNgcAATYDAAEoAwABQAMAATADAAEBAQABIAYAATD/ + SW1hZ2VMaXN0U3RyZWFtZXIBAAAABERhdGEHAgIAAAAJAwAAAA8DAAAAaCEAAAJNU0Z0AUkBTAIBAQgB + AAFYAQEBWAEBARABAAEQAQAE/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,60 +217,60 @@ 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wMqAUAY AAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wgAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B 2wGWARIB/xgAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsB - lgESAf8B2wGWARIB/wJ8AVoB+BAAAXEBbwFRAfcB2wGWARIB/wwAAzMBUAHbAZYBEgH/BAAB2wGWARIB - /xAAAdsBlgESAf8QAAJbAVkBwCAAAkcBRgGAAyoBQBQAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsB - lgESAf8MAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8IAAHbAZYBEgH/AdsB + lgESAf8B2wGWARIB/wJ8AVsB+BAAAm8BUQH3AdsBlgESAf8MAAMzAVAB2wGWARIB/wQAAdsBlgESAf8Q + AAHbAZYBEgH/EAACWwFZAcAgAAJHAUYBgAMqAUAUAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYB + EgH/DAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/CAAB2wGWARIB/wHbAZYB + EgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B + 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8EAAHbAZYBEgH/FAAB + 2wGWARIB/wQAAdsBlgESAf8QAAHbAZYBEgH/EAACWwFZAcAgAAJHAUYBgAMqAUAQAAHbAZYBEgH/AdsB + lgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8QAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYB + EgH/CAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/EAADEgEYAdsBlgESAf8B + 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wQAAdsBlgESAf8UAAHbAZYBEgH/BAAB2wGWARIB + /xAAAdsBlgESAf8QAAJbAVkBwAgAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB + /wQAAkcBRgGAAyoBQBAAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8UAAM9AWgB2wGWARIB + /wHbAZYBEgH/BAAB2wGWARIB/wM6AWAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/xgAAxgB + IAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/CAAB2wGWARIB/xAAAdsBlgESAf8EAAHbAZYB + EgH/EAAB2wGWARIB/xAAAlsBWQHAIAACRwFGAYADKgFAEAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8o + AAHbAZYBEgH/CAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8cAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB + /wwAAmUBXAHnAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wQAAdsBlgESAf8B + 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/xQAAlsBWQHACAAB2wGWARIB/wHbAZYBEgH/AdsB + lgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8CRwFGAYADKgFAEAAB2wGWARIB/wJvAVEB9ygAAdsB + lgESAf8B2wGWARIB/wgAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/HAAB2wGWARIB/wHbAZYBEgH/AdsB + lgESAf9QAAJbAVkBwCAAAkcBRgGAAyoBQBAAAdsBlgESAf8oAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB + /wgAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/HAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8MAAJBAUAB + cAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8EAAHbAZYBEgH/AdsBlgESAf8B + 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8UAAJbAVkBwAgAAlsBWQHAAlsBWQHAAlsBWQHADAACRwFGAYAD + KgFAGAAB2wGWARIB/wHbAZYBEgH/GAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wQAAdsB + lgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8cAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYB + EgH/CAAB2wGWARIB/xAAAdsBlgESAf8EAAHbAZYBEgH/DAADKgFAAdsBlgESAf8QAAJbAVkBwAgAAzoB + YAM6AWADOgFgDAACRwFGAYADKgFAFAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/xAAAdsB + lgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8DDAEQAwYBCAHbAZYBEgH/AdsBlgESAf8B2wGWARIB + /wHbAZYBEgH/AdsBlgESAf8UAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8E + AAHbAZYBEgH/FAAB2wGWARIB/wQAAdsBlgESAf8QAAHbAZYBEgH/EAACWwFZAcAgAAJHAUYBgAMqAUAU + AAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8IAAHbAZYBEgH/AdsBlgESAf8B + 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8IAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsB lgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB - /wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wQAAdsBlgESAf8U - AAHbAZYBEgH/BAAB2wGWARIB/xAAAdsBlgESAf8QAAJbAVkBwCAAAkcBRgGAAyoBQBAAAdsBlgESAf8B - 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/xAAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsB - lgESAf8IAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8QAAMSARgB2wGWARIB - /wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/BAAB2wGWARIB/xQAAdsBlgESAf8EAAHbAZYB - EgH/EAAB2wGWARIB/xAAAlsBWQHACAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYB - EgH/BAACRwFGAYADKgFAEAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/xQAAz0BaAHbAZYB - EgH/AdsBlgESAf8EAAHbAZYBEgH/AzoBYAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/GAAD - GAEgAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8IAAHbAZYBEgH/EAAB2wGWARIB/wQAAdsB - lgESAf8QAAHbAZYBEgH/EAACWwFZAcAgAAJHAUYBgAMqAUAQAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB - /ygAAdsBlgESAf8IAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/xwAAdsBlgESAf8B2wGWARIB/wHbAZYB - EgH/DAACZQFcAecB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/BAAB2wGWARIB - /wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/FAACWwFZAcAIAAHbAZYBEgH/AdsBlgESAf8B - 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wJHAUYBgAMqAUAQAAHbAZYBEgH/Am8BUQH3KAAB - 2wGWARIB/wHbAZYBEgH/CAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8cAAHbAZYBEgH/AdsBlgESAf8B - 2wGWARIB/1AAAlsBWQHAIAACRwFGAYADKgFAEAAB2wGWARIB/ygAAdsBlgESAf8B2wGWARIB/wHbAZYB - EgH/CAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8cAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wwAAkEB - QAFwAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wQAAdsBlgESAf8B2wGWARIB - /wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/xQAAlsBWQHACAACWwFZAcACWwFZAcACWwFZAcAMAAJHAUYB - gAMqAUAYAAHbAZYBEgH/AdsBlgESAf8YAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/BAAB - 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/xwAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsB - lgESAf8IAAHbAZYBEgH/EAAB2wGWARIB/wQAAdsBlgESAf8MAAMqAUAB2wGWARIB/xAAAlsBWQHACAAD - OgFgAzoBYAM6AWAMAAJHAUYBgAMqAUAUAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/EAAB - 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wMMARADBgEIAdsBlgESAf8B2wGWARIB/wHbAZYB - EgH/AdsBlgESAf8B2wGWARIB/xQAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB - /wQAAdsBlgESAf8UAAHbAZYBEgH/BAAB2wGWARIB/xAAAdsBlgESAf8QAAJbAVkBwCAAAkcBRgGAAyoB - QBQAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wgAAdsBlgESAf8B2wGWARIB - /wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wgAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B - 2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYB - EgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/BAAB2wGWARIB/xQAAdsBlgESAf8EAAHbAZYBEgH/EAAB - 2wGWARIB/xAAAlsBWQHAAmkBYAHoAxgBIAMYASADGAEgAxgBIAMYASADGAEgAxgBIAHbAZYBEgH/AyoB - QBgAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8DVgGwCAAB2wGWARIB/wHbAZYBEgH/AdsB - lgESAf8YAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYB - EgH/AdsBlgESAf8B2wGWARIB/xAAAdsBlgESAf8B2wGWARIB/xAAAdsBlgESAf8EAAHbAZYBEgH/EAAB - 2wGWARIB/xQAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsB - lgESAf8B2wGWARIB/wHbAZYBEgH/IAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYB - EgH/KAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB - /wHbAZYBEgH/GAAB2wGWARIB/wHbAZYBEgH/AyoBQAHbAZYBEgH/AdsBlgESAf8IAAHbAZYBEgH/AdsB - lgESAf8CXwFbAdgCWwFZAcAB2wGWARIB/wHbAZYBEgH/YAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B - 2wGWARIB/yQAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/DAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8Y - AAJHAUYBgAJjAV0B3wJYAVYBsxQAAmMBXQHfAmMBXQHfAwYBCKAAAxgBIAwAAlEBUAGfVAABQgFNAT4H - AAE+AwABKAMAAUADAAEwAwABAQEAAQEFAAGAAQEWAAP/gQAB/wHyAQABgAH4AQ8BwAEBAf8BwAEAAYAB - 4AEDAYABAAH/AewBAAGAAcMB4wGAAQAB+AHMAQABgAHPAfkCAAH4AcABAAGAAY8B+QIAAdABQwEAAYAB - nwH8AgABgAEPAoABnwH8AgABAgEHAv8BnwH8AgABhwEPAoABnwH4AgABzwGfAQABgAHOATkCAAGHAQ8B - AAGAAcYBMQIAAYIBBwEAAYAB7gE7AgABgAEPAQABgAH+AT8CAAHQAX8BAAGAAf4BPwGAAQAB+AH/AQAB - gAH+AT8BgAEBAfgB/wGBAcAB/gE/AeABBwL/Af4BHwHxAccB4wHnA/8BBwHwAQ8BwQGBAeABAwHxAYMB - 8AEHAZwBvQHvAfMB4QHBAYABAAG+Ab0B7wHzAcEB4QGDAcABvgG9AewBEwHDAeIBBwHgAd4BvQHvAfMB - xwH+AccB8QHAAYMB7AEDAc8B/AHHAfEC/wHvAfMB3wH4AccB8QHAAYMB7AFzAfMB8AGHAfAB3gG5AewB - cwHhAeABAwHgAb4BvQHvAfMB4AHBAYABAAG+Ab0B4AEDAfABYwHwAQcBngG9AfABBwH4AT8B8AEPAcEB - gQL/Af4BHwHxAccC4wT/Af0B3wL/Cw== + /wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wQAAdsBlgESAf8UAAHbAZYBEgH/BAAB2wGWARIB/xAAAdsB + lgESAf8QAAJbAVkBwAJpAWAB6AMYASADGAEgAxgBIAMYASADGAEgAxgBIAMYASAB2wGWARIB/wMqAUAY + AAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/A1YBsAgAAdsBlgESAf8B2wGWARIB/wHbAZYB + EgH/GAAB2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB + /wHbAZYBEgH/AdsBlgESAf8QAAHbAZYBEgH/AdsBlgESAf8QAAHbAZYBEgH/BAAB2wGWARIB/xAAAdsB + lgESAf8UAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYB + EgH/AdsBlgESAf8B2wGWARIB/yAAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB + /ygAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsBlgESAf8B + 2wGWARIB/xgAAdsBlgESAf8B2wGWARIB/wMqAUAB2wGWARIB/wHbAZYBEgH/CAAB2wGWARIB/wHbAZYB + EgH/Al8BWwHYAlsBWQHAAdsBlgESAf8B2wGWARIB/2AAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/AdsB + lgESAf8kAAHbAZYBEgH/AdsBlgESAf8B2wGWARIB/wwAAdsBlgESAf8B2wGWARIB/wHbAZYBEgH/GAAC + RwFGAYACYwFdAd8CWAFWAbMUAAJjAV0B3wJjAV0B3wMGAQigAAMYASAMAAJRAVABn1QAAUIBTQE+BwAB + PgMAASgDAAFAAwABMAMAAQEBAAEBBQABgAEBFgAD/4EAAf8B8gEAAYAB+AEPAcABAQH/AcABAAGAAeAB + AwGAAQAB/wHsAQABgAHDAeMBgAEAAfgBzAEAAYABzwH5AgAB+AHAAQABgAGPAfkCAAHQAUMBAAGAAZ8B + /AIAAYABDwKAAZ8B/AIAAQIBBwL/AZ8B/AIAAYcBDwKAAZ8B+AIAAc8BnwEAAYABzgE5AgABhwEPAQAB + gAHGATECAAGCAQcBAAGAAe4BOwIAAYABDwEAAYAB/gE/AgAB0AF/AQABgAH+AT8BgAEAAfgB/wEAAYAB + /gE/AYABAQH4Af8BgQHAAf4BPwHgAQcC/wH+AR8B8QHHAeMB5wP/AQcB8AEPAcEBgQHgAQMB8QGDAfAB + BwGcAb0B7wHzAeEBwQGAAQABvgG9Ae8B8wHBAeEBgwHAAb4BvQHsARMBwwHiAQcB4AHeAb0B7wHzAccB + /gHHAfEBwAGDAewBAwHPAfwBxwHxAv8B7wHzAd8B+AHHAfEBwAGDAewBcwHzAfABhwHwAd4BuQHsAXMB + 4QHgAQMB4AG+Ab0B7wHzAeABwQGAAQABvgG9AeABAwHwAWMB8AEHAZ4BvQHwAQcB+AE/AfABDwHBAYEC + /wH+AR8B8QHHAuME/wH9Ad8C/ws= diff --git a/FATrace.OEMApp/Services/DownloadTaskWorker.cs b/FATrace.OEMApp/Services/DownloadTaskWorker.cs index 2256086..21e5728 100644 --- a/FATrace.OEMApp/Services/DownloadTaskWorker.cs +++ b/FATrace.OEMApp/Services/DownloadTaskWorker.cs @@ -7,6 +7,7 @@ using FATrace.Model; using FATrace.HKNetLib.Wrapper; using NLog; using FATrace.OEMApp; +using System.IO; using TaskStatus = FATrace.Model.TaskStatus; namespace FATrace.OEMApp.Services @@ -27,10 +28,10 @@ namespace FATrace.OEMApp.Services private readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly SemaphoreSlim _singleRunner = new(1, 1); // 保证同一时间只有一个下载 - private CancellationTokenSource? _cts; - private Task? _loopTask; + private CancellationTokenSource? _cts; // 后台主循环的取消令牌(停止服务时发出) + private Task? _loopTask; // 后台主循环 Task - private HkCamera? _hk; + private HkCamera? _hk; // 供下载与事件回调使用的海康客户端(由 UI 注入) private DownloadTaskWorker() { } @@ -46,10 +47,11 @@ namespace FATrace.OEMApp.Services { _hk = hk ?? throw new ArgumentNullException(nameof(hk)); if (_cts != null) return; // 已启动 - // 恢复上次未完成的运行中任务为待处理 + // 恢复上次未完成的运行中任务为待处理,然后再循环执行 try { var db = FSqlContext.FDb; + // 将异常退出时处于 Running 的任务回滚为 Pending,避免卡住队列 db.Update() .Set(a => a.Status, TaskStatus.Pending) .Set(a => a.Progress, 0) @@ -67,10 +69,10 @@ namespace FATrace.OEMApp.Services .Set(a => a.UpdateTime, DateTime.Now) .Where(a => a.Status == TaskStatus.Failed && a.TryCount < maxRetry) .ExecuteAffrows(); - } catch { } _cts = new CancellationTokenSource(); + //开始后台主循环 _loopTask = Task.Run(() => RunAsync(_cts.Token)); _logger.Info("[DownloadTaskWorker] 已启动"); } @@ -82,6 +84,8 @@ namespace FATrace.OEMApp.Services { try { + // 发出取消信号,RunAsync 将尽快退出; + // 正在处理的任务会在 token 取消时尝试停止 SDK 下载 _cts?.Cancel(); } catch { } @@ -97,29 +101,35 @@ namespace FATrace.OEMApp.Services /// 业务条码/编号,关联 OEMRawUse.Code 与 VideoAction.Code。 /// 原料名称。 /// 操作用户。 - /// NVR 下载开始时间(默认当前时间-5 分钟)。 + /// NVR 下载开始时间(默认当前时间-30 秒)。 /// NVR 下载结束时间(默认当前时间)。 + /// 原料条码(可选)。未提供时默认等于 code。 /// 新增 DownloadTask 的自增 Id。 - public long Enqueue(string code, string rawName, string user, DateTime? start = null, DateTime? end = null) + public long Enqueue(string code, string rawName, string user, DateTime? start = null, DateTime? end = null, string? rawCode = null) { if (string.IsNullOrWhiteSpace(code)) throw new ArgumentException("code 不能为空"); if (string.IsNullOrWhiteSpace(rawName)) throw new ArgumentException("rawName 不能为空"); if (string.IsNullOrWhiteSpace(user)) throw new ArgumentException("user 不能为空"); var now = DateTime.Now; + // 构造持久化任务: + // - 默认回溯 30 秒(符合项目规则) + // - rawCode 未提供时回落到 code,避免非空列插入失败 var task = new DownloadTask { Code = code, RawName = rawName, + RawCode = rawCode ?? code, User = user, Status = TaskStatus.Pending, Progress = 0, - NvrStartTime = start ?? now.AddMinutes(-5), + NvrStartTime = start ?? now.AddSeconds(-30), NvrEndTime = end ?? now, CreateTime = now, UpdateTime = now }; + // 入库返回自增 Id,便于 UI 提示与后续跟踪 var id = FSqlContext.FDb.Insert(task).ExecuteIdentity(); _logger.Info("[DownloadTaskWorker] 入队 DownloadTask: Id={Id}, Code={Code}", id, code); return id; @@ -145,12 +155,13 @@ namespace FATrace.OEMApp.Services if (next == null) { // 暂无任务,稍候再查 - await Task.Delay(1000, token); + await Task.Delay(5000, token); continue; } // 使用信号量确保同一时间仅有一个任务进入下载处理 await _singleRunner.WaitAsync(token); + // 通过 ContinueWith 在任务结束时释放信号量,避免阻塞主循环 _ = ProcessTaskAsync(next, token).ContinueWith(_ => _singleRunner.Release()); } catch (OperationCanceledException) @@ -185,6 +196,7 @@ namespace FATrace.OEMApp.Services var db = FSqlContext.FDb; + // 步骤1:状态入库(Running/TryCount/Progress/UpdateTime) // 标记运行中 t.Status = TaskStatus.Running; t.TryCount += 1; @@ -196,6 +208,7 @@ namespace FATrace.OEMApp.Services .Where(a => a.Id == t.Id) .ExecuteAffrowsAsync(); + // 步骤2:生成保存路径(含安全文件名),并确保保存目录存在 // 生成本地文件名/路径 var saveBase = _hk.NVRVideoSavePath; if (string.IsNullOrWhiteSpace(saveBase)) @@ -205,25 +218,36 @@ namespace FATrace.OEMApp.Services return; } var filePath = NVRCom.GetVideoName(saveBase, t.Code ?? "CODE"); + // 确保保存目录存在,避免 SDK 写文件失败 + try + { + var dir = Path.GetDirectoryName(filePath); + if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + } + catch { } await db.Update() .Set(a => a.VideoFilePath, filePath) .Set(a => a.UpdateTime, DateTime.Now) .Where(a => a.Id == t.Id) .ExecuteAffrowsAsync(); + // 步骤3:订阅 SDK 事件 -> TCS 转换 // 事件 -> TCS // 订阅两个事件:进度与完成。进度事件写回数据库;完成事件用于唤醒等待 - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + 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) - .Where(a => a.Id == t.Id) - .ExecuteAffrows(); + //db.Update() + // .Set(a => a.Progress, Math.Max((short)0, Math.Min((short)100, p))) + // .Set(a => a.UpdateTime, DateTime.Now) + // .Where(a => a.Id == t.Id) + // .ExecuteAffrows(); } catch { } } @@ -231,14 +255,16 @@ namespace FATrace.OEMApp.Services { try { - tcs.TrySetResult(true); + tcs.TrySetResult(msg); } catch { } } + _hk.NVRLoadVideoProcessEventHandler += OnProgress; _hk.NVRLoadVideoCompleteEventHandler += OnComplete; + // 步骤4:调用 SDK 按时间下载,并等待完成事件(含超时/取消处理) try { // 发起下载(按时间范围) @@ -249,18 +275,60 @@ namespace FATrace.OEMApp.Services return; } - // HkCamera 内部会启动进度监控:此处等待完成事件触发 - using (token.Register(() => tcs.TrySetCanceled())) + // 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))); + Task completed = tcs.Task; + Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(timeoutSec), token); + using (token.Register(() => { - await tcs.Task; + try { tcs.TrySetCanceled(); } catch { } + })) + { + var finished = await Task.WhenAny(completed, timeoutTask); + if (finished == timeoutTask) + { + try { _hk.Sdk_NET_DVR_StopGetFile(); } catch { } + await MarkFailedAsync(t, $"下载超时({timeoutSec}s)"); + return; + } } + 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); + return; + } + + // 步骤5:文件有效性检查(存在且大小>0) + // 文件有效性检查 + try + { + if (!File.Exists(filePath) || new FileInfo(filePath).Length <= 0) + { + await MarkFailedAsync(t, "下载文件不存在或为空"); + return; + } + } + catch + { + // 文件系统异常 + await MarkFailedAsync(t, "下载文件验证异常"); + return; + } + + // 步骤6:入库——写 OEMRawUse 并创建 Jellyfin 监听任务 // 下载完成:写 OEMRawUse 并创建 JellyfinMonitorTask // 1) 插入 OEMRawUse(UrlState=false, VideoUrl 空) var rawUse = new OEMRawUse { InBagCode = t.Code, RawName = t.RawName, + RawCode = t.RawCode, User = t.User, UrlState = false, VideoUrl = string.Empty, @@ -286,6 +354,7 @@ namespace FATrace.OEMApp.Services }; db.Insert(jfTask).ExecuteAffrows(); + // 步骤7:收尾——标记下载完成并记录日志 // 标记下载完成 t.Status = TaskStatus.Completed; t.Progress = 100; @@ -296,10 +365,12 @@ namespace FATrace.OEMApp.Services .Where(a => a.Id == t.Id) .ExecuteAffrowsAsync(); - _logger.Info("[DownloadTaskWorker] 下载完成,已创建 Jellyfin 监控任务。DownloadTaskId={Id}, OEMRawUseId={RawUseId}", t.Id, rawUseId); + _logger.Info("[DownloadTaskWorker] 下载完成,已创建 Jellyfin 监控任务 DownloadTaskId={Id}, OEMRawUseId={RawUseId}", t.Id, rawUseId); } catch (OperationCanceledException) { + // 取消:停止当前 SDK 下载并标记失败 + try { _hk.Sdk_NET_DVR_StopGetFile(); } catch { } await MarkFailedAsync(t, "任务被取消"); } catch (Exception ex) @@ -311,6 +382,7 @@ namespace FATrace.OEMApp.Services // 释放事件订阅,避免内存泄露或重复触发 try { _hk.NVRLoadVideoProcessEventHandler -= OnProgress; } catch { } try { _hk.NVRLoadVideoCompleteEventHandler -= OnComplete; } catch { } + //_singleRunner.Release(); } } @@ -322,6 +394,8 @@ namespace FATrace.OEMApp.Services private Task MarkFailedAsync(DownloadTask t, string? error) { var db = FSqlContext.FDb; + // 将状态置为 Failed,记录错误信息与时间; + // 不抛出异常,保证主循环可以继续处理后续任务 t.Status = TaskStatus.Failed; t.Error = error; t.UpdateTime = DateTime.Now; diff --git a/FATrace.OEMApp/Services/TimeClearDataService.cs b/FATrace.OEMApp/Services/TimeClearDataService.cs new file mode 100644 index 0000000..3d64be9 --- /dev/null +++ b/FATrace.OEMApp/Services/TimeClearDataService.cs @@ -0,0 +1,142 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FATrace.Com; +using FATrace.Model; +using NLog; + +namespace FATrace.OEMApp.Services +{ + public sealed class TimeClearDataService + { + private readonly Logger _logger = LogManager.GetCurrentClassLogger(); + private CancellationTokenSource? _cts; + private Task? _loopTask; + private TimeSpan _runAt = new TimeSpan(2, 0, 0); + private int _retentionDays = 365; + private bool _enabled = true; + + public event Action? Info; + + public void Start() + { + if (_cts != null) return; + + try + { + _enabled = ConfigHelper.GetBoolOrDefault("DataCleanupEnabled", true); + var tod = ConfigHelper.GetStringOrDefault("DataCleanupTimeOfDay", "02:00:00"); + if (!TimeSpan.TryParse(tod, out _runAt)) + { + _runAt = new TimeSpan(2, 0, 0); + } + _retentionDays = Math.Max(1, ConfigHelper.GetIntOrDefault("DataRetentionDays", 365)); + } + catch { } + + if (!_enabled) + { + _logger.Warn("[TimeClear] 已禁用,未启动"); + return; + } + + _cts = new CancellationTokenSource(); + _loopTask = Task.Run(() => RunAsync(_cts.Token)); + _logger.Info("[TimeClear] 服务已启动,时间点={RunAt}, 保留天数={Days}", _runAt, _retentionDays); + SafeInfo($"定时清理服务启动,时间点={_runAt}, 保留天数={_retentionDays}"); + } + + public void Stop() + { + try { _cts?.Cancel(); } catch { } + _cts = null; + _logger.Info("[TimeClear] 服务已停止"); + } + + private async Task RunAsync(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + try + { + var now = DateTime.Now; + var next = GetNextRunTime(now); + var delay = next - now; + SafeInfo($"距离下一次清理还有 {delay:hh\\:mm\\:ss},计划时间 {next:yyyy-MM-dd HH:mm:ss}"); + await Task.Delay(delay, token); + await CleanupOnceAsync(token); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + _logger.Error(ex, "[TimeClear] 后台循环异常"); + try { await Task.Delay(TimeSpan.FromMinutes(1), token); } catch { } + } + } + } + + private DateTime GetNextRunTime(DateTime now) + { + var next = now.Date + _runAt; + if (next <= now.AddSeconds(1)) next = next.AddDays(1); + return next; + } + + 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}"); + + long delJf = 0, delDl = 0, delRaw = 0, delAct = 0; + try + { + delJf = FSqlContext.FDb.Delete().Where(a => a.CreateTime < cutoff).ExecuteAffrows(); + } + catch (Exception ex) + { + _logger.Warn(ex, "[TimeClear] 清理 JellyfinMonitorTask 失败"); + } + + try + { + delDl = FSqlContext.FDb.Delete().Where(a => a.CreateTime < cutoff).ExecuteAffrows(); + } + catch (Exception ex) + { + _logger.Warn(ex, "[TimeClear] 清理 DownloadTask 失败"); + } + + try + { + delRaw = FSqlContext.FDb.Delete().Where(a => a.CreateTime < cutoff).ExecuteAffrows(); + } + catch (Exception ex) + { + _logger.Warn(ex, "[TimeClear] 清理 OEMRawUse 失败"); + } + + try + { + delAct = FSqlContext.FDb.Delete().Where(a => a.CreateTime < cutoff).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}"); + + await Task.CompletedTask; + } + + private void SafeInfo(string msg) + { + try { Info?.Invoke(msg); } catch { } + } + } +}