初步版本251204
This commit is contained in:
92
FATrace.OEMApp/Services/CsvService.cs
Normal file
92
FATrace.OEMApp/Services/CsvService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) 插入 OEMRawUse(UrlState=false, VideoUrl 空)
|
||||
//// 步骤6:入库(不再使用 Jellyfin 监听):
|
||||
//// 1) 插入 VideoAction(保存本地视频元数据)
|
||||
//var va = new VideoAction
|
||||
//{
|
||||
// Code = downloadTask.Code,
|
||||
// User = downloadTask.User,
|
||||
// VideoFilePath = filePath,
|
||||
// VideoName = Path.GetFileName(filePath) ?? string.Empty,
|
||||
// StartTime = downloadTask.NvrStartTime,
|
||||
// EndTime = downloadTask.NvrEndTime,
|
||||
// CreateTime = DateTime.Now
|
||||
//};
|
||||
//var videoActionId = db.Insert<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user