using Microsoft.Extensions.Hosting;
|
using SqlSugar;
|
using System;
|
using System.IO;
|
using System.Threading;
|
using System.Threading.Tasks;
|
using WIDESEAWCS_Core.DB;
|
using WIDESEAWCS_Core.Seed;
|
using WIDESEAWCS_Model.Models;
|
|
namespace WIDESEAWCS_Core.LogHelper
|
{
|
/// <summary>
|
/// 日志数据清理服务
|
/// </summary>
|
public class LogCleanupService : IHostedService, IDisposable
|
{
|
private Timer _cleanupTimer;
|
private bool _isDisposed = false;
|
private readonly object _lockObject = new object();
|
|
/// <summary>
|
/// 数据保留天数(默认90天)
|
/// </summary>
|
public int RetentionDays { get; set; } = 90;
|
|
/// <summary>
|
/// 清理间隔(小时,默认24小时)
|
/// </summary>
|
public int CleanupIntervalHours { get; set; } = 24;
|
|
/// <summary>
|
/// 清理执行时间(小时,0-23,默认凌晨2点)
|
/// </summary>
|
public int CleanupHour { get; set; } = 2;
|
|
/// <summary>
|
/// 是否启用归档
|
/// </summary>
|
public bool EnableArchive { get; set; } = false;
|
|
/// <summary>
|
/// 是否启用自动清理
|
/// </summary>
|
public bool EnableAutoCleanup { get; set; } = true;
|
|
/// <summary>
|
/// 日志文件保留天数
|
/// </summary>
|
public int LogRetentionDays { get; set; } = 30;
|
/// <summary>
|
/// 日志文件路径
|
/// </summary>
|
private string LogFilePath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs", "LogCleanup", $"Cleanup_{DateTime.Now:yyyyMMdd}.log");
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
{
|
if (EnableAutoCleanup)
|
{
|
// 确保日志目录存在
|
EnsureLogDirectoryExists();
|
|
// 计算到下一个清理时间的间隔
|
var now = DateTime.Now;
|
var nextCleanupTime = DateTime.Today.AddHours(CleanupHour);
|
|
if (now > nextCleanupTime)
|
{
|
nextCleanupTime = nextCleanupTime.AddDays(1);
|
}
|
|
var timeToFirstCleanup = nextCleanupTime - now;
|
|
// 设置定时器,在指定时间执行清理
|
_cleanupTimer = new Timer(CleanupCallback, null,
|
timeToFirstCleanup,
|
TimeSpan.FromHours(CleanupIntervalHours));
|
|
WriteLog($"日志清理服务已启动,首次清理时间: {nextCleanupTime:yyyy-MM-dd HH:mm:ss}");
|
}
|
|
return Task.CompletedTask;
|
}
|
|
/// <summary>
|
/// 确保日志目录存在
|
/// </summary>
|
private void EnsureLogDirectoryExists()
|
{
|
try
|
{
|
string logDirectory = Path.GetDirectoryName(LogFilePath);
|
if (!Directory.Exists(logDirectory))
|
{
|
Directory.CreateDirectory(logDirectory);
|
}
|
}
|
catch (Exception ex)
|
{
|
Console.WriteLine($"创建日志目录失败: {ex.Message}");
|
}
|
}
|
|
/// <summary>
|
/// 写入日志到文件
|
/// </summary>
|
private void WriteLog(string message, bool isError = false)
|
{
|
try
|
{
|
lock (_lockObject)
|
{
|
EnsureLogDirectoryExists();
|
string logMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {(isError ? "[ERROR]" : "[INFO]")} {message}";
|
|
// 写入文件
|
File.AppendAllText(LogFilePath, logMessage + Environment.NewLine);
|
|
// 同时输出到控制台(可选,便于调试)
|
Console.WriteLine(logMessage);
|
|
// 日志文件过大时自动清理(保留最近30天的日志文件)
|
AutoCleanupLogFiles();
|
}
|
}
|
catch (Exception ex)
|
{
|
// 如果文件日志失败,至少输出到控制台
|
Console.WriteLine($"写入日志文件失败: {ex.Message}");
|
Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}");
|
}
|
}
|
|
/// <summary>
|
/// 自动清理过期的日志文件(保留30天)
|
/// </summary>
|
private void AutoCleanupLogFiles()
|
{
|
try
|
{
|
string logDirectory = Path.GetDirectoryName(LogFilePath);
|
if (Directory.Exists(logDirectory))
|
{
|
var files = Directory.GetFiles(logDirectory, "Cleanup_*.log");
|
DateTime cutoffDate = DateTime.Now.AddDays(-30);
|
|
foreach (var file in files)
|
{
|
try
|
{
|
// 从文件名中提取日期
|
string fileName = Path.GetFileNameWithoutExtension(file);
|
if (fileName.StartsWith("Cleanup_"))
|
{
|
string dateStr = fileName.Replace("Cleanup_", "");
|
if (DateTime.TryParseExact(dateStr, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out DateTime fileDate))
|
{
|
if (fileDate < cutoffDate)
|
{
|
File.Delete(file);
|
WriteLog($"删除过期日志文件: {Path.GetFileName(file)}");
|
}
|
}
|
}
|
}
|
catch (Exception ex)
|
{
|
Console.WriteLine($"删除过期日志文件失败 {file}: {ex.Message}");
|
}
|
}
|
}
|
}
|
catch (Exception ex)
|
{
|
Console.WriteLine($"清理日志文件失败: {ex.Message}");
|
}
|
}
|
|
private void CleanupCallback(object state)
|
{
|
Task.Run(() => CleanupAsync());
|
}
|
|
/// <summary>
|
/// 执行清理
|
/// </summary>
|
public async Task CleanupAsync()
|
{
|
try
|
{
|
WriteLog($"开始清理 {RetentionDays} 天前的日志数据");
|
|
using (SqlSugarClient sugarClient = new SqlSugarClient(new ConnectionConfig()
|
{
|
ConnectionString = DBContext.GetMainConnectionDb().Connection,
|
IsAutoCloseConnection = true,
|
DbType = DBContext.DbType,
|
}))
|
{
|
DateTime cutoffDate = DateTime.Now.AddDays(-RetentionDays);
|
|
WriteLog($"截止日期: {cutoffDate:yyyy-MM-dd HH:mm:ss}");
|
|
if (EnableArchive)
|
{
|
// 先归档再删除
|
await ArchiveDataAsync(sugarClient, cutoffDate);
|
}
|
|
// 删除历史数据
|
int deletedCount = await sugarClient.Deleteable<Dt_InterfaceLog>()
|
.Where(x => x.CreateDate < cutoffDate)
|
.ExecuteCommandAsync();
|
|
WriteLog($"清理完成,共删除 {deletedCount} 条记录");
|
|
// 可选:记录清理日志到数据库
|
await LogCleanupResult(sugarClient, deletedCount, cutoffDate);
|
}
|
}
|
catch (Exception ex)
|
{
|
WriteLog($"清理失败: {ex.Message}", true);
|
WriteLog($"详细错误: {ex.ToString()}", true);
|
}
|
}
|
|
private async Task ArchiveDataAsync(SqlSugarClient sugarClient, DateTime cutoffDate)
|
{
|
try
|
{
|
string archiveTableName = $"Dt_InterfaceLog_Archive_{DateTime.Now:yyyyMM}";
|
|
WriteLog($"开始归档数据到表: {archiveTableName}");
|
|
// 创建归档表
|
string createTableSql = $@"
|
IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='{archiveTableName}' AND xtype='U')
|
BEGIN
|
SELECT * INTO {archiveTableName} FROM Dt_InterfaceLog WHERE 1=0
|
ALTER TABLE {archiveTableName} ADD ArchiveDate datetime DEFAULT GETDATE()
|
END";
|
await sugarClient.Ado.ExecuteCommandAsync(createTableSql);
|
|
// 归档数据
|
string archiveSql = $@"
|
INSERT INTO {archiveTableName} (Id, ApiCode, ApiName, ApiAddress, PushFrequency,
|
PushState, Requestor, RequestParameters, RequestParametersHash, Recipient,
|
ResponseParameters, ResponseParametersHash, Remark, CreateTime, ArchiveDate)
|
SELECT Id, ApiCode, ApiName, ApiAddress, PushFrequency, PushState,
|
Requestor, RequestParameters, RequestParametersHash, Recipient,
|
ResponseParameters, ResponseParametersHash, Remark, CreateTime, GETDATE()
|
FROM Dt_InterfaceLog
|
WHERE CreateTime < @CutoffDate";
|
|
int archivedCount = await sugarClient.Ado.ExecuteCommandAsync(archiveSql,
|
new { CutoffDate = cutoffDate });
|
|
WriteLog($"归档完成,共归档 {archivedCount} 条记录到 {archiveTableName}");
|
}
|
catch (Exception ex)
|
{
|
WriteLog($"归档失败: {ex.Message}", true);
|
}
|
}
|
|
private async Task LogCleanupResult(SqlSugarClient sugarClient, int deletedCount, DateTime cutoffDate)
|
{
|
try
|
{
|
// 创建清理日志表(如果不存在)
|
string createLogTableSql = @"
|
IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='LogCleanupHistory' AND xtype='U')
|
BEGIN
|
CREATE TABLE LogCleanupHistory (
|
Id INT IDENTITY(1,1) PRIMARY KEY,
|
TableName NVARCHAR(100),
|
DeletedCount INT,
|
CutoffDate DATETIME,
|
CleanupTime DATETIME DEFAULT GETDATE()
|
)
|
END";
|
await sugarClient.Ado.ExecuteCommandAsync(createLogTableSql);
|
|
// 记录清理日志
|
string insertLogSql = @"
|
INSERT INTO LogCleanupHistory (TableName, DeletedCount, CutoffDate, CleanupTime)
|
VALUES ('Dt_InterfaceLog', @DeletedCount, @CutoffDate, GETDATE())";
|
|
await sugarClient.Ado.ExecuteCommandAsync(insertLogSql,
|
new { DeletedCount = deletedCount, CutoffDate = cutoffDate });
|
|
WriteLog($"清理记录已写入数据库日志表");
|
}
|
catch (Exception ex)
|
{
|
WriteLog($"写入数据库日志失败: {ex.Message}", true);
|
}
|
}
|
|
/// <summary>
|
/// 手动执行清理
|
/// </summary>
|
public async Task<int> ManualCleanup(int days = 90)
|
{
|
try
|
{
|
WriteLog($"手动清理开始,删除超过 {days} 天的数据");
|
|
using (SqlSugarClient sugarClient = new SqlSugarClient(new ConnectionConfig()
|
{
|
ConnectionString = DBContext.GetMainConnectionDb().Connection,
|
IsAutoCloseConnection = true,
|
DbType = DBContext.DbType,
|
}))
|
{
|
DateTime cutoffDate = DateTime.Now.AddDays(-days);
|
int deletedCount = await sugarClient.Deleteable<Dt_InterfaceLog>()
|
.Where(x => x.CreateDate < cutoffDate)
|
.ExecuteCommandAsync();
|
|
WriteLog($"手动清理完成,删除 {deletedCount} 条记录");
|
return deletedCount;
|
}
|
}
|
catch (Exception ex)
|
{
|
WriteLog($"手动清理失败: {ex.Message}", true);
|
return 0;
|
}
|
}
|
|
/// <summary>
|
/// 获取清理统计信息
|
/// </summary>
|
public async Task<CleanupStatistics> GetStatistics()
|
{
|
try
|
{
|
using (SqlSugarClient sugarClient = new SqlSugarClient(new ConnectionConfig()
|
{
|
ConnectionString = DBContext.GetMainConnectionDb().Connection,
|
IsAutoCloseConnection = true,
|
DbType = DBContext.DbType,
|
}))
|
{
|
var totalCount = await sugarClient.Queryable<Dt_InterfaceLog>().CountAsync();
|
var oldestLog = await sugarClient.Queryable<Dt_InterfaceLog>()
|
.OrderBy(x => x.CreateDate)
|
.FirstAsync();
|
var newestLog = await sugarClient.Queryable<Dt_InterfaceLog>()
|
.OrderBy(x => x.CreateDate, OrderByType.Desc)
|
.FirstAsync();
|
|
return new CleanupStatistics
|
{
|
TotalCount = totalCount,
|
OldestLogTime = oldestLog?.CreateDate,
|
NewestLogTime = newestLog?.CreateDate,
|
RetentionDays = RetentionDays,
|
LastCleanupTime = GetLastCleanupTime(),
|
LogFilePath = LogFilePath
|
};
|
}
|
}
|
catch (Exception ex)
|
{
|
WriteLog($"获取统计信息失败: {ex.Message}", true);
|
return null;
|
}
|
}
|
|
private DateTime? GetLastCleanupTime()
|
{
|
try
|
{
|
string logDirectory = Path.GetDirectoryName(LogFilePath);
|
if (Directory.Exists(logDirectory))
|
{
|
var latestLogFile = Directory.GetFiles(logDirectory, "Cleanup_*.log")
|
.OrderByDescending(f => f)
|
.FirstOrDefault();
|
|
if (latestLogFile != null && File.Exists(latestLogFile))
|
{
|
return File.GetLastWriteTime(latestLogFile);
|
}
|
}
|
}
|
catch (Exception ex)
|
{
|
Console.WriteLine($"获取上次清理时间失败: {ex.Message}");
|
}
|
return null;
|
}
|
|
public Task StopAsync(CancellationToken cancellationToken)
|
{
|
_cleanupTimer?.Dispose();
|
WriteLog("日志清理服务已停止");
|
return Task.CompletedTask;
|
}
|
|
public void Dispose()
|
{
|
if (!_isDisposed)
|
{
|
_cleanupTimer?.Dispose();
|
_isDisposed = true;
|
}
|
}
|
}
|
|
/// <summary>
|
/// 清理统计信息
|
/// </summary>
|
public class CleanupStatistics
|
{
|
public int TotalCount { get; set; }
|
public DateTime? OldestLogTime { get; set; }
|
public DateTime? NewestLogTime { get; set; }
|
public int RetentionDays { get; set; }
|
public DateTime? LastCleanupTime { get; set; }
|
public string LogFilePath { get; set; }
|
}
|
}
|