初步版本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

@@ -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,18 +271,26 @@ 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
// 订阅两个事件:进度与完成。进度事件写回数据库;完成事件用于唤醒等待
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)
@@ -259,7 +307,7 @@ namespace FATrace.OEMApp.Services
}
catch { }
}
_hk.NVRLoadVideoProcessEventHandler += OnProgress;
_hk.NVRLoadVideoCompleteEventHandler += OnComplete;
@@ -268,114 +316,136 @@ namespace FATrace.OEMApp.Services
try
{
// 发起下载(按时间范围)
var res = _hk.Sdk_NET_DVR_GetFileByTime_V40(t.NvrStartTime, t.NvrEndTime, filePath);
var res = _hk.Sdk_NET_DVR_GetFileByTime_V40(downloadTask.NvrStartTime, downloadTask.NvrEndTime, filePath);
if (!res.Result)
{
await MarkFailedAsync(t, res.Msg);
await MarkFailedAsync(downloadTask, res.Msg);
return;
}
// HkCamera 内部会启动进度监控:此处等待完成事件触发(带超时)
var timeWindowSec = Math.Max(1, (int)(t.NvrEndTime - t.NvrStartTime).TotalSeconds);
var timeoutSec = ConfigHelper.GetIntOrDefault("DownloadTaskTimeoutSeconds", Math.Max(120, Math.Min(1800, timeWindowSec * 3)));
//下载过程中的监控事件
// 计算这次下载的时间窗口长度(秒),用于推导合理的超时
var timeWindowSec = Math.Max(1, (int)(downloadTask.NvrEndTime - downloadTask.NvrStartTime).TotalSeconds);
// 计算总超时秒数:优先读配置 DownloadTaskTimeoutSeconds
// 如果未配置,用时间窗口 * 3 的经验值,并限制在 [120, 1800] 区间,防止过短/过长。
var timeoutSec = ConfigHelper.GetIntOrDefault(
"DownloadTaskTimeoutSeconds",
Math.Max(120, Math.Min(1800, timeWindowSec * 3))
);
// completed 代表“下载完成”这个事件(由 tcs.TrySetResult 在回调里触发)
Task completed = tcs.Task;
// timeoutTask 是“超时定时器”,超时时间到会完成;同时受 token 取消影响
Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(timeoutSec), token);
// 当外部取消Stop/退出)时,主动让 tcs 进入取消态,避免一直等待 completed
using (token.Register(() =>
{
try { tcs.TrySetCanceled(); } catch { }
}))
{
// 等待“完成事件”或“超时”二者之一先发生
var finished = await Task.WhenAny(completed, timeoutTask);
// 如果先等到了 timeoutTask说明超时发生
if (finished == timeoutTask)
{
// 尽力停止 SDK 的下载(防止后台还在跑)
try { _hk.Sdk_NET_DVR_StopGetFile(); } catch { }
await MarkFailedAsync(t, $"下载超时({timeoutSec}s)");
return;
// 将任务标记为失败(原因是超时),写入数据库状态
await MarkFailedAsync(downloadTask, $"下载超时({timeoutSec}s)");
return; // 结束本次任务处理
}
}
// 能走到这里,表示 completed 先发生(下载完成事件被触发)
// 读取回调携带的完成消息(有些 SDK 会返回提示文本,例如“下载完成”)
var completeMsg = await ((Task<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;
}
}
}