mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-07-13 00:11:23 +08:00
feat(logManagement): implement log management service
This commit is contained in:
131
Api/Management/LogManagementController.cs
Normal file
131
Api/Management/LogManagementController.cs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Foxel.Models;
|
||||||
|
using Foxel.Models.Response.Log;
|
||||||
|
using Foxel.Models.Request.Log;
|
||||||
|
using Foxel.Controllers;
|
||||||
|
using Foxel.Services.Management;
|
||||||
|
|
||||||
|
namespace Foxel.Api.Management;
|
||||||
|
|
||||||
|
[Authorize(Roles = "Administrator")]
|
||||||
|
[Route("api/management/log")]
|
||||||
|
public class LogManagementController(ILogManagementService logManagementService) : BaseApiController
|
||||||
|
{
|
||||||
|
[HttpGet("get_logs")]
|
||||||
|
public async Task<ActionResult<PaginatedResult<LogResponse>>> GetLogs(
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 10,
|
||||||
|
[FromQuery] string? searchQuery = null,
|
||||||
|
[FromQuery] LogLevel? level = null,
|
||||||
|
[FromQuery] DateTime? startDate = null,
|
||||||
|
[FromQuery] DateTime? endDate = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var logs = await logManagementService.GetLogsAsync(page, pageSize, searchQuery, level, startDate, endDate);
|
||||||
|
return PaginatedSuccess(logs.Data, logs.TotalCount, logs.Page, logs.PageSize);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return PaginatedError<LogResponse>($"获取日志列表失败: {ex.Message}", 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("get_log/{id}")]
|
||||||
|
public async Task<ActionResult<BaseResult<LogResponse>>> GetLogById(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var log = await logManagementService.GetLogByIdAsync(id);
|
||||||
|
return Success(log, "日志获取成功");
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException)
|
||||||
|
{
|
||||||
|
return Error<LogResponse>("找不到指定日志", 404);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Error<LogResponse>($"获取日志失败: {ex.Message}", 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("delete_log")]
|
||||||
|
public async Task<ActionResult<BaseResult<bool>>> DeleteLog([FromBody] int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await logManagementService.DeleteLogAsync(id);
|
||||||
|
return Success(result, "日志删除成功");
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException)
|
||||||
|
{
|
||||||
|
return Error<bool>("找不到要删除的日志", 404);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Error<bool>($"删除日志失败: {ex.Message}", 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("batch_delete_logs")]
|
||||||
|
public async Task<ActionResult<BaseResult<BatchDeleteResult>>> BatchDeleteLogs([FromBody] List<int> ids)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (ids.Count == 0)
|
||||||
|
{
|
||||||
|
return Error<BatchDeleteResult>("未提供日志ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await logManagementService.BatchDeleteLogsAsync(ids);
|
||||||
|
return Success(result, $"成功删除 {result.SuccessCount} 条日志,失败 {result.FailedCount} 条");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Error<BatchDeleteResult>($"批量删除日志失败: {ex.Message}", 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("clear_logs")]
|
||||||
|
public async Task<ActionResult<BaseResult<int>>> ClearLogs([FromBody] ClearLogsRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int deletedCount;
|
||||||
|
|
||||||
|
if (request.ClearAll)
|
||||||
|
{
|
||||||
|
deletedCount = await logManagementService.ClearAllLogsAsync();
|
||||||
|
return Success(deletedCount, $"成功清空所有日志,共删除 {deletedCount} 条记录");
|
||||||
|
}
|
||||||
|
else if (request.BeforeDate.HasValue)
|
||||||
|
{
|
||||||
|
deletedCount = await logManagementService.ClearLogsByDateAsync(request.BeforeDate.Value);
|
||||||
|
return Success(deletedCount, $"成功清空 {request.BeforeDate.Value:yyyy-MM-dd} 之前的日志,共删除 {deletedCount} 条记录");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Error<int>("请指定清空条件:要么清空全部,要么指定日期");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Error<int>($"清空日志失败: {ex.Message}", 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("get_statistics")]
|
||||||
|
public async Task<ActionResult<BaseResult<LogStatistics>>> GetLogStatistics()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var statistics = await logManagementService.GetLogStatisticsAsync();
|
||||||
|
return Success(statistics, "日志统计获取成功");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Error<LogStatistics>($"获取日志统计失败: {ex.Message}", 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Extensions/LoggingExtensions.cs
Normal file
21
Extensions/LoggingExtensions.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using Foxel.Services.Logging;
|
||||||
|
|
||||||
|
namespace Foxel.Extensions;
|
||||||
|
|
||||||
|
public static class LoggingExtensions
|
||||||
|
{
|
||||||
|
public static ILoggingBuilder AddDatabaseLogging(this ILoggingBuilder builder, Action<DatabaseLoggerConfiguration>? configure = null)
|
||||||
|
{
|
||||||
|
var config = new DatabaseLoggerConfiguration();
|
||||||
|
configure?.Invoke(config);
|
||||||
|
|
||||||
|
builder.Services.Configure<DatabaseLoggerConfiguration>(options =>
|
||||||
|
{
|
||||||
|
options.MinLevel = config.MinLevel;
|
||||||
|
options.Enabled = config.Enabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<ILoggerProvider, DatabaseLoggerProvider>();
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddSingleton<IAlbumService, AlbumService>();
|
services.AddSingleton<IAlbumService, AlbumService>();
|
||||||
services.AddSingleton<IUserManagementService, UserManagementService>();
|
services.AddSingleton<IUserManagementService, UserManagementService>();
|
||||||
services.AddSingleton<IPictureManagementService, PictureManagementService>();
|
services.AddSingleton<IPictureManagementService, PictureManagementService>();
|
||||||
|
services.AddSingleton<ILogManagementService, LogManagementService>();
|
||||||
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
||||||
services.AddHostedService<QueuedHostedService>();
|
services.AddHostedService<QueuedHostedService>();
|
||||||
services.AddSingleton<LocalStorageProvider>();
|
services.AddSingleton<LocalStorageProvider>();
|
||||||
|
|||||||
49
Models/DataBase/Log.cs
Normal file
49
Models/DataBase/Log.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace Foxel.Models.DataBase;
|
||||||
|
|
||||||
|
public class Log : BaseModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public LogLevel Level { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[StringLength(4000)]
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[StringLength(255)]
|
||||||
|
public string? Category { get; set; }
|
||||||
|
|
||||||
|
public int? EventId { get; set; }
|
||||||
|
|
||||||
|
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
[Column(TypeName = "text")]
|
||||||
|
public string? Exception { get; set; }
|
||||||
|
|
||||||
|
[StringLength(255)]
|
||||||
|
public string? RequestPath { get; set; }
|
||||||
|
|
||||||
|
[StringLength(50)]
|
||||||
|
public string? RequestMethod { get; set; }
|
||||||
|
|
||||||
|
public int? StatusCode { get; set; }
|
||||||
|
|
||||||
|
public int? UserId { get; set; }
|
||||||
|
|
||||||
|
[ForeignKey("UserId")]
|
||||||
|
public User? User { get; set; }
|
||||||
|
|
||||||
|
[StringLength(50)]
|
||||||
|
public string? IPAddress { get; set; }
|
||||||
|
|
||||||
|
[StringLength(255)]
|
||||||
|
public string? Application { get; set; } = "Foxel";
|
||||||
|
|
||||||
|
[StringLength(255)]
|
||||||
|
public string? MachineName { get; set; } = Environment.MachineName;
|
||||||
|
|
||||||
|
[Column(TypeName = "text")]
|
||||||
|
public string? Properties { get; set; } // JSON格式存储额外属性
|
||||||
|
}
|
||||||
7
Models/Request/Log/ClearLogsRequest.cs
Normal file
7
Models/Request/Log/ClearLogsRequest.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Foxel.Models.Request.Log;
|
||||||
|
|
||||||
|
public class ClearLogsRequest
|
||||||
|
{
|
||||||
|
public DateTime? BeforeDate { get; set; }
|
||||||
|
public bool ClearAll { get; set; } = false;
|
||||||
|
}
|
||||||
18
Models/Response/Log/LogResponse.cs
Normal file
18
Models/Response/Log/LogResponse.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Foxel.Models.Response.Log;
|
||||||
|
|
||||||
|
public class LogResponse
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public LogLevel Level { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public string Category { get; set; } = string.Empty;
|
||||||
|
public int? EventId { get; set; }
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
public string? Exception { get; set; }
|
||||||
|
public string? RequestPath { get; set; }
|
||||||
|
public string? RequestMethod { get; set; }
|
||||||
|
public int? StatusCode { get; set; }
|
||||||
|
public string? IPAddress { get; set; }
|
||||||
|
public int? UserId { get; set; }
|
||||||
|
public string? Properties { get; set; }
|
||||||
|
}
|
||||||
24
Models/Response/Log/LogStatistics.cs
Normal file
24
Models/Response/Log/LogStatistics.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
namespace Foxel.Models.Response.Log;
|
||||||
|
|
||||||
|
public class LogStatistics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 总日志数
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 今日日志数
|
||||||
|
/// </summary>
|
||||||
|
public int TodayCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误日志数(Error + Critical)
|
||||||
|
/// </summary>
|
||||||
|
public int ErrorCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 警告日志数
|
||||||
|
/// </summary>
|
||||||
|
public int WarningCount { get; set; }
|
||||||
|
}
|
||||||
@@ -12,4 +12,5 @@ public class MyDbContext(DbContextOptions<MyDbContext> options) : DbContext(opti
|
|||||||
public DbSet<Favorite> Favorites { get; set; } = null!;
|
public DbSet<Favorite> Favorites { get; set; } = null!;
|
||||||
public DbSet<Album> Albums { get; set; } = null!;
|
public DbSet<Album> Albums { get; set; } = null!;
|
||||||
public DbSet<Role> Roles { get; set; } = null!;
|
public DbSet<Role> Roles { get; set; } = null!;
|
||||||
|
public DbSet<Log> Logs { get; set; } = null!;
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,11 @@ using Microsoft.AspNetCore.HttpOverrides;
|
|||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
var environment = builder.Environment;
|
var environment = builder.Environment;
|
||||||
Console.WriteLine($"当前环境: {environment.EnvironmentName}");
|
Console.WriteLine($"当前环境: {environment.EnvironmentName}");
|
||||||
|
builder.Logging.AddDatabaseLogging(config =>
|
||||||
|
{
|
||||||
|
config.MinLevel = LogLevel.Information;
|
||||||
|
config.Enabled = true;
|
||||||
|
});
|
||||||
builder.Services.AddMemoryCache();
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.AddApplicationDbContext(builder.Configuration);
|
builder.Services.AddApplicationDbContext(builder.Configuration);
|
||||||
builder.Services.AddApplicationOpenApi();
|
builder.Services.AddApplicationOpenApi();
|
||||||
|
|||||||
@@ -5,29 +5,21 @@ using Foxel.Utils;
|
|||||||
|
|
||||||
namespace Foxel.Services.AI;
|
namespace Foxel.Services.AI;
|
||||||
|
|
||||||
public class AiService : IAiService
|
public class AiService(IHttpClientFactory httpClientFactory, IConfigService configService, ILogger<AiService> logger)
|
||||||
|
: IAiService
|
||||||
{
|
{
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
|
||||||
private readonly IConfigService _configService;
|
|
||||||
private HttpClient? _httpClient;
|
private HttpClient? _httpClient;
|
||||||
private string? _currentApiKey;
|
private string? _currentApiKey;
|
||||||
private string? _currentBaseUrl;
|
private string? _currentBaseUrl;
|
||||||
|
|
||||||
public AiService(IHttpClientFactory httpClientFactory, IConfigService configService)
|
|
||||||
{
|
|
||||||
_httpClientFactory = httpClientFactory;
|
|
||||||
_configService = configService;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpClient ConfigureHttpClient()
|
private HttpClient ConfigureHttpClient()
|
||||||
{
|
{
|
||||||
string apiKey = _configService["AI:ApiKey"];
|
string apiKey = configService["AI:ApiKey"];
|
||||||
string baseUrl = _configService["AI:ApiEndpoint"];
|
string baseUrl = configService["AI:ApiEndpoint"];
|
||||||
|
|
||||||
// 检查是否需要重新创建 HttpClient
|
|
||||||
if (_httpClient == null || _currentApiKey != apiKey || _currentBaseUrl != baseUrl)
|
if (_httpClient == null || _currentApiKey != apiKey || _currentBaseUrl != baseUrl)
|
||||||
{
|
{
|
||||||
_httpClient = _httpClientFactory.CreateClient();
|
_httpClient = httpClientFactory.CreateClient();
|
||||||
_httpClient.BaseAddress = new Uri(baseUrl);
|
_httpClient.BaseAddress = new Uri(baseUrl);
|
||||||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||||
|
|
||||||
@@ -44,7 +36,7 @@ public class AiService : IAiService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var client = ConfigureHttpClient();
|
var client = ConfigureHttpClient();
|
||||||
string model = _configService["AI:Model"];
|
string model = configService["AI:Model"];
|
||||||
var imageUrl = new ImageUrl
|
var imageUrl = new ImageUrl
|
||||||
{
|
{
|
||||||
Url = $"data:image/jpeg;base64,{base64Image}"
|
Url = $"data:image/jpeg;base64,{base64Image}"
|
||||||
@@ -59,7 +51,7 @@ public class AiService : IAiService
|
|||||||
var textContent = new TextContent
|
var textContent = new TextContent
|
||||||
{
|
{
|
||||||
Type = "text",
|
Type = "text",
|
||||||
Text = _configService["AI:ImageAnalysisPrompt"] ??
|
Text = configService["AI:ImageAnalysisPrompt"] ??
|
||||||
"请详细分析这张图片,并提供全面的描述,以便用于向量嵌入和基于文本的图像搜索。描述需要包含:主体对象、场景环境、色彩特点、构图布局、风格特征、情绪氛围、细节特征等关键元素。请提供一个简短有力的标题,然后提供详细描述。\n\n请以JSON格式返回,格式如下:\n{\"title\": \"简短概括图片的核心内容\", \"description\": \"全面详细的描述,包含上述所有元素,使用丰富精确的词汇,避免笼统表达\"}\n\n请确保返回有效的JSON格式。"
|
"请详细分析这张图片,并提供全面的描述,以便用于向量嵌入和基于文本的图像搜索。描述需要包含:主体对象、场景环境、色彩特点、构图布局、风格特征、情绪氛围、细节特征等关键元素。请提供一个简短有力的标题,然后提供详细描述。\n\n请以JSON格式返回,格式如下:\n{\"title\": \"简短概括图片的核心内容\", \"description\": \"全面详细的描述,包含上述所有元素,使用丰富精确的词汇,避免笼统表达\"}\n\n请确保返回有效的JSON格式。"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,7 +86,7 @@ public class AiService : IAiService
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"AI分析图片时出错: {ex.Message}");
|
logger.LogError(ex, "AI分析图片时出错");
|
||||||
return ("处理失败", $"AI分析过程中发生错误: {ex.Message}");
|
return ("处理失败", $"AI分析过程中发生错误: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,10 +100,10 @@ public class AiService : IAiService
|
|||||||
if (availableTags.Count == 0)
|
if (availableTags.Count == 0)
|
||||||
return new List<string>();
|
return new List<string>();
|
||||||
|
|
||||||
string model = _configService["AI:Model"];
|
string model = configService["AI:Model"];
|
||||||
var tagsText = string.Join(", ", availableTags);
|
var tagsText = string.Join(", ", availableTags);
|
||||||
|
|
||||||
string promptTemplate = _configService["AI:TagMatchingPrompt"] ??
|
string promptTemplate = configService["AI:TagMatchingPrompt"] ??
|
||||||
"以下是一组标签:[{tagsText}]。\n\n请从这些标签中严格选择与下面描述内容高度相关的标签(最多选择5个)。只选择确实匹配的标签,如果找不到完全匹配或高度相关的标签,宁可返回空数组也不要选择不太相关的标签。\n\n描述内容:{description}\n\n请以JSON格式返回,格式如下:\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\"]}}\n\n请确保返回有效的JSON格式前面不要加```,并且只包含确实匹配的标签名称。";
|
"以下是一组标签:[{tagsText}]。\n\n请从这些标签中严格选择与下面描述内容高度相关的标签(最多选择5个)。只选择确实匹配的标签,如果找不到完全匹配或高度相关的标签,宁可返回空数组也不要选择不太相关的标签。\n\n描述内容:{description}\n\n请以JSON格式返回,格式如下:\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\"]}}\n\n请确保返回有效的JSON格式前面不要加```,并且只包含确实匹配的标签名称。";
|
||||||
|
|
||||||
// 替换占位符
|
// 替换占位符
|
||||||
@@ -209,7 +201,7 @@ public class AiService : IAiService
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"AI匹配标签时出错: {ex.Message}");
|
logger.LogError(ex, "AI匹配标签时出错");
|
||||||
return new List<string>();
|
return new List<string>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,7 +214,7 @@ public class AiService : IAiService
|
|||||||
// 获取配置好的 HttpClient
|
// 获取配置好的 HttpClient
|
||||||
var client = ConfigureHttpClient();
|
var client = ConfigureHttpClient();
|
||||||
|
|
||||||
string model = _configService["AI:Model"];
|
string model = configService["AI:Model"];
|
||||||
|
|
||||||
var imageUrl = new ImageUrl
|
var imageUrl = new ImageUrl
|
||||||
{
|
{
|
||||||
@@ -240,7 +232,7 @@ public class AiService : IAiService
|
|||||||
if (allowNewTags)
|
if (allowNewTags)
|
||||||
{
|
{
|
||||||
// 获取配置的标签生成提示词,如果没有则使用默认提示词
|
// 获取配置的标签生成提示词,如果没有则使用默认提示词
|
||||||
string defaultPrompt = _configService["AI:TagGenerationPrompt"] ??
|
string defaultPrompt = configService["AI:TagGenerationPrompt"] ??
|
||||||
"请为图片生成5个最相关的标签,每个标签应该是简短且描述性的词语或短语。\n\n请以JSON格式返回,格式如下:\n{\"tags\": [\"标签1\", \"标签2\", \"标签3\", \"标签4\", \"标签5\"]}\n\n请确保返回有效的JSON格式。";
|
"请为图片生成5个最相关的标签,每个标签应该是简短且描述性的词语或短语。\n\n请以JSON格式返回,格式如下:\n{\"tags\": [\"标签1\", \"标签2\", \"标签3\", \"标签4\", \"标签5\"]}\n\n请确保返回有效的JSON格式。";
|
||||||
|
|
||||||
// 如果允许新标签,则提供现有标签作为参考,但允许生成新标签
|
// 如果允许新标签,则提供现有标签作为参考,但允许生成新标签
|
||||||
@@ -255,7 +247,7 @@ public class AiService : IAiService
|
|||||||
return new List<string>();
|
return new List<string>();
|
||||||
|
|
||||||
var tagsText = string.Join(", ", availableTags);
|
var tagsText = string.Join(", ", availableTags);
|
||||||
string templatePrompt = _configService["AI:TagMatchingPrompt"] ??
|
string templatePrompt = configService["AI:TagMatchingPrompt"] ??
|
||||||
"以下是一组标签:[{tagsText}]。\n\n请从这些标签中严格选择与图片内容高度相关的标签(最多选择5个)。只选择确实匹配的标签,如果找不到完全匹配或高度相关的标签,宁可返回空数组也不要选择不太相关的标签。\n\n请以JSON格式返回,格式如下:\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\"]}}\n\n请确保返回有效的JSON格式,并且只包含上述列表中的标签名称。";
|
"以下是一组标签:[{tagsText}]。\n\n请从这些标签中严格选择与图片内容高度相关的标签(最多选择5个)。只选择确实匹配的标签,如果找不到完全匹配或高度相关的标签,宁可返回空数组也不要选择不太相关的标签。\n\n请以JSON格式返回,格式如下:\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\"]}}\n\n请确保返回有效的JSON格式,并且只包含上述列表中的标签名称。";
|
||||||
|
|
||||||
promptText = templatePrompt.Replace("{tagsText}", tagsText);
|
promptText = templatePrompt.Replace("{tagsText}", tagsText);
|
||||||
@@ -358,7 +350,7 @@ public class AiService : IAiService
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"AI从图片生成标签时出错: {ex.Message}");
|
logger.LogError(ex, "AI从图片生成标签时出错");
|
||||||
return new List<string>();
|
return new List<string>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,7 +362,7 @@ public class AiService : IAiService
|
|||||||
// 获取配置好的 HttpClient
|
// 获取配置好的 HttpClient
|
||||||
var client = ConfigureHttpClient();
|
var client = ConfigureHttpClient();
|
||||||
|
|
||||||
string model = _configService["AI:EmbeddingModel"];
|
string model = configService["AI:EmbeddingModel"];
|
||||||
|
|
||||||
var requestContent = new
|
var requestContent = new
|
||||||
{
|
{
|
||||||
@@ -385,7 +377,7 @@ public class AiService : IAiService
|
|||||||
var embedResult = await response.Content.ReadFromJsonAsync<EmbeddingResponse>();
|
var embedResult = await response.Content.ReadFromJsonAsync<EmbeddingResponse>();
|
||||||
if (embedResult?.Data == null || embedResult.Data.Length == 0)
|
if (embedResult?.Data == null || embedResult.Data.Length == 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine("嵌入向量API返回空结果");
|
logger.LogWarning("嵌入向量API返回空结果");
|
||||||
return Array.Empty<float>();
|
return Array.Empty<float>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +385,7 @@ public class AiService : IAiService
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"获取嵌入向量时出错: {ex.Message}");
|
logger.LogError(ex, "获取嵌入向量时出错");
|
||||||
return Array.Empty<float>();
|
return Array.Empty<float>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ using Foxel.Models.DataBase;
|
|||||||
using Foxel.Models.Request.Auth;
|
using Foxel.Models.Request.Auth;
|
||||||
using Foxel.Services.Configuration;
|
using Foxel.Services.Configuration;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using static Foxel.Utils.AuthHelper;
|
using static Foxel.Utils.AuthHelper;
|
||||||
|
|
||||||
namespace Foxel.Services.Auth;
|
namespace Foxel.Services.Auth;
|
||||||
|
|
||||||
public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfigService configuration)
|
public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfigService configuration, ILogger<AuthService> logger)
|
||||||
: IAuthService
|
: IAuthService
|
||||||
{
|
{
|
||||||
public async Task<(bool success, string message, User? user)> RegisterUserAsync(RegisterRequest request)
|
public async Task<(bool success, string message, User? user)> RegisterUserAsync(RegisterRequest request)
|
||||||
@@ -218,7 +219,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
if (!tokenResponse.IsSuccessStatusCode)
|
if (!tokenResponse.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errorContent = await tokenResponse.Content.ReadAsStringAsync();
|
var errorContent = await tokenResponse.Content.ReadAsStringAsync();
|
||||||
Console.WriteLine($"获取GitHub访问令牌失败: {tokenResponse.StatusCode}, {errorContent}");
|
logger.LogError("获取GitHub访问令牌失败: {StatusCode}, {ErrorContent}", tokenResponse.StatusCode, errorContent);
|
||||||
return (GitHubAuthResult.TokenRequestFailed, $"获取GitHub访问令牌失败: {errorContent}", null);
|
return (GitHubAuthResult.TokenRequestFailed, $"获取GitHub访问令牌失败: {errorContent}", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +229,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
if (!tokenJson.RootElement.TryGetProperty("access_token", out var accessTokenElement) ||
|
if (!tokenJson.RootElement.TryGetProperty("access_token", out var accessTokenElement) ||
|
||||||
accessTokenElement.GetString() == null)
|
accessTokenElement.GetString() == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"GitHub响应中未找到access_token: {tokenResponseContent}");
|
logger.LogError("GitHub响应中未找到access_token: {TokenResponseContent}", tokenResponseContent);
|
||||||
return (GitHubAuthResult.TokenRequestFailed, "获取GitHub访问令牌失败,响应中未包含令牌。", null);
|
return (GitHubAuthResult.TokenRequestFailed, "获取GitHub访问令牌失败,响应中未包含令牌。", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +242,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
if (!userResponse.IsSuccessStatusCode)
|
if (!userResponse.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errorContent = await userResponse.Content.ReadAsStringAsync();
|
var errorContent = await userResponse.Content.ReadAsStringAsync();
|
||||||
Console.WriteLine($"获取GitHub用户信息失败: {userResponse.StatusCode}, {errorContent}");
|
logger.LogError("获取GitHub用户信息失败: {StatusCode}, {ErrorContent}", userResponse.StatusCode, errorContent);
|
||||||
return (GitHubAuthResult.UserInfoFailed, $"获取GitHub用户信息失败: {errorContent}", null);
|
return (GitHubAuthResult.UserInfoFailed, $"获取GitHub用户信息失败: {errorContent}", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +331,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
if (!tokenResponse.IsSuccessStatusCode)
|
if (!tokenResponse.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errorContent = await tokenResponse.Content.ReadAsStringAsync();
|
var errorContent = await tokenResponse.Content.ReadAsStringAsync();
|
||||||
Console.WriteLine($"获取LinuxDo访问令牌失败: {tokenResponse.StatusCode}, {errorContent}");
|
logger.LogError("获取LinuxDo访问令牌失败: {StatusCode}, {ErrorContent}", tokenResponse.StatusCode, errorContent);
|
||||||
return (LinuxDoAuthResult.TokenRequestFailed, $"获取LinuxDo访问令牌失败: {errorContent}", null);
|
return (LinuxDoAuthResult.TokenRequestFailed, $"获取LinuxDo访问令牌失败: {errorContent}", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +341,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
if (!tokenJson.RootElement.TryGetProperty("access_token", out var accessTokenElement) ||
|
if (!tokenJson.RootElement.TryGetProperty("access_token", out var accessTokenElement) ||
|
||||||
accessTokenElement.GetString() == null)
|
accessTokenElement.GetString() == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"LinuxDo响应中未找到access_token: {tokenResponseContent}");
|
logger.LogError("LinuxDo响应中未找到access_token: {TokenResponseContent}", tokenResponseContent);
|
||||||
return (LinuxDoAuthResult.TokenRequestFailed, "获取LinuxDo访问令牌失败,响应中未包含令牌。", null);
|
return (LinuxDoAuthResult.TokenRequestFailed, "获取LinuxDo访问令牌失败,响应中未包含令牌。", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +355,7 @@ public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfi
|
|||||||
if (!userResponse.IsSuccessStatusCode)
|
if (!userResponse.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errorContent = await userResponse.Content.ReadAsStringAsync();
|
var errorContent = await userResponse.Content.ReadAsStringAsync();
|
||||||
Console.WriteLine($"获取LinuxDo用户信息失败: {userResponse.StatusCode}, {errorContent}");
|
logger.LogError("获取LinuxDo用户信息失败: {StatusCode}, {ErrorContent}", userResponse.StatusCode, errorContent);
|
||||||
return (LinuxDoAuthResult.UserInfoFailed, $"获取LinuxDo用户信息失败: {errorContent}", null);
|
return (LinuxDoAuthResult.UserInfoFailed, $"获取LinuxDo用户信息失败: {errorContent}", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,14 +22,17 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
|
|||||||
private readonly SemaphoreSlim _signal;
|
private readonly SemaphoreSlim _signal;
|
||||||
private readonly int _maxConcurrentTasks;
|
private readonly int _maxConcurrentTasks;
|
||||||
private bool _isDisposed;
|
private bool _isDisposed;
|
||||||
|
private readonly ILogger<BackgroundTaskQueue> _logger;
|
||||||
|
|
||||||
public BackgroundTaskQueue(
|
public BackgroundTaskQueue(
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
IDbContextFactory<MyDbContext> contextFactory,
|
IDbContextFactory<MyDbContext> contextFactory,
|
||||||
IConfigService configuration)
|
IConfigService configuration,
|
||||||
|
ILogger<BackgroundTaskQueue> logger)
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
_contextFactory = contextFactory;
|
_contextFactory = contextFactory;
|
||||||
|
_logger = logger;
|
||||||
_activeTasks = new ConcurrentDictionary<Guid, PictureProcessingTask>();
|
_activeTasks = new ConcurrentDictionary<Guid, PictureProcessingTask>();
|
||||||
_pictureStatus = new ConcurrentDictionary<int, PictureProcessingStatus>();
|
_pictureStatus = new ConcurrentDictionary<int, PictureProcessingStatus>();
|
||||||
_processingTasks = new List<Task>();
|
_processingTasks = new List<Task>();
|
||||||
@@ -120,7 +123,7 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
|
|||||||
|
|
||||||
if (unfinishedPictures.Any())
|
if (unfinishedPictures.Any())
|
||||||
{
|
{
|
||||||
Console.WriteLine($"正在恢复 {unfinishedPictures.Count} 个未完成的图片处理任务");
|
_logger.LogInformation("正在恢复 {Count} 个未完成的图片处理任务", unfinishedPictures.Count);
|
||||||
|
|
||||||
foreach (var picture in unfinishedPictures)
|
foreach (var picture in unfinishedPictures)
|
||||||
{
|
{
|
||||||
@@ -131,14 +134,14 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
|
|||||||
{
|
{
|
||||||
// 重新加入队列
|
// 重新加入队列
|
||||||
await QueuePictureProcessingTaskAsync(picture.Id, originalFilePath);
|
await QueuePictureProcessingTaskAsync(picture.Id, originalFilePath);
|
||||||
Console.WriteLine($"已恢复图片处理任务: ID={picture.Id}, 路径={originalFilePath}");
|
_logger.LogInformation("已恢复图片处理任务: ID={PictureId}, 路径={FilePath}", picture.Id, originalFilePath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 如果文件不存在,则标记为失败
|
// 如果文件不存在,则标记为失败
|
||||||
picture.ProcessingStatus = ProcessingStatus.Failed;
|
picture.ProcessingStatus = ProcessingStatus.Failed;
|
||||||
picture.ProcessingError = "系统重启后找不到原始图片文件";
|
picture.ProcessingError = "系统重启后找不到原始图片文件";
|
||||||
Console.WriteLine($"无法恢复图片处理任务: ID={picture.Id}, 找不到文件: {originalFilePath}");
|
_logger.LogWarning("无法恢复图片处理任务: ID={PictureId}, 找不到文件: {FilePath}", picture.Id, originalFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,12 +149,12 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.WriteLine("没有需要恢复的图片处理任务");
|
_logger.LogInformation("没有需要恢复的图片处理任务");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"恢复未完成的任务时发生错误: {ex.Message}");
|
_logger.LogError(ex, "恢复未完成的任务时发生错误");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,12 +410,12 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception saveEx)
|
catch (Exception saveEx)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"保存失败状态时出错: {saveEx.Message}");
|
_logger.LogError(saveEx, "保存失败状态时出错");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录错误日志
|
// 记录错误日志
|
||||||
Console.WriteLine($"图片处理失败: 图片ID={task.PictureId}, 错误: {ex.Message}");
|
_logger.LogError(ex, "图片处理失败: 图片ID={PictureId}", task.PictureId);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -425,7 +428,7 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"删除临时文件失败: {localFilePath}, 错误: {ex.Message}");
|
_logger.LogWarning(ex, "删除临时文件失败: {FilePath}", localFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,7 +474,14 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
|
|||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
_signal.Dispose();
|
_signal.Dispose();
|
||||||
Task.WhenAll(_processingTasks).Wait(5000);
|
try
|
||||||
|
{
|
||||||
|
Task.WhenAll(_processingTasks).Wait(5000);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "等待处理任务完成时超时");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_isDisposed = true;
|
_isDisposed = true;
|
||||||
|
|||||||
@@ -37,29 +37,41 @@ public class ConfigService(
|
|||||||
return cachedValue;
|
return cachedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果缓存中没有,从数据库获取
|
try
|
||||||
await using var context = await contextFactory.CreateDbContextAsync();
|
|
||||||
var config = await context.Configs.FirstOrDefaultAsync(c => c.Key == key);
|
|
||||||
|
|
||||||
if (config == null)
|
|
||||||
{
|
{
|
||||||
// 尝试从环境变量获取
|
// 如果缓存中没有,从数据库获取
|
||||||
string envVarKey = key.ToUpper().Replace(".", "_").Replace("-", "_");
|
await using var context = await contextFactory.CreateDbContextAsync();
|
||||||
string? envVarValue = Environment.GetEnvironmentVariable(envVarKey);
|
var config = await context.Configs.FirstOrDefaultAsync(c => c.Key == key);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(envVarValue))
|
if (config == null)
|
||||||
{
|
{
|
||||||
memoryCache.Set($"config:{key}", envVarValue, _cacheExpiration);
|
// 尝试从环境变量获取
|
||||||
return envVarValue;
|
string envVarKey = key.ToUpper().Replace(".", "_").Replace("-", "_");
|
||||||
|
string? envVarValue = Environment.GetEnvironmentVariable(envVarKey);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(envVarValue))
|
||||||
|
{
|
||||||
|
memoryCache.Set($"config:{key}", envVarValue, _cacheExpiration);
|
||||||
|
return envVarValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将配置值添加到缓存
|
||||||
|
memoryCache.Set($"config:{key}", config.Value, _cacheExpiration);
|
||||||
|
|
||||||
|
return config.Value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 在数据库初始化期间,可能会出现表不存在的情况,这时静默处理
|
||||||
|
if (!ex.Message.Contains("does not exist"))
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "获取配置值时出错: {Key}", key);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将配置值添加到缓存
|
|
||||||
memoryCache.Set($"config:{key}", config.Value, _cacheExpiration);
|
|
||||||
|
|
||||||
return config.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T?> GetValueAsync<T>(string key, T? defaultValue = default)
|
public async Task<T?> GetValueAsync<T>(string key, T? defaultValue = default)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Foxel.Models.DataBase;
|
using Foxel.Models.DataBase;
|
||||||
using Foxel.Services.Configuration;
|
using Foxel.Services.Configuration;
|
||||||
|
using Foxel.Services.Logging;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Foxel.Services.Initializer;
|
namespace Foxel.Services.Initializer;
|
||||||
@@ -14,6 +15,9 @@ public class DatabaseInitializer(
|
|||||||
|
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
|
// 在初始化期间禁用数据库日志记录
|
||||||
|
DatabaseLogger.SetDatabaseReady(false);
|
||||||
|
|
||||||
logger.LogInformation("开始检查数据库初始化状态...");
|
logger.LogInformation("开始检查数据库初始化状态...");
|
||||||
|
|
||||||
// 执行数据库迁移
|
// 执行数据库迁移
|
||||||
@@ -24,6 +28,8 @@ public class DatabaseInitializer(
|
|||||||
configService[InitializationFlag] == "true")
|
configService[InitializationFlag] == "true")
|
||||||
{
|
{
|
||||||
logger.LogInformation("数据库已完成初始化,跳过初始化步骤");
|
logger.LogInformation("数据库已完成初始化,跳过初始化步骤");
|
||||||
|
// 启用数据库日志记录
|
||||||
|
DatabaseLogger.SetDatabaseReady(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +92,9 @@ public class DatabaseInitializer(
|
|||||||
await configService.SetConfigAsync(InitializationFlag, "true", "系统初始化完成标志");
|
await configService.SetConfigAsync(InitializationFlag, "true", "系统初始化完成标志");
|
||||||
|
|
||||||
logger.LogInformation("数据库配置初始化完成");
|
logger.LogInformation("数据库配置初始化完成");
|
||||||
|
|
||||||
|
// 初始化完成后启用数据库日志记录
|
||||||
|
DatabaseLogger.SetDatabaseReady(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task MigrateDatabaseAsync()
|
private async Task MigrateDatabaseAsync()
|
||||||
|
|||||||
112
Services/Logging/DatabaseLogger.cs
Normal file
112
Services/Logging/DatabaseLogger.cs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Foxel.Models.DataBase;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Foxel.Services.Logging;
|
||||||
|
|
||||||
|
public class DatabaseLogger(string categoryName, IServiceProvider serviceProvider, DatabaseLoggerConfiguration config)
|
||||||
|
: ILogger
|
||||||
|
{
|
||||||
|
private static volatile bool _isDatabaseReady;
|
||||||
|
|
||||||
|
public static void SetDatabaseReady(bool isReady)
|
||||||
|
{
|
||||||
|
_isDatabaseReady = isReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable BeginScope<TState>(TState state) where TState : notnull => null!;
|
||||||
|
|
||||||
|
public bool IsEnabled(LogLevel logLevel) => logLevel >= config.MinLevel;
|
||||||
|
|
||||||
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||||
|
Func<TState, Exception?, string> formatter)
|
||||||
|
{
|
||||||
|
if (!IsEnabled(logLevel) || !_isDatabaseReady)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var message = formatter(state, exception);
|
||||||
|
if (string.IsNullOrEmpty(message))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = serviceProvider.CreateScope();
|
||||||
|
var contextFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<MyDbContext>>();
|
||||||
|
var httpContextAccessor = scope.ServiceProvider.GetService<IHttpContextAccessor>();
|
||||||
|
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
if (!await IsDatabaseAvailableAsync(context))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var httpContext = httpContextAccessor?.HttpContext;
|
||||||
|
|
||||||
|
var log = new Log
|
||||||
|
{
|
||||||
|
Level = logLevel,
|
||||||
|
Message = message.Length > 4000 ? message[..4000] : message,
|
||||||
|
Category = categoryName,
|
||||||
|
EventId = eventId.Id,
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
Exception = exception?.ToString(),
|
||||||
|
RequestPath = httpContext?.Request.Path.ToString(),
|
||||||
|
RequestMethod = httpContext?.Request.Method,
|
||||||
|
StatusCode = httpContext?.Response.StatusCode,
|
||||||
|
IPAddress = httpContext?.Connection.RemoteIpAddress?.ToString(),
|
||||||
|
Properties = SerializeState(state)
|
||||||
|
};
|
||||||
|
if (httpContext?.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
var userIdClaim = httpContext.User.FindFirst("UserId");
|
||||||
|
if (userIdClaim != null && int.TryParse(userIdClaim.Value, out var userId))
|
||||||
|
{
|
||||||
|
log.UserId = userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Logs.Add(log);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"写入数据库日志时出错: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<bool> IsDatabaseAvailableAsync(MyDbContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await context.Database.ExecuteSqlRawAsync("SELECT 1 FROM \"Logs\" LIMIT 1");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? SerializeState<TState>(TState state)
|
||||||
|
{
|
||||||
|
if (state is string)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = false,
|
||||||
|
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||||
|
};
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(state, options);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return state?.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Services/Logging/DatabaseLoggerConfiguration.cs
Normal file
7
Services/Logging/DatabaseLoggerConfiguration.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Foxel.Services.Logging;
|
||||||
|
|
||||||
|
public class DatabaseLoggerConfiguration
|
||||||
|
{
|
||||||
|
public LogLevel MinLevel { get; set; } = LogLevel.Information;
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
}
|
||||||
23
Services/Logging/DatabaseLoggerProvider.cs
Normal file
23
Services/Logging/DatabaseLoggerProvider.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Foxel.Services.Logging;
|
||||||
|
|
||||||
|
[ProviderAlias("Database")]
|
||||||
|
public class DatabaseLoggerProvider : ILoggerProvider
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly DatabaseLoggerConfiguration _config;
|
||||||
|
|
||||||
|
public DatabaseLoggerProvider(IServiceProvider serviceProvider, IOptions<DatabaseLoggerConfiguration> config)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_config = config.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogger CreateLogger(string categoryName)
|
||||||
|
{
|
||||||
|
return new DatabaseLogger(categoryName, _serviceProvider, _config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
20
Services/Management/ILogManagementService.cs
Normal file
20
Services/Management/ILogManagementService.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using Foxel.Models;
|
||||||
|
using Foxel.Models.Response.Log;
|
||||||
|
|
||||||
|
namespace Foxel.Services.Management;
|
||||||
|
|
||||||
|
public interface ILogManagementService
|
||||||
|
{
|
||||||
|
Task<PaginatedResult<LogResponse>> GetLogsAsync(int page, int pageSize, string? searchQuery = null, LogLevel? level = null, DateTime? startDate = null, DateTime? endDate = null);
|
||||||
|
Task<LogResponse> GetLogByIdAsync(int id);
|
||||||
|
Task<bool> DeleteLogAsync(int id);
|
||||||
|
Task<BatchDeleteResult> BatchDeleteLogsAsync(List<int> ids);
|
||||||
|
Task<int> ClearLogsByDateAsync(DateTime beforeDate);
|
||||||
|
Task<int> ClearAllLogsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取日志统计信息
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>日志统计数据</returns>
|
||||||
|
Task<LogStatistics> GetLogStatisticsAsync();
|
||||||
|
}
|
||||||
181
Services/Management/LogManagementService.cs
Normal file
181
Services/Management/LogManagementService.cs
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Foxel.Models;
|
||||||
|
using Foxel.Models.DataBase;
|
||||||
|
using Foxel.Models.Response.Log;
|
||||||
|
|
||||||
|
namespace Foxel.Services.Management;
|
||||||
|
|
||||||
|
public class LogManagementService(IDbContextFactory<MyDbContext> contextFactory) : ILogManagementService
|
||||||
|
{
|
||||||
|
public async Task<PaginatedResult<LogResponse>> GetLogsAsync(int page, int pageSize, string? searchQuery = null, LogLevel? level = null, DateTime? startDate = null, DateTime? endDate = null)
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var query = context.Logs.AsQueryable();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(searchQuery))
|
||||||
|
{
|
||||||
|
query = query.Where(l => l.Message.Contains(searchQuery) ||
|
||||||
|
l.Category.Contains(searchQuery) ||
|
||||||
|
(l.Exception != null && l.Exception.Contains(searchQuery)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(l => l.Level == level.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(l => l.Timestamp >= startDate.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(l => l.Timestamp <= endDate.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync();
|
||||||
|
|
||||||
|
var logs = await query
|
||||||
|
.OrderByDescending(l => l.Timestamp)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(l => new LogResponse
|
||||||
|
{
|
||||||
|
Id = l.Id,
|
||||||
|
Level = l.Level,
|
||||||
|
Message = l.Message,
|
||||||
|
Category = l.Category,
|
||||||
|
EventId = l.EventId,
|
||||||
|
Timestamp = l.Timestamp,
|
||||||
|
Exception = l.Exception,
|
||||||
|
RequestPath = l.RequestPath,
|
||||||
|
RequestMethod = l.RequestMethod,
|
||||||
|
StatusCode = l.StatusCode,
|
||||||
|
IPAddress = l.IPAddress,
|
||||||
|
UserId = l.UserId,
|
||||||
|
Properties = l.Properties
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return new PaginatedResult<LogResponse>
|
||||||
|
{
|
||||||
|
Data = logs,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LogResponse> GetLogByIdAsync(int id)
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var log = await context.Logs.FirstOrDefaultAsync(l => l.Id == id);
|
||||||
|
if (log == null)
|
||||||
|
throw new KeyNotFoundException($"找不到ID为 {id} 的日志");
|
||||||
|
|
||||||
|
return new LogResponse
|
||||||
|
{
|
||||||
|
Id = log.Id,
|
||||||
|
Level = log.Level,
|
||||||
|
Message = log.Message,
|
||||||
|
Category = log.Category,
|
||||||
|
EventId = log.EventId,
|
||||||
|
Timestamp = log.Timestamp,
|
||||||
|
Exception = log.Exception,
|
||||||
|
RequestPath = log.RequestPath,
|
||||||
|
RequestMethod = log.RequestMethod,
|
||||||
|
StatusCode = log.StatusCode,
|
||||||
|
IPAddress = log.IPAddress,
|
||||||
|
UserId = log.UserId,
|
||||||
|
Properties = log.Properties
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteLogAsync(int id)
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var log = await context.Logs.FirstOrDefaultAsync(l => l.Id == id);
|
||||||
|
if (log == null)
|
||||||
|
throw new KeyNotFoundException($"找不到ID为 {id} 的日志");
|
||||||
|
|
||||||
|
context.Logs.Remove(log);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BatchDeleteResult> BatchDeleteLogsAsync(List<int> ids)
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var result = new BatchDeleteResult();
|
||||||
|
|
||||||
|
var logs = await context.Logs.Where(l => ids.Contains(l.Id)).ToListAsync();
|
||||||
|
result.SuccessCount = logs.Count;
|
||||||
|
result.FailedCount = ids.Count - logs.Count;
|
||||||
|
|
||||||
|
if (logs.Any())
|
||||||
|
{
|
||||||
|
context.Logs.RemoveRange(logs);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> ClearLogsByDateAsync(DateTime beforeDate)
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var logsToDelete = await context.Logs
|
||||||
|
.Where(l => l.Timestamp < beforeDate)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (logsToDelete.Any())
|
||||||
|
{
|
||||||
|
context.Logs.RemoveRange(logsToDelete);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return logsToDelete.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> ClearAllLogsAsync()
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
var totalCount = await context.Logs.CountAsync();
|
||||||
|
|
||||||
|
if (totalCount > 0)
|
||||||
|
{
|
||||||
|
await context.Database.ExecuteSqlRawAsync("DELETE FROM \"Logs\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LogStatistics> GetLogStatisticsAsync()
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
// 使用UTC时间避免PostgreSQL时区问题
|
||||||
|
var todayUtc = DateTime.UtcNow.Date;
|
||||||
|
var tomorrowUtc = todayUtc.AddDays(1);
|
||||||
|
|
||||||
|
var totalCount = await context.Logs.CountAsync();
|
||||||
|
var todayCount = await context.Logs.CountAsync(l => l.Timestamp >= todayUtc && l.Timestamp < tomorrowUtc);
|
||||||
|
var errorCount = await context.Logs.CountAsync(l => l.Level == LogLevel.Error || l.Level == LogLevel.Critical);
|
||||||
|
var warningCount = await context.Logs.CountAsync(l => l.Level == LogLevel.Warning);
|
||||||
|
|
||||||
|
return new LogStatistics
|
||||||
|
{
|
||||||
|
TotalCount = totalCount,
|
||||||
|
TodayCount = todayCount,
|
||||||
|
ErrorCount = errorCount,
|
||||||
|
WarningCount = warningCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,14 @@ using Foxel.Models.Response.Picture;
|
|||||||
using Foxel.Services.Configuration;
|
using Foxel.Services.Configuration;
|
||||||
using Foxel.Services.Storage;
|
using Foxel.Services.Storage;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Foxel.Services.Management;
|
namespace Foxel.Services.Management;
|
||||||
|
|
||||||
public class PictureManagementService(
|
public class PictureManagementService(
|
||||||
IDbContextFactory<MyDbContext> contextFactory,
|
IDbContextFactory<MyDbContext> contextFactory,
|
||||||
IStorageService storageService) : IPictureManagementService
|
IStorageService storageService,
|
||||||
|
ILogger<PictureManagementService> logger) : IPictureManagementService
|
||||||
{
|
{
|
||||||
public async Task<PaginatedResult<PictureResponse>> GetPicturesAsync(int page = 1, int pageSize = 10, string? searchQuery = null, int? userId = null)
|
public async Task<PaginatedResult<PictureResponse>> GetPicturesAsync(int page = 1, int pageSize = 10, string? searchQuery = null, int? userId = null)
|
||||||
{
|
{
|
||||||
@@ -143,7 +145,7 @@ public class PictureManagementService(
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"删除图片文件时出错:{ex.Message}");
|
logger.LogWarning(ex, "删除图片文件时出错,图片ID: {PictureId}", id);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using Foxel.Services.Storage;
|
|||||||
using Foxel.Services.VectorDB;
|
using Foxel.Services.VectorDB;
|
||||||
using Foxel.Utils;
|
using Foxel.Utils;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Foxel.Services.Media;
|
namespace Foxel.Services.Media;
|
||||||
|
|
||||||
@@ -20,7 +21,8 @@ public class PictureService(
|
|||||||
IConfigService configuration,
|
IConfigService configuration,
|
||||||
IBackgroundTaskQueue backgroundTaskQueue,
|
IBackgroundTaskQueue backgroundTaskQueue,
|
||||||
IVectorDbService vectorDbService,
|
IVectorDbService vectorDbService,
|
||||||
IStorageService storageService)
|
IStorageService storageService,
|
||||||
|
ILogger<PictureService> logger)
|
||||||
: IPictureService
|
: IPictureService
|
||||||
{
|
{
|
||||||
public async Task<PaginatedResult<PictureResponse>> GetPicturesAsync(
|
public async Task<PaginatedResult<PictureResponse>> GetPicturesAsync(
|
||||||
@@ -60,7 +62,7 @@ public class PictureService(
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// 如果向量搜索失败,记录错误并回退到标准搜索
|
// 如果向量搜索失败,记录错误并回退到标准搜索
|
||||||
Console.WriteLine($"向量搜索失败,回退到标准搜索: {ex.Message}");
|
logger.LogWarning("向量搜索失败,回退到标准搜索: {Message}", ex.Message);
|
||||||
|
|
||||||
// 如果是明确的配置错误,则向上抛出异常
|
// 如果是明确的配置错误,则向上抛出异常
|
||||||
if (ex.Message.Contains("请检查嵌入模型配置"))
|
if (ex.Message.Contains("请检查嵌入模型配置"))
|
||||||
@@ -70,7 +72,6 @@ public class PictureService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行标准搜索(作为默认方法或向量搜索的回退选项)
|
|
||||||
return await PerformStandardSearchAsync(
|
return await PerformStandardSearchAsync(
|
||||||
dbContext, page, pageSize, searchQuery, tags,
|
dbContext, page, pageSize, searchQuery, tags,
|
||||||
startDate, endDate, userId, sortBy, onlyWithGps,
|
startDate, endDate, userId, sortBy, onlyWithGps,
|
||||||
@@ -573,7 +574,7 @@ public class PictureService(
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"生成缩略图失败: {ex.Message}");
|
logger.LogError(ex, "生成缩略图失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,7 +770,7 @@ public class PictureService(
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
errorMsg = $"数据库记录已删除,但删除文件失败: {ex.Message}";
|
errorMsg = $"数据库记录已删除,但删除文件失败: {ex.Message}";
|
||||||
Console.WriteLine($"删除图片文件时出错:{ex.Message}");
|
logger.LogError(ex, "删除图片文件时出错");
|
||||||
}
|
}
|
||||||
|
|
||||||
results[pictureId] = (true, errorMsg, userId);
|
results[pictureId] = (true, errorMsg, userId);
|
||||||
@@ -827,13 +828,13 @@ public class PictureService(
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 记录获取到空向量的警告
|
// 记录获取到空向量的警告
|
||||||
Console.WriteLine($"警告: 图片 {pictureId} 的嵌入向量为空,跳过向量更新");
|
logger.LogWarning("图片 {PictureId} 的嵌入向量为空,跳过向量更新", pictureId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// 记录错误但不抛出异常,允许其他字段的更新继续进行
|
// 记录错误但不抛出异常,允许其他字段的更新继续进行
|
||||||
Console.WriteLine($"更新图片 {pictureId} 的嵌入向量时出错: {ex.Message}");
|
logger.LogError(ex, "更新图片 {PictureId} 的嵌入向量时出错", pictureId);
|
||||||
// 不设置 picture.Embedding,保持原值不变
|
// 不设置 picture.Embedding,保持原值不变
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ using Foxel.Models;
|
|||||||
using Foxel.Models.DataBase;
|
using Foxel.Models.DataBase;
|
||||||
using Foxel.Models.Response.Tag;
|
using Foxel.Models.Response.Tag;
|
||||||
using Foxel.Services.Media;
|
using Foxel.Services.Media;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Foxel.Services;
|
namespace Foxel.Services;
|
||||||
|
|
||||||
public class TagService(IDbContextFactory<MyDbContext> contextFactory) : ITagService
|
public class TagService(IDbContextFactory<MyDbContext> contextFactory, ILogger<TagService> logger) : ITagService
|
||||||
{
|
{
|
||||||
public async Task<PaginatedResult<TagResponse>> GetFilteredTagsAsync(
|
public async Task<PaginatedResult<TagResponse>> GetFilteredTagsAsync(
|
||||||
int page = 1,
|
int page = 1,
|
||||||
@@ -91,8 +92,7 @@ public class TagService(IDbContextFactory<MyDbContext> contextFactory) : ITagSer
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// 记录详细错误信息
|
// 记录详细错误信息
|
||||||
Console.WriteLine($"GetFilteredTagsAsync error: {ex.Message}");
|
logger.LogError(ex, "GetFilteredTagsAsync error");
|
||||||
Console.WriteLine($"Stack trace: {ex.StackTrace}");
|
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,20 @@ using COSXML.Model.Tag;
|
|||||||
using COSXML.Transfer;
|
using COSXML.Transfer;
|
||||||
using Foxel.Services.Attributes;
|
using Foxel.Services.Attributes;
|
||||||
using Foxel.Services.Configuration;
|
using Foxel.Services.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Foxel.Services.Storage.Providers;
|
namespace Foxel.Services.Storage.Providers;
|
||||||
|
|
||||||
public class CustomQCloudCredentialProvider : DefaultSessionQCloudCredentialProvider
|
public class CustomQCloudCredentialProvider : DefaultSessionQCloudCredentialProvider
|
||||||
{
|
{
|
||||||
private readonly IConfigService _configService;
|
private readonly IConfigService _configService;
|
||||||
|
private readonly ILogger<CustomQCloudCredentialProvider> _logger;
|
||||||
|
|
||||||
public CustomQCloudCredentialProvider(IConfigService configService)
|
public CustomQCloudCredentialProvider(IConfigService configService, ILogger<CustomQCloudCredentialProvider> logger)
|
||||||
: base(null, null, 0L, null)
|
: base(null, null, 0L, null)
|
||||||
{
|
{
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
|
_logger = logger;
|
||||||
Refresh();
|
Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,14 +37,14 @@ public class CustomQCloudCredentialProvider : DefaultSessionQCloudCredentialProv
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"刷新临时密钥时出错: {ex.Message}");
|
_logger.LogError(ex, "刷新临时密钥时出错");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[StorageProvider(StorageType.Cos)]
|
[StorageProvider(StorageType.Cos)]
|
||||||
public class CosStorageProvider(IConfigService configService) : IStorageProvider
|
public class CosStorageProvider(IConfigService configService, ILogger<CosStorageProvider> logger) : IStorageProvider
|
||||||
{
|
{
|
||||||
private CosXml CreateClient()
|
private CosXml CreateClient()
|
||||||
{
|
{
|
||||||
@@ -51,7 +54,10 @@ public class CosStorageProvider(IConfigService configService) : IStorageProvider
|
|||||||
.SetDebugLog(true)
|
.SetDebugLog(true)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var cosCredentialProvider = new CustomQCloudCredentialProvider(configService);
|
var cosCredentialProvider = new CustomQCloudCredentialProvider(configService,
|
||||||
|
logger.IsEnabled(LogLevel.Debug) ?
|
||||||
|
Microsoft.Extensions.Logging.LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger<CustomQCloudCredentialProvider>() :
|
||||||
|
Microsoft.Extensions.Logging.Abstractions.NullLogger<CustomQCloudCredentialProvider>.Instance);
|
||||||
|
|
||||||
return new CosXmlServer(config, cosCredentialProvider);
|
return new CosXmlServer(config, cosCredentialProvider);
|
||||||
}
|
}
|
||||||
@@ -93,17 +99,17 @@ public class CosStorageProvider(IConfigService configService) : IStorageProvider
|
|||||||
}
|
}
|
||||||
catch (CosClientException clientEx)
|
catch (CosClientException clientEx)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"COS客户端异常: {clientEx}");
|
logger.LogError(clientEx, "COS客户端异常");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (CosServerException serverEx)
|
catch (CosServerException serverEx)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"COS服务器异常: {serverEx.GetInfo()}");
|
logger.LogError(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo());
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"上传文件到腾讯云COS时出错: {ex.Message}");
|
logger.LogError(ex, "上传文件到腾讯云COS时出错");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,15 +127,15 @@ public class CosStorageProvider(IConfigService configService) : IStorageProvider
|
|||||||
}
|
}
|
||||||
catch (CosClientException clientEx)
|
catch (CosClientException clientEx)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"COS客户端异常: {clientEx}");
|
logger.LogWarning(clientEx, "COS客户端异常");
|
||||||
}
|
}
|
||||||
catch (CosServerException serverEx)
|
catch (CosServerException serverEx)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"COS服务器异常: {serverEx.GetInfo()}");
|
logger.LogWarning(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo());
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"从腾讯云COS删除文件时出错: {ex.Message}");
|
logger.LogWarning(ex, "从腾讯云COS删除文件时出错");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +177,7 @@ public class CosStorageProvider(IConfigService configService) : IStorageProvider
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"生成腾讯云COS文件URL时出错: {ex.Message}");
|
logger.LogError(ex, "生成腾讯云COS文件URL时出错");
|
||||||
return "/images/unavailable.gif";
|
return "/images/unavailable.gif";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,17 +210,17 @@ public class CosStorageProvider(IConfigService configService) : IStorageProvider
|
|||||||
}
|
}
|
||||||
catch (CosClientException clientEx)
|
catch (CosClientException clientEx)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"COS客户端异常: {clientEx}");
|
logger.LogError(clientEx, "COS客户端异常");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (CosServerException serverEx)
|
catch (CosServerException serverEx)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"COS服务器异常: {serverEx.GetInfo()}");
|
logger.LogError(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo());
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"从腾讯云COS下载文件时出错: {ex.Message}");
|
logger.LogError(ex, "从腾讯云COS下载文件时出错");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
using Foxel.Services.Attributes;
|
using Foxel.Services.Attributes;
|
||||||
using Foxel.Services.Configuration;
|
using Foxel.Services.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Foxel.Services.Storage.Providers;
|
namespace Foxel.Services.Storage.Providers;
|
||||||
|
|
||||||
[StorageProvider(StorageType.Local)]
|
[StorageProvider(StorageType.Local)]
|
||||||
public class LocalStorageProvider(IConfigService configService) : IStorageProvider
|
public class LocalStorageProvider(IConfigService configService, ILogger<LocalStorageProvider> logger) : IStorageProvider
|
||||||
{
|
{
|
||||||
private readonly string _baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Uploads");
|
private readonly string _baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Uploads");
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ using Amazon.S3.Model;
|
|||||||
using Amazon.S3.Transfer;
|
using Amazon.S3.Transfer;
|
||||||
using Foxel.Services.Attributes;
|
using Foxel.Services.Attributes;
|
||||||
using Foxel.Services.Configuration;
|
using Foxel.Services.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Foxel.Services.Storage.Providers;
|
namespace Foxel.Services.Storage.Providers;
|
||||||
|
|
||||||
[StorageProvider(StorageType.S3)]
|
[StorageProvider(StorageType.S3)]
|
||||||
public class S3StorageProvider(IConfigService configService) : IStorageProvider
|
public class S3StorageProvider(IConfigService configService, ILogger<S3StorageProvider> logger) : IStorageProvider
|
||||||
{
|
{
|
||||||
private AmazonS3Client CreateClient()
|
private AmazonS3Client CreateClient()
|
||||||
{
|
{
|
||||||
@@ -63,7 +64,7 @@ public class S3StorageProvider(IConfigService configService) : IStorageProvider
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"上传文件到S3时出错: {ex.Message}");
|
logger.LogError(ex, "上传文件到S3时出错");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,7 +87,7 @@ public class S3StorageProvider(IConfigService configService) : IStorageProvider
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"从S3删除文件时出错: {ex.Message}");
|
logger.LogWarning(ex, "从S3删除文件时出错");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +119,7 @@ public class S3StorageProvider(IConfigService configService) : IStorageProvider
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"生成S3文件URL时出错: {ex.Message}");
|
logger.LogError(ex, "生成S3文件URL时出错");
|
||||||
return "/images/unavailable.gif";
|
return "/images/unavailable.gif";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,7 +160,7 @@ public class S3StorageProvider(IConfigService configService) : IStorageProvider
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"从S3下载文件时出错: {ex.Message}");
|
logger.LogError(ex, "从S3下载文件时出错");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ using System.Text.Json.Serialization;
|
|||||||
using Foxel.Services.Attributes;
|
using Foxel.Services.Attributes;
|
||||||
using Foxel.Services.Configuration;
|
using Foxel.Services.Configuration;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Foxel.Services.Storage.Providers;
|
namespace Foxel.Services.Storage.Providers;
|
||||||
|
|
||||||
[StorageProvider(StorageType.Telegram)]
|
[StorageProvider(StorageType.Telegram)]
|
||||||
public class TelegramStorageProvider(IConfigService configService) : IStorageProvider
|
public class TelegramStorageProvider(IConfigService configService, ILogger<TelegramStorageProvider> logger) : IStorageProvider
|
||||||
{
|
{
|
||||||
public async Task<string> SaveAsync(Stream fileStream, string fileName, string contentType)
|
public async Task<string> SaveAsync(Stream fileStream, string fileName, string contentType)
|
||||||
{
|
{
|
||||||
@@ -40,7 +41,7 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro
|
|||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var errorContent = await response.Content.ReadAsStringAsync();
|
var errorContent = await response.Content.ReadAsStringAsync();
|
||||||
Console.WriteLine($"Telegram API 请求失败: 状态码: {response.StatusCode}, 响应: {errorContent}");
|
logger.LogError("Telegram API 请求失败: 状态码: {StatusCode}, 响应: {Response}", response.StatusCode, errorContent);
|
||||||
throw new ApplicationException($"Telegram API 请求失败: {response.StatusCode}");
|
throw new ApplicationException($"Telegram API 请求失败: {response.StatusCode}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"发送文件到 Telegram 时出错: {ex.Message}");
|
logger.LogError(ex, "发送文件到 Telegram 时出错");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +115,7 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"删除 Telegram 文件时出错: {ex.Message}");
|
logger.LogWarning(ex, "删除 Telegram 文件时出错");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +134,7 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"生成 Telegram 文件 URL 时出错: {ex.Message}");
|
logger.LogError(ex, "生成 Telegram 文件 URL 时出错");
|
||||||
return $"/images/unavailable.gif";
|
return $"/images/unavailable.gif";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,7 +204,7 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"下载 Telegram 文件时出错: {ex.Message}");
|
logger.LogError(ex, "下载 Telegram 文件时出错");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ using System.Net.Http.Headers;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Foxel.Services.Attributes;
|
using Foxel.Services.Attributes;
|
||||||
using Foxel.Services.Configuration;
|
using Foxel.Services.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Foxel.Services.Storage.Providers;
|
namespace Foxel.Services.Storage.Providers;
|
||||||
|
|
||||||
[StorageProvider(StorageType.WebDAV)]
|
[StorageProvider(StorageType.WebDAV)]
|
||||||
public class WebDavStorageProvider(IConfigService configService) : IStorageProvider
|
public class WebDavStorageProvider(IConfigService configService, ILogger<WebDavStorageProvider> logger) : IStorageProvider
|
||||||
{
|
{
|
||||||
private HttpClient CreateClient()
|
private HttpClient CreateClient()
|
||||||
{
|
{
|
||||||
@@ -55,7 +56,7 @@ public class WebDavStorageProvider(IConfigService configService) : IStorageProvi
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"上传文件到WebDAV时出错: {ex.Message}");
|
logger.LogError(ex, "上传文件到WebDAV时出错");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +81,7 @@ public class WebDavStorageProvider(IConfigService configService) : IStorageProvi
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"从WebDAV删除文件时出错: {ex.Message}");
|
logger.LogWarning(ex, "从WebDAV删除文件时出错");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ public class WebDavStorageProvider(IConfigService configService) : IStorageProvi
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"生成WebDAV文件URL时出错: {ex.Message}");
|
logger.LogError(ex, "生成WebDAV文件URL时出错");
|
||||||
return "/images/unavailable.gif";
|
return "/images/unavailable.gif";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,7 +145,7 @@ public class WebDavStorageProvider(IConfigService configService) : IStorageProvi
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"从WebDAV下载文件时出错: {ex.Message}");
|
logger.LogError(ex, "从WebDAV下载文件时出错");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,7 +211,7 @@ public class WebDavStorageProvider(IConfigService configService) : IStorageProvi
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"确保WebDAV目录存在时出错: {ex.Message}");
|
logger.LogError(ex, "确保WebDAV目录存在时出错");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Foxel.Services.Attributes;
|
using Foxel.Services.Attributes;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Foxel.Services.Storage;
|
namespace Foxel.Services.Storage;
|
||||||
|
|
||||||
@@ -9,11 +10,13 @@ namespace Foxel.Services.Storage;
|
|||||||
public class StorageService : IStorageService
|
public class StorageService : IStorageService
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger<StorageService> _logger;
|
||||||
private readonly Dictionary<StorageType, Type> _storageProviders = new();
|
private readonly Dictionary<StorageType, Type> _storageProviders = new();
|
||||||
|
|
||||||
public StorageService(IServiceProvider serviceProvider)
|
public StorageService(IServiceProvider serviceProvider, ILogger<StorageService> logger)
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
RegisterStorageProviders();
|
RegisterStorageProviders();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +50,7 @@ public class StorageService : IStorageService
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"扫描程序集 {assembly.FullName} 出错: {ex.Message}");
|
_logger.LogWarning(ex, "扫描程序集 {AssemblyName} 时发生错误", assembly.FullName);
|
||||||
// 继续扫描其他程序集
|
// 继续扫描其他程序集
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Foxel.Utils;
|
namespace Foxel.Utils;
|
||||||
|
|
||||||
public static class AiHelper
|
public static class AiHelper
|
||||||
@@ -6,8 +8,9 @@ public static class AiHelper
|
|||||||
/// 从AI响应中提取标题和描述
|
/// 从AI响应中提取标题和描述
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="aiResponse">AI生成的响应文本</param>
|
/// <param name="aiResponse">AI生成的响应文本</param>
|
||||||
|
/// <param name="logger">日志记录器</param>
|
||||||
/// <returns>包含标题和描述的元组</returns>
|
/// <returns>包含标题和描述的元组</returns>
|
||||||
public static (string title, string description) ExtractTitleAndDescription(string aiResponse)
|
public static (string title, string description) ExtractTitleAndDescription(string aiResponse, ILogger? logger = null)
|
||||||
{
|
{
|
||||||
string title = "AI生成的标题";
|
string title = "AI生成的标题";
|
||||||
string description = "AI生成的描述";
|
string description = "AI生成的描述";
|
||||||
@@ -83,7 +86,7 @@ public static class AiHelper
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"解析AI响应时出错: {ex.Message}");
|
logger?.LogWarning(ex, "解析AI响应时出错");
|
||||||
description = $"原始AI响应: {aiResponse}";
|
description = $"原始AI响应: {aiResponse}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,3 +88,13 @@ export {
|
|||||||
rebuildVectors
|
rebuildVectors
|
||||||
} from './vectorDbApi';
|
} from './vectorDbApi';
|
||||||
|
|
||||||
|
// 导出LogManagement API
|
||||||
|
export {
|
||||||
|
getLogs,
|
||||||
|
getLogById,
|
||||||
|
deleteLog,
|
||||||
|
batchDeleteLogs,
|
||||||
|
clearLogs,
|
||||||
|
getLogStatistics
|
||||||
|
} from './logManagementApi';
|
||||||
|
|
||||||
|
|||||||
83
Web/src/api/logManagementApi.ts
Normal file
83
Web/src/api/logManagementApi.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { fetchApi } from './fetchClient';
|
||||||
|
import {
|
||||||
|
type BaseResult,
|
||||||
|
type PaginatedResult,
|
||||||
|
type LogResponse,
|
||||||
|
type LogFilterRequest,
|
||||||
|
type ClearLogsRequest,
|
||||||
|
type BatchDeleteResult,
|
||||||
|
type LogStatistics
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
// 获取日志列表
|
||||||
|
export const getLogs = async (
|
||||||
|
filters: LogFilterRequest = {}
|
||||||
|
): Promise<PaginatedResult<LogResponse>> => {
|
||||||
|
const { page = 1, pageSize = 10, searchQuery, level, startDate, endDate } = filters;
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
page: page.toString(),
|
||||||
|
pageSize: pageSize.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchQuery) params.append('searchQuery', searchQuery);
|
||||||
|
if (level) params.append('level', level.toString());
|
||||||
|
if (startDate) params.append('startDate', startDate);
|
||||||
|
if (endDate) params.append('endDate', endDate);
|
||||||
|
|
||||||
|
const response = await fetchApi(`/management/log/get_logs?${params.toString()}`);
|
||||||
|
return response as PaginatedResult<LogResponse>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据ID获取单个日志
|
||||||
|
export const getLogById = async (id: number): Promise<BaseResult<LogResponse>> => {
|
||||||
|
return fetchApi<LogResponse>(
|
||||||
|
`/management/log/get_log/${id}`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除日志
|
||||||
|
export const deleteLog = async (id: number): Promise<BaseResult<boolean>> => {
|
||||||
|
return fetchApi<boolean>(
|
||||||
|
'/management/log/delete_log',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(id)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量删除日志
|
||||||
|
export const batchDeleteLogs = async (
|
||||||
|
ids: number[]
|
||||||
|
): Promise<BaseResult<BatchDeleteResult>> => {
|
||||||
|
return fetchApi<BatchDeleteResult>(
|
||||||
|
'/management/log/batch_delete_logs',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(ids)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清空日志
|
||||||
|
export const clearLogs = async (
|
||||||
|
request: ClearLogsRequest
|
||||||
|
): Promise<BaseResult<number>> => {
|
||||||
|
return fetchApi<number>(
|
||||||
|
'/management/log/clear_logs',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取日志统计信息
|
||||||
|
export const getLogStatistics = async (): Promise<BaseResult<LogStatistics>> => {
|
||||||
|
return fetchApi<LogStatistics>(
|
||||||
|
'/management/log/get_statistics',
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -315,3 +315,56 @@ export interface UserDetailResponse {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
statistics: UserStatistics;
|
statistics: UserStatistics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 日志级别枚举
|
||||||
|
export type LogLevel = 'Trace' | 'Debug' | 'Information' | 'Warning' | 'Error' | 'Critical';
|
||||||
|
|
||||||
|
export const LogLevel = {
|
||||||
|
Trace: 'Trace' as LogLevel,
|
||||||
|
Debug: 'Debug' as LogLevel,
|
||||||
|
Information: 'Information' as LogLevel,
|
||||||
|
Warning: 'Warning' as LogLevel,
|
||||||
|
Error: 'Error' as LogLevel,
|
||||||
|
Critical: 'Critical' as LogLevel
|
||||||
|
};
|
||||||
|
|
||||||
|
// 日志响应数据
|
||||||
|
export interface LogResponse {
|
||||||
|
id: number;
|
||||||
|
level: LogLevel | number; // 支持数字和字符串两种形式
|
||||||
|
message: string;
|
||||||
|
category: string;
|
||||||
|
eventId?: number;
|
||||||
|
timestamp: Date;
|
||||||
|
exception?: string;
|
||||||
|
requestPath?: string;
|
||||||
|
requestMethod?: string;
|
||||||
|
statusCode?: number;
|
||||||
|
ipAddress?: string;
|
||||||
|
userId?: string;
|
||||||
|
properties?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志筛选请求参数
|
||||||
|
export interface LogFilterRequest {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
searchQuery?: string;
|
||||||
|
level?: LogLevel | number; // 支持数字和字符串两种形式
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空日志请求
|
||||||
|
export interface ClearLogsRequest {
|
||||||
|
clearAll?: boolean;
|
||||||
|
beforeDate?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志统计信息
|
||||||
|
export interface LogStatistics {
|
||||||
|
totalCount: number;
|
||||||
|
todayCount: number;
|
||||||
|
errorCount: number;
|
||||||
|
warningCount: number;
|
||||||
|
}
|
||||||
|
|||||||
734
Web/src/pages/admin/log/Index.tsx
Normal file
734
Web/src/pages/admin/log/Index.tsx
Normal file
@@ -0,0 +1,734 @@
|
|||||||
|
import React, {useState, useEffect, useMemo} from 'react';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
DatePicker,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Tooltip,
|
||||||
|
Badge,
|
||||||
|
Popconfirm,
|
||||||
|
Drawer,
|
||||||
|
Alert,
|
||||||
|
Statistic
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
ClearOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
WarningOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
FilterOutlined,
|
||||||
|
FileTextOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import type {ColumnsType} from 'antd/es/table';
|
||||||
|
import type {TableRowSelection} from 'antd/es/table/interface';
|
||||||
|
import {useOutletContext} from 'react-router';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import type {Dayjs} from 'dayjs';
|
||||||
|
import {getLogs, deleteLog, batchDeleteLogs, clearLogs, getLogById, getLogStatistics} from '../../../api';
|
||||||
|
import type {LogResponse, LogLevel, LogFilterRequest, LogStatistics} from '../../../api/types';
|
||||||
|
|
||||||
|
const {Title, Text, Paragraph} = Typography;
|
||||||
|
const {RangePicker} = DatePicker;
|
||||||
|
const {confirm} = Modal;
|
||||||
|
|
||||||
|
// 日志级别数字到字符串的映射
|
||||||
|
const LOG_LEVEL_MAP: Record<number, LogLevel> = {
|
||||||
|
0: 'Trace',
|
||||||
|
1: 'Debug',
|
||||||
|
2: 'Information',
|
||||||
|
3: 'Warning',
|
||||||
|
4: 'Error',
|
||||||
|
5: 'Critical'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 日志级别颜色映射
|
||||||
|
const LOG_LEVEL_COLORS: Record<LogLevel, string> = {
|
||||||
|
Trace: 'default',
|
||||||
|
Debug: 'blue',
|
||||||
|
Information: 'green',
|
||||||
|
Warning: 'orange',
|
||||||
|
Error: 'red',
|
||||||
|
Critical: 'magenta'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 日志级别图标映射
|
||||||
|
const LOG_LEVEL_ICONS: Record<LogLevel, React.ReactNode> = {
|
||||||
|
Trace: <FileTextOutlined/>,
|
||||||
|
Debug: <InfoCircleOutlined/>,
|
||||||
|
Information: <InfoCircleOutlined/>,
|
||||||
|
Warning: <WarningOutlined/>,
|
||||||
|
Error: <ExclamationCircleOutlined/>,
|
||||||
|
Critical: <ExclamationCircleOutlined/>
|
||||||
|
};
|
||||||
|
|
||||||
|
const AdminLogManagement: React.FC = () => {
|
||||||
|
const {isMobile} = useOutletContext<{ isMobile: boolean; isAdminPanel?: boolean }>();
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [logs, setLogs] = useState<LogResponse[]>([]);
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total: number, range: [number, number]) =>
|
||||||
|
`第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 筛选条件
|
||||||
|
const [filters, setFilters] = useState<LogFilterRequest>({});
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [selectedLevel, setSelectedLevel] = useState<LogLevel | undefined>();
|
||||||
|
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs] | null>(null);
|
||||||
|
|
||||||
|
// 日志详情抽屉
|
||||||
|
const [detailDrawerOpen, setDetailDrawerOpen] = useState(false);
|
||||||
|
const [selectedLog, setSelectedLog] = useState<LogResponse | null>(null);
|
||||||
|
const [detailLoading, setDetailLoading] = useState(false);
|
||||||
|
|
||||||
|
// 统计信息
|
||||||
|
const [logStats, setLogStats] = useState<LogStatistics>({
|
||||||
|
totalCount: 0,
|
||||||
|
todayCount: 0,
|
||||||
|
errorCount: 0,
|
||||||
|
warningCount: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取日志统计
|
||||||
|
const fetchLogStatistics = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getLogStatistics();
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setLogStats(response.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching log statistics:', error);
|
||||||
|
message.error('获取日志统计失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取日志列表
|
||||||
|
const fetchLogs = async (params: LogFilterRequest = {}) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await getLogs({
|
||||||
|
page: params.page || pagination.current,
|
||||||
|
pageSize: params.pageSize || pagination.pageSize,
|
||||||
|
searchQuery: params.searchQuery || searchText,
|
||||||
|
level: params.level || selectedLevel,
|
||||||
|
startDate: params.startDate,
|
||||||
|
endDate: params.endDate,
|
||||||
|
...params
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setLogs(response.data);
|
||||||
|
setPagination(prev => ({
|
||||||
|
...prev,
|
||||||
|
current: response.page,
|
||||||
|
total: response.totalCount
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching logs:', error);
|
||||||
|
message.error('获取日志列表失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看日志详情
|
||||||
|
const handleViewDetail = async (logId: number) => {
|
||||||
|
setDetailLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await getLogById(logId);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setSelectedLog(response.data);
|
||||||
|
setDetailDrawerOpen(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching log detail:', error);
|
||||||
|
message.error('获取日志详情失败');
|
||||||
|
} finally {
|
||||||
|
setDetailLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除单个日志
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const response = await deleteLog(id);
|
||||||
|
if (response.success) {
|
||||||
|
message.success('日志删除成功');
|
||||||
|
await Promise.all([fetchLogs(), fetchLogStatistics()]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting log:', error);
|
||||||
|
message.error('删除日志失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量删除日志
|
||||||
|
const handleBatchDelete = async () => {
|
||||||
|
if (selectedRowKeys.length === 0) {
|
||||||
|
message.warning('请选择要删除的日志');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除选中的 ${selectedRowKeys.length} 条日志吗?`,
|
||||||
|
icon: <ExclamationCircleOutlined/>,
|
||||||
|
okText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
okType: 'danger',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const response = await batchDeleteLogs(selectedRowKeys as number[]);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
message.success(`成功删除 ${response.data.successCount} 条日志`);
|
||||||
|
setSelectedRowKeys([]);
|
||||||
|
await Promise.all([fetchLogs(), fetchLogStatistics()]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error batch deleting logs:', error);
|
||||||
|
message.error('批量删除日志失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清空日志
|
||||||
|
const handleClearLogs = (type: 'all' | 'old') => {
|
||||||
|
const title = type === 'all' ? '清空所有日志' : '清空历史日志';
|
||||||
|
const content = type === 'all'
|
||||||
|
? '确定要清空所有日志吗?此操作不可恢复!'
|
||||||
|
: '确定要清空7天前的历史日志吗?此操作不可恢复!';
|
||||||
|
|
||||||
|
confirm({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
icon: <ExclamationCircleOutlined/>,
|
||||||
|
okText: '确定',
|
||||||
|
cancelText: '取消',
|
||||||
|
okType: 'danger',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const request = type === 'all'
|
||||||
|
? {clearAll: true}
|
||||||
|
: {beforeDate: dayjs().subtract(7, 'day').toDate()};
|
||||||
|
|
||||||
|
const response = await clearLogs(request);
|
||||||
|
if (response.success) {
|
||||||
|
message.success(`成功清空 ${response.data} 条日志`);
|
||||||
|
await Promise.all([fetchLogs(), fetchLogStatistics()]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing logs:', error);
|
||||||
|
message.error('清空日志失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索和筛选
|
||||||
|
const handleSearch = () => {
|
||||||
|
const newFilters: LogFilterRequest = {
|
||||||
|
page: 1,
|
||||||
|
searchQuery: searchText,
|
||||||
|
level: selectedLevel,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dateRange) {
|
||||||
|
newFilters.startDate = dateRange[0].format('YYYY-MM-DD');
|
||||||
|
newFilters.endDate = dateRange[1].format('YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilters(newFilters);
|
||||||
|
fetchLogs(newFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置筛选
|
||||||
|
const handleResetFilters = () => {
|
||||||
|
setSearchText('');
|
||||||
|
setSelectedLevel(undefined);
|
||||||
|
setDateRange(null);
|
||||||
|
setFilters({});
|
||||||
|
setPagination(prev => ({...prev, current: 1}));
|
||||||
|
fetchLogs({page: 1});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取日志级别字符串
|
||||||
|
const getLogLevelString = (level: number | string): LogLevel => {
|
||||||
|
if (typeof level === 'number') {
|
||||||
|
return LOG_LEVEL_MAP[level] || 'Information';
|
||||||
|
}
|
||||||
|
return level as LogLevel;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns = useMemo<ColumnsType<LogResponse>>(() => [
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
dataIndex: 'timestamp',
|
||||||
|
key: 'timestamp',
|
||||||
|
width: 160,
|
||||||
|
render: (timestamp: Date) => (
|
||||||
|
<Text type="secondary">
|
||||||
|
{dayjs(timestamp).format('MM-DD HH:mm:ss')}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '级别',
|
||||||
|
dataIndex: 'level',
|
||||||
|
key: 'level',
|
||||||
|
width: 120,
|
||||||
|
render: (level: LogLevel | number) => {
|
||||||
|
const levelString = getLogLevelString(level);
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
color={LOG_LEVEL_COLORS[levelString]}
|
||||||
|
icon={LOG_LEVEL_ICONS[levelString]}
|
||||||
|
>
|
||||||
|
{levelString}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
filters: [
|
||||||
|
{text: 'Trace', value: 0},
|
||||||
|
{text: 'Debug', value: 1},
|
||||||
|
{text: 'Information', value: 2},
|
||||||
|
{text: 'Warning', value: 3},
|
||||||
|
{text: 'Error', value: 4},
|
||||||
|
{text: 'Critical', value: 5},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '分类',
|
||||||
|
dataIndex: 'category',
|
||||||
|
key: 'category',
|
||||||
|
width: 150,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (category: string) => (
|
||||||
|
<Tooltip title={category}>
|
||||||
|
<Text>{category}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '消息',
|
||||||
|
dataIndex: 'message',
|
||||||
|
key: 'message',
|
||||||
|
ellipsis: true,
|
||||||
|
render: (message: string) => (
|
||||||
|
<Tooltip title={message}>
|
||||||
|
<Text>{message}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '请求信息',
|
||||||
|
key: 'request',
|
||||||
|
width: 120,
|
||||||
|
responsive: ['lg'],
|
||||||
|
render: (_, record) => {
|
||||||
|
if (record.requestPath) {
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" size={0}>
|
||||||
|
<Text type="secondary" style={{fontSize: '12px'}}>
|
||||||
|
{record.requestMethod} {record.statusCode}
|
||||||
|
</Text>
|
||||||
|
<Text type="secondary" style={{fontSize: '12px'}}>
|
||||||
|
{record.requestPath}
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'IP地址',
|
||||||
|
dataIndex: 'ipAddress',
|
||||||
|
key: 'ipAddress',
|
||||||
|
width: 120,
|
||||||
|
responsive: ['xl'],
|
||||||
|
render: (ip: string) => ip || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 120,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space>
|
||||||
|
<Tooltip title="查看详情">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<EyeOutlined/>}
|
||||||
|
onClick={() => handleViewDetail(record.id)}
|
||||||
|
loading={detailLoading}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定删除此日志吗?"
|
||||||
|
onConfirm={() => handleDelete(record.id)}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined/>}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
], [detailLoading]);
|
||||||
|
|
||||||
|
// 行选择配置
|
||||||
|
const rowSelection: TableRowSelection<LogResponse> = {
|
||||||
|
selectedRowKeys,
|
||||||
|
onChange: setSelectedRowKeys,
|
||||||
|
preserveSelectedRowKeys: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化加载
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all([fetchLogs(), fetchLogStatistics()]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="admin-log-management">
|
||||||
|
<Title level={2}>日志管理</Title>
|
||||||
|
<Text type="secondary" style={{marginBottom: 24, display: 'block'}}>
|
||||||
|
查看和管理系统运行日志,监控系统状态和问题排查。
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<Row gutter={[16, 16]} style={{marginBottom: 24}}>
|
||||||
|
<Col xs={12} sm={6}>
|
||||||
|
<Card size="small">
|
||||||
|
<Statistic
|
||||||
|
title="总日志数"
|
||||||
|
value={logStats.totalCount}
|
||||||
|
prefix={<FileTextOutlined/>}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} sm={6}>
|
||||||
|
<Card size="small">
|
||||||
|
<Statistic
|
||||||
|
title="今日日志"
|
||||||
|
value={logStats.todayCount}
|
||||||
|
prefix={<InfoCircleOutlined/>}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} sm={6}>
|
||||||
|
<Card size="small">
|
||||||
|
<Statistic
|
||||||
|
title="错误日志"
|
||||||
|
value={logStats.errorCount}
|
||||||
|
prefix={<ExclamationCircleOutlined/>}
|
||||||
|
valueStyle={{color: '#cf1322'}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={12} sm={6}>
|
||||||
|
<Card size="small">
|
||||||
|
<Statistic
|
||||||
|
title="警告日志"
|
||||||
|
value={logStats.warningCount}
|
||||||
|
prefix={<WarningOutlined/>}
|
||||||
|
valueStyle={{color: '#fa8c16'}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
{/* 筛选条件 */}
|
||||||
|
<Row gutter={[16, 16]} style={{marginBottom: 16}}>
|
||||||
|
<Col xs={24} sm={12} md={8}>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索日志消息或分类"
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
onPressEnter={handleSearch}
|
||||||
|
prefix={<SearchOutlined/>}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={6}>
|
||||||
|
<Select
|
||||||
|
placeholder="选择日志级别"
|
||||||
|
value={selectedLevel}
|
||||||
|
onChange={setSelectedLevel}
|
||||||
|
allowClear
|
||||||
|
style={{width: '100%'}}
|
||||||
|
>
|
||||||
|
<Select.Option value="Trace">Trace</Select.Option>
|
||||||
|
<Select.Option value="Debug">Debug</Select.Option>
|
||||||
|
<Select.Option value="Information">Information</Select.Option>
|
||||||
|
<Select.Option value="Warning">Warning</Select.Option>
|
||||||
|
<Select.Option value="Error">Error</Select.Option>
|
||||||
|
<Select.Option value="Critical">Critical</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={10}>
|
||||||
|
<Space size="small" style={{width: '100%', justifyContent: 'space-between'}}>
|
||||||
|
<RangePicker
|
||||||
|
value={dateRange}
|
||||||
|
onChange={(dates) => setDateRange(dates as [Dayjs, Dayjs] | null)}
|
||||||
|
placeholder={['开始日期', '结束日期']}
|
||||||
|
style={{flex: 1}}
|
||||||
|
/>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<FilterOutlined/>}
|
||||||
|
onClick={handleSearch}
|
||||||
|
>
|
||||||
|
筛选
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined/>}
|
||||||
|
onClick={handleResetFilters}
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<Row justify="space-between" style={{marginBottom: 16}}>
|
||||||
|
<Col>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined/>}
|
||||||
|
onClick={handleBatchDelete}
|
||||||
|
disabled={selectedRowKeys.length === 0}
|
||||||
|
>
|
||||||
|
批量删除 ({selectedRowKeys.length})
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
icon={<ClearOutlined/>}
|
||||||
|
onClick={() => handleClearLogs('old')}
|
||||||
|
>
|
||||||
|
清空历史日志
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
icon={<ClearOutlined/>}
|
||||||
|
onClick={() => handleClearLogs('all')}
|
||||||
|
>
|
||||||
|
清空所有日志
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined/>}
|
||||||
|
onClick={() => fetchLogs()}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{/* 数据表格 */}
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={logs}
|
||||||
|
rowKey="id"
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
pagination={{
|
||||||
|
...pagination,
|
||||||
|
onChange: (page, pageSize) => {
|
||||||
|
setPagination(prev => ({...prev, current: page, pageSize: pageSize || 20}));
|
||||||
|
fetchLogs({...filters, page, pageSize});
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
loading={loading}
|
||||||
|
size={isMobile ? 'small' : 'middle'}
|
||||||
|
scroll={isMobile ? {x: 800} : undefined}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 日志详情抽屉 */}
|
||||||
|
<Drawer
|
||||||
|
title="日志详情"
|
||||||
|
placement="right"
|
||||||
|
width={isMobile ? '100%' : 600}
|
||||||
|
open={detailDrawerOpen}
|
||||||
|
onClose={() => setDetailDrawerOpen(false)}
|
||||||
|
>
|
||||||
|
{selectedLog && (
|
||||||
|
<Space direction="vertical" size="large" style={{width: '100%'}}>
|
||||||
|
<Card title="基本信息" size="small">
|
||||||
|
<Row gutter={[16, 8]}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Text strong>时间:</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={16}>
|
||||||
|
<Text>{dayjs(selectedLog.timestamp).format('YYYY-MM-DD HH:mm:ss')}</Text>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col span={8}>
|
||||||
|
<Text strong>级别:</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={16}>
|
||||||
|
<Tag color={LOG_LEVEL_COLORS[getLogLevelString(selectedLog.level)]}
|
||||||
|
icon={LOG_LEVEL_ICONS[getLogLevelString(selectedLog.level)]}>
|
||||||
|
{getLogLevelString(selectedLog.level)}
|
||||||
|
</Tag>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col span={8}>
|
||||||
|
<Text strong>分类:</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={16}>
|
||||||
|
<Text>{selectedLog.category}</Text>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{selectedLog.eventId != null && (
|
||||||
|
<>
|
||||||
|
<Col span={8}>
|
||||||
|
<Text strong>事件ID:</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={16}>
|
||||||
|
<Text>{selectedLog.eventId}</Text>
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="消息内容" size="small">
|
||||||
|
<Paragraph>
|
||||||
|
<Text>{selectedLog.message}</Text>
|
||||||
|
</Paragraph>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{selectedLog.exception && (
|
||||||
|
<Card title="异常信息" size="small">
|
||||||
|
<Alert
|
||||||
|
message="异常详情"
|
||||||
|
description={
|
||||||
|
<pre style={{whiteSpace: 'pre-wrap', fontSize: '12px'}}>
|
||||||
|
{selectedLog.exception}
|
||||||
|
</pre>
|
||||||
|
}
|
||||||
|
type="error"
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(selectedLog.requestPath || selectedLog.ipAddress) && (
|
||||||
|
<Card title="请求信息" size="small">
|
||||||
|
<Row gutter={[16, 8]}>
|
||||||
|
{selectedLog.requestMethod && (
|
||||||
|
<>
|
||||||
|
<Col span={8}>
|
||||||
|
<Text strong>请求方法:</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={16}>
|
||||||
|
<Tag>{selectedLog.requestMethod}</Tag>
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedLog.requestPath && (
|
||||||
|
<>
|
||||||
|
<Col span={8}>
|
||||||
|
<Text strong>请求路径:</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={16}>
|
||||||
|
<Text code>{selectedLog.requestPath}</Text>
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedLog.statusCode && (
|
||||||
|
<>
|
||||||
|
<Col span={8}>
|
||||||
|
<Text strong>状态码:</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={16}>
|
||||||
|
<Badge
|
||||||
|
status={selectedLog.statusCode >= 400 ? 'error' : 'success'}
|
||||||
|
text={selectedLog.statusCode}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedLog.ipAddress && (
|
||||||
|
<>
|
||||||
|
<Col span={8}>
|
||||||
|
<Text strong>IP地址:</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={16}>
|
||||||
|
<Text code>{selectedLog.ipAddress}</Text>
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedLog.userId && (
|
||||||
|
<>
|
||||||
|
<Col span={8}>
|
||||||
|
<Text strong>用户ID:</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={16}>
|
||||||
|
<Text>{selectedLog.userId}</Text>
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedLog.properties && (
|
||||||
|
<Card title="附加属性" size="small">
|
||||||
|
<pre style={{whiteSpace: 'pre-wrap', fontSize: '12px'}}>
|
||||||
|
{selectedLog.properties}
|
||||||
|
</pre>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminLogManagement;
|
||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
CompassOutlined,
|
CompassOutlined,
|
||||||
DashboardOutlined,
|
DashboardOutlined,
|
||||||
UserOutlined
|
UserOutlined,
|
||||||
|
LogoutOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
import AllImages from '../pages/allImages/Index';
|
import AllImages from '../pages/allImages/Index';
|
||||||
@@ -22,6 +23,7 @@ import System from '../pages/admin/system/Index';
|
|||||||
import UserManagement from '../pages/admin/users/Index';
|
import UserManagement from '../pages/admin/users/Index';
|
||||||
import PictureManagement from '../pages/admin/pictures/Index';
|
import PictureManagement from '../pages/admin/pictures/Index';
|
||||||
import UserDetail from '../pages/admin/users/UserDetail';
|
import UserDetail from '../pages/admin/users/UserDetail';
|
||||||
|
import AdminLogManagement from '../pages/admin/log/Index';
|
||||||
|
|
||||||
export interface RouteConfig {
|
export interface RouteConfig {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -168,6 +170,17 @@ const routes: RouteConfig[] = [
|
|||||||
breadcrumb: {
|
breadcrumb: {
|
||||||
title: '图片管理'
|
title: '图片管理'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'log',
|
||||||
|
key: 'admin-log',
|
||||||
|
icon: <LogoutOutlined />,
|
||||||
|
label: '日志中心',
|
||||||
|
element: <AdminLogManagement />,
|
||||||
|
area: 'admin',
|
||||||
|
breadcrumb: {
|
||||||
|
title: '日志中心'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'system',
|
path: 'system',
|
||||||
|
|||||||
Reference in New Issue
Block a user