初级的功能
This commit is contained in:
@@ -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<DownloadTask>()
|
||||
.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
|
||||
/// <param name="code">业务条码/编号,关联 OEMRawUse.Code 与 VideoAction.Code。</param>
|
||||
/// <param name="rawName">原料名称。</param>
|
||||
/// <param name="user">操作用户。</param>
|
||||
/// <param name="start">NVR 下载开始时间(默认当前时间-5 分钟)。</param>
|
||||
/// <param name="start">NVR 下载开始时间(默认当前时间-30 秒)。</param>
|
||||
/// <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)
|
||||
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<DownloadTask>(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<DownloadTask>()
|
||||
.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<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
void OnProgress(object? s, short p)
|
||||
{
|
||||
try
|
||||
{
|
||||
db.Update<DownloadTask>()
|
||||
.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<DownloadTask>()
|
||||
// .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<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);
|
||||
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<JellyfinMonitorTask>(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;
|
||||
|
||||
142
FATrace.OEMApp/Services/TimeClearDataService.cs
Normal file
142
FATrace.OEMApp/Services/TimeClearDataService.cs
Normal file
@@ -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<string>? 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<JellyfinMonitorTask>().Where(a => a.CreateTime < cutoff).ExecuteAffrows();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "[TimeClear] 清理 JellyfinMonitorTask 失败");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
delDl = FSqlContext.FDb.Delete<DownloadTask>().Where(a => a.CreateTime < cutoff).ExecuteAffrows();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "[TimeClear] 清理 DownloadTask 失败");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
delRaw = FSqlContext.FDb.Delete<OEMRawUse>().Where(a => a.CreateTime < cutoff).ExecuteAffrows();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "[TimeClear] 清理 OEMRawUse 失败");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
delAct = FSqlContext.FDb.Delete<VideoAction>().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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user