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 FileRetentionDays = 365; private bool _enabled = true; /// /// 数据库保存的信息天数 /// private int DbRetentionDays = 365; public event Action? Info; public void Start() { FileRetentionDays=ConfigHelper.GetIntOrDefault("VideoFileSaveDay", 365); DbRetentionDays = ConfigHelper.GetIntOrDefault("DbSaveDay", 365); 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); } } catch { } if (!_enabled) { _logger.Warn("[TimeClear] 已禁用,未启动"); return; } _cts = new CancellationTokenSource(); _loopTask = Task.Run(() => RunAsync(_cts.Token)); _logger.Info("[TimeClear] 服务已启动,时间点={RunAt}, 文件保留天数={FileDays}, 数据库保留天数={DbDays}", _runAt, FileRetentionDays, DbRetentionDays); SafeInfo($"定时清理服务启动,时间点={_runAt}, 文件保留天数={FileRetentionDays}, 数据库保留天数={DbRetentionDays}"); } 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 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; try { var toDelFile = FSqlContext.FDb.Select() .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] 扫描 DownloadTask 待删文件失败"); } try { var dbOld = FSqlContext.FDb.Select() .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() .Where(a => a.CreateTime < dbCutoff) .ExecuteAffrows(); } catch (Exception ex) { _logger.Warn(ex, "[TimeClear] 清理 DownloadTask 失败"); } try { delJf = FSqlContext.FDb.Delete().Where(a => a.CreateTime < dbCutoff).ExecuteAffrows(); } catch (Exception ex) { _logger.Warn(ex, "[TimeClear] 清理 JellyfinMonitorTask 失败"); } try { delRaw = FSqlContext.FDb.Delete().Where(a => a.CreateTime < dbCutoff).ExecuteAffrows(); } catch (Exception ex) { _logger.Warn(ex, "[TimeClear] 清理 OEMRawUse 失败"); } try { delAct = FSqlContext.FDb.Delete().Where(a => a.CreateTime < dbCutoff).ExecuteAffrows(); } catch (Exception ex) { _logger.Warn(ex, "[TimeClear] 清理 VideoAction 失败"); } _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; } private void SafeInfo(string msg) { try { Info?.Invoke(msg); } catch { } } } }