From a95651b04ab460cdd488d02ff5ea419eb8aae1d4 Mon Sep 17 00:00:00 2001 From: shiyu Date: Fri, 6 Jun 2025 11:39:39 +0800 Subject: [PATCH] feat(logManagement): implement log management service --- Api/Management/LogManagementController.cs | 131 ++++ Extensions/LoggingExtensions.cs | 21 + Extensions/ServiceCollectionExtensions.cs | 1 + Models/DataBase/Log.cs | 49 ++ Models/Request/Log/ClearLogsRequest.cs | 7 + Models/Response/Log/LogResponse.cs | 18 + Models/Response/Log/LogStatistics.cs | 24 + MyDbContext.cs | 1 + Program.cs | 5 + Services/AI/AiService.cs | 44 +- Services/Auth/AuthService.cs | 15 +- Services/Background/BackgroundTaskQueue.cs | 30 +- Services/Configuration/ConfigService.cs | 44 +- Services/Initializer/DatabaseInitializer.cs | 9 + Services/Logging/DatabaseLogger.cs | 112 +++ .../Logging/DatabaseLoggerConfiguration.cs | 7 + Services/Logging/DatabaseLoggerProvider.cs | 23 + Services/Management/ILogManagementService.cs | 20 + Services/Management/LogManagementService.cs | 181 +++++ .../Management/PictureManagementService.cs | 6 +- Services/Media/PictureService.cs | 15 +- Services/Media/TagService.cs | 6 +- .../Storage/Providers/CosStorageProvider.cs | 34 +- .../Storage/Providers/LocalStorageProvider.cs | 3 +- .../Storage/Providers/S3StorageProvider.cs | 11 +- .../Providers/TelegramStorageProvider.cs | 13 +- .../Providers/WebDAVStorageProvider.cs | 13 +- Services/Storage/StorageService.cs | 7 +- Utils/AiHelper.cs | 7 +- Web/src/api/index.ts | 10 + Web/src/api/logManagementApi.ts | 83 ++ Web/src/api/types.ts | 53 ++ Web/src/pages/admin/log/Index.tsx | 734 ++++++++++++++++++ Web/src/routes/index.tsx | 15 +- 34 files changed, 1644 insertions(+), 108 deletions(-) create mode 100644 Api/Management/LogManagementController.cs create mode 100644 Extensions/LoggingExtensions.cs create mode 100644 Models/DataBase/Log.cs create mode 100644 Models/Request/Log/ClearLogsRequest.cs create mode 100644 Models/Response/Log/LogResponse.cs create mode 100644 Models/Response/Log/LogStatistics.cs create mode 100644 Services/Logging/DatabaseLogger.cs create mode 100644 Services/Logging/DatabaseLoggerConfiguration.cs create mode 100644 Services/Logging/DatabaseLoggerProvider.cs create mode 100644 Services/Management/ILogManagementService.cs create mode 100644 Services/Management/LogManagementService.cs create mode 100644 Web/src/api/logManagementApi.ts create mode 100644 Web/src/pages/admin/log/Index.tsx diff --git a/Api/Management/LogManagementController.cs b/Api/Management/LogManagementController.cs new file mode 100644 index 0000000..3a00d2e --- /dev/null +++ b/Api/Management/LogManagementController.cs @@ -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>> 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($"获取日志列表失败: {ex.Message}", 500); + } + } + + [HttpGet("get_log/{id}")] + public async Task>> GetLogById(int id) + { + try + { + var log = await logManagementService.GetLogByIdAsync(id); + return Success(log, "日志获取成功"); + } + catch (KeyNotFoundException) + { + return Error("找不到指定日志", 404); + } + catch (Exception ex) + { + return Error($"获取日志失败: {ex.Message}", 500); + } + } + + [HttpPost("delete_log")] + public async Task>> DeleteLog([FromBody] int id) + { + try + { + var result = await logManagementService.DeleteLogAsync(id); + return Success(result, "日志删除成功"); + } + catch (KeyNotFoundException) + { + return Error("找不到要删除的日志", 404); + } + catch (Exception ex) + { + return Error($"删除日志失败: {ex.Message}", 500); + } + } + + [HttpPost("batch_delete_logs")] + public async Task>> BatchDeleteLogs([FromBody] List ids) + { + try + { + if (ids.Count == 0) + { + return Error("未提供日志ID"); + } + + var result = await logManagementService.BatchDeleteLogsAsync(ids); + return Success(result, $"成功删除 {result.SuccessCount} 条日志,失败 {result.FailedCount} 条"); + } + catch (Exception ex) + { + return Error($"批量删除日志失败: {ex.Message}", 500); + } + } + + [HttpPost("clear_logs")] + public async Task>> 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("请指定清空条件:要么清空全部,要么指定日期"); + } + } + catch (Exception ex) + { + return Error($"清空日志失败: {ex.Message}", 500); + } + } + + [HttpGet("get_statistics")] + public async Task>> GetLogStatistics() + { + try + { + var statistics = await logManagementService.GetLogStatisticsAsync(); + return Success(statistics, "日志统计获取成功"); + } + catch (Exception ex) + { + return Error($"获取日志统计失败: {ex.Message}", 500); + } + } +} diff --git a/Extensions/LoggingExtensions.cs b/Extensions/LoggingExtensions.cs new file mode 100644 index 0000000..01e4b69 --- /dev/null +++ b/Extensions/LoggingExtensions.cs @@ -0,0 +1,21 @@ +using Foxel.Services.Logging; + +namespace Foxel.Extensions; + +public static class LoggingExtensions +{ + public static ILoggingBuilder AddDatabaseLogging(this ILoggingBuilder builder, Action? configure = null) + { + var config = new DatabaseLoggerConfiguration(); + configure?.Invoke(config); + + builder.Services.Configure(options => + { + options.MinLevel = config.MinLevel; + options.Enabled = config.Enabled; + }); + + builder.Services.AddSingleton(); + return builder; + } +} diff --git a/Extensions/ServiceCollectionExtensions.cs b/Extensions/ServiceCollectionExtensions.cs index 0bfdb50..cd0f5a4 100644 --- a/Extensions/ServiceCollectionExtensions.cs +++ b/Extensions/ServiceCollectionExtensions.cs @@ -29,6 +29,7 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddHostedService(); services.AddSingleton(); diff --git a/Models/DataBase/Log.cs b/Models/DataBase/Log.cs new file mode 100644 index 0000000..61c75dd --- /dev/null +++ b/Models/DataBase/Log.cs @@ -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格式存储额外属性 +} diff --git a/Models/Request/Log/ClearLogsRequest.cs b/Models/Request/Log/ClearLogsRequest.cs new file mode 100644 index 0000000..f1cc11a --- /dev/null +++ b/Models/Request/Log/ClearLogsRequest.cs @@ -0,0 +1,7 @@ +namespace Foxel.Models.Request.Log; + +public class ClearLogsRequest +{ + public DateTime? BeforeDate { get; set; } + public bool ClearAll { get; set; } = false; +} diff --git a/Models/Response/Log/LogResponse.cs b/Models/Response/Log/LogResponse.cs new file mode 100644 index 0000000..c4507b6 --- /dev/null +++ b/Models/Response/Log/LogResponse.cs @@ -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; } +} diff --git a/Models/Response/Log/LogStatistics.cs b/Models/Response/Log/LogStatistics.cs new file mode 100644 index 0000000..6aceeb0 --- /dev/null +++ b/Models/Response/Log/LogStatistics.cs @@ -0,0 +1,24 @@ +namespace Foxel.Models.Response.Log; + +public class LogStatistics +{ + /// + /// 总日志数 + /// + public int TotalCount { get; set; } + + /// + /// 今日日志数 + /// + public int TodayCount { get; set; } + + /// + /// 错误日志数(Error + Critical) + /// + public int ErrorCount { get; set; } + + /// + /// 警告日志数 + /// + public int WarningCount { get; set; } +} diff --git a/MyDbContext.cs b/MyDbContext.cs index 1c1f60d..d292d13 100644 --- a/MyDbContext.cs +++ b/MyDbContext.cs @@ -12,4 +12,5 @@ public class MyDbContext(DbContextOptions options) : DbContext(opti public DbSet Favorites { get; set; } = null!; public DbSet Albums { get; set; } = null!; public DbSet Roles { get; set; } = null!; + public DbSet Logs { get; set; } = null!; } \ No newline at end of file diff --git a/Program.cs b/Program.cs index 7c969fc..8208c1e 100644 --- a/Program.cs +++ b/Program.cs @@ -6,6 +6,11 @@ using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); var environment = builder.Environment; Console.WriteLine($"当前环境: {environment.EnvironmentName}"); +builder.Logging.AddDatabaseLogging(config => +{ + config.MinLevel = LogLevel.Information; + config.Enabled = true; +}); builder.Services.AddMemoryCache(); builder.Services.AddApplicationDbContext(builder.Configuration); builder.Services.AddApplicationOpenApi(); diff --git a/Services/AI/AiService.cs b/Services/AI/AiService.cs index 5f3fe88..fa0947e 100644 --- a/Services/AI/AiService.cs +++ b/Services/AI/AiService.cs @@ -5,29 +5,21 @@ using Foxel.Utils; namespace Foxel.Services.AI; -public class AiService : IAiService +public class AiService(IHttpClientFactory httpClientFactory, IConfigService configService, ILogger logger) + : IAiService { - private readonly IHttpClientFactory _httpClientFactory; - private readonly IConfigService _configService; private HttpClient? _httpClient; private string? _currentApiKey; private string? _currentBaseUrl; - public AiService(IHttpClientFactory httpClientFactory, IConfigService configService) - { - _httpClientFactory = httpClientFactory; - _configService = configService; - } - private HttpClient ConfigureHttpClient() { - string apiKey = _configService["AI:ApiKey"]; - string baseUrl = _configService["AI:ApiEndpoint"]; + string apiKey = configService["AI:ApiKey"]; + string baseUrl = configService["AI:ApiEndpoint"]; - // 检查是否需要重新创建 HttpClient if (_httpClient == null || _currentApiKey != apiKey || _currentBaseUrl != baseUrl) { - _httpClient = _httpClientFactory.CreateClient(); + _httpClient = httpClientFactory.CreateClient(); _httpClient.BaseAddress = new Uri(baseUrl); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); @@ -44,7 +36,7 @@ public class AiService : IAiService try { var client = ConfigureHttpClient(); - string model = _configService["AI:Model"]; + string model = configService["AI:Model"]; var imageUrl = new ImageUrl { Url = $"data:image/jpeg;base64,{base64Image}" @@ -59,7 +51,7 @@ public class AiService : IAiService var textContent = new TextContent { Type = "text", - Text = _configService["AI:ImageAnalysisPrompt"] ?? + Text = configService["AI:ImageAnalysisPrompt"] ?? "请详细分析这张图片,并提供全面的描述,以便用于向量嵌入和基于文本的图像搜索。描述需要包含:主体对象、场景环境、色彩特点、构图布局、风格特征、情绪氛围、细节特征等关键元素。请提供一个简短有力的标题,然后提供详细描述。\n\n请以JSON格式返回,格式如下:\n{\"title\": \"简短概括图片的核心内容\", \"description\": \"全面详细的描述,包含上述所有元素,使用丰富精确的词汇,避免笼统表达\"}\n\n请确保返回有效的JSON格式。" }; @@ -94,7 +86,7 @@ public class AiService : IAiService } catch (Exception ex) { - Console.WriteLine($"AI分析图片时出错: {ex.Message}"); + logger.LogError(ex, "AI分析图片时出错"); return ("处理失败", $"AI分析过程中发生错误: {ex.Message}"); } } @@ -108,10 +100,10 @@ public class AiService : IAiService if (availableTags.Count == 0) return new List(); - string model = _configService["AI:Model"]; + string model = configService["AI:Model"]; 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格式前面不要加```,并且只包含确实匹配的标签名称。"; // 替换占位符 @@ -209,7 +201,7 @@ public class AiService : IAiService } catch (Exception ex) { - Console.WriteLine($"AI匹配标签时出错: {ex.Message}"); + logger.LogError(ex, "AI匹配标签时出错"); return new List(); } } @@ -222,7 +214,7 @@ public class AiService : IAiService // 获取配置好的 HttpClient var client = ConfigureHttpClient(); - string model = _configService["AI:Model"]; + string model = configService["AI:Model"]; var imageUrl = new ImageUrl { @@ -240,7 +232,7 @@ public class AiService : IAiService 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格式。"; // 如果允许新标签,则提供现有标签作为参考,但允许生成新标签 @@ -255,7 +247,7 @@ public class AiService : IAiService return new List(); 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格式,并且只包含上述列表中的标签名称。"; promptText = templatePrompt.Replace("{tagsText}", tagsText); @@ -358,7 +350,7 @@ public class AiService : IAiService } catch (Exception ex) { - Console.WriteLine($"AI从图片生成标签时出错: {ex.Message}"); + logger.LogError(ex, "AI从图片生成标签时出错"); return new List(); } } @@ -370,7 +362,7 @@ public class AiService : IAiService // 获取配置好的 HttpClient var client = ConfigureHttpClient(); - string model = _configService["AI:EmbeddingModel"]; + string model = configService["AI:EmbeddingModel"]; var requestContent = new { @@ -385,7 +377,7 @@ public class AiService : IAiService var embedResult = await response.Content.ReadFromJsonAsync(); if (embedResult?.Data == null || embedResult.Data.Length == 0) { - Console.WriteLine("嵌入向量API返回空结果"); + logger.LogWarning("嵌入向量API返回空结果"); return Array.Empty(); } @@ -393,7 +385,7 @@ public class AiService : IAiService } catch (Exception ex) { - Console.WriteLine($"获取嵌入向量时出错: {ex.Message}"); + logger.LogError(ex, "获取嵌入向量时出错"); return Array.Empty(); } } diff --git a/Services/Auth/AuthService.cs b/Services/Auth/AuthService.cs index 9a70dd9..d3116ae 100644 --- a/Services/Auth/AuthService.cs +++ b/Services/Auth/AuthService.cs @@ -5,12 +5,13 @@ using Foxel.Models.DataBase; using Foxel.Models.Request.Auth; using Foxel.Services.Configuration; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using static Foxel.Utils.AuthHelper; namespace Foxel.Services.Auth; -public class AuthService(IDbContextFactory dbContextFactory, IConfigService configuration) +public class AuthService(IDbContextFactory dbContextFactory, IConfigService configuration, ILogger logger) : IAuthService { public async Task<(bool success, string message, User? user)> RegisterUserAsync(RegisterRequest request) @@ -218,7 +219,7 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi if (!tokenResponse.IsSuccessStatusCode) { 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); } @@ -228,7 +229,7 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi if (!tokenJson.RootElement.TryGetProperty("access_token", out var accessTokenElement) || accessTokenElement.GetString() == null) { - Console.WriteLine($"GitHub响应中未找到access_token: {tokenResponseContent}"); + logger.LogError("GitHub响应中未找到access_token: {TokenResponseContent}", tokenResponseContent); return (GitHubAuthResult.TokenRequestFailed, "获取GitHub访问令牌失败,响应中未包含令牌。", null); } @@ -241,7 +242,7 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi if (!userResponse.IsSuccessStatusCode) { 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); } @@ -330,7 +331,7 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi if (!tokenResponse.IsSuccessStatusCode) { 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); } @@ -340,7 +341,7 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi if (!tokenJson.RootElement.TryGetProperty("access_token", out var accessTokenElement) || accessTokenElement.GetString() == null) { - Console.WriteLine($"LinuxDo响应中未找到access_token: {tokenResponseContent}"); + logger.LogError("LinuxDo响应中未找到access_token: {TokenResponseContent}", tokenResponseContent); return (LinuxDoAuthResult.TokenRequestFailed, "获取LinuxDo访问令牌失败,响应中未包含令牌。", null); } @@ -354,7 +355,7 @@ public class AuthService(IDbContextFactory dbContextFactory, IConfi if (!userResponse.IsSuccessStatusCode) { 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); } diff --git a/Services/Background/BackgroundTaskQueue.cs b/Services/Background/BackgroundTaskQueue.cs index 8624689..32a13c3 100644 --- a/Services/Background/BackgroundTaskQueue.cs +++ b/Services/Background/BackgroundTaskQueue.cs @@ -22,14 +22,17 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable private readonly SemaphoreSlim _signal; private readonly int _maxConcurrentTasks; private bool _isDisposed; + private readonly ILogger _logger; public BackgroundTaskQueue( IServiceProvider serviceProvider, IDbContextFactory contextFactory, - IConfigService configuration) + IConfigService configuration, + ILogger logger) { _serviceProvider = serviceProvider; _contextFactory = contextFactory; + _logger = logger; _activeTasks = new ConcurrentDictionary(); _pictureStatus = new ConcurrentDictionary(); _processingTasks = new List(); @@ -120,7 +123,7 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable if (unfinishedPictures.Any()) { - Console.WriteLine($"正在恢复 {unfinishedPictures.Count} 个未完成的图片处理任务"); + _logger.LogInformation("正在恢复 {Count} 个未完成的图片处理任务", unfinishedPictures.Count); foreach (var picture in unfinishedPictures) { @@ -131,14 +134,14 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable { // 重新加入队列 await QueuePictureProcessingTaskAsync(picture.Id, originalFilePath); - Console.WriteLine($"已恢复图片处理任务: ID={picture.Id}, 路径={originalFilePath}"); + _logger.LogInformation("已恢复图片处理任务: ID={PictureId}, 路径={FilePath}", picture.Id, originalFilePath); } else { // 如果文件不存在,则标记为失败 picture.ProcessingStatus = ProcessingStatus.Failed; 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 { - Console.WriteLine("没有需要恢复的图片处理任务"); + _logger.LogInformation("没有需要恢复的图片处理任务"); } } catch (Exception ex) { - Console.WriteLine($"恢复未完成的任务时发生错误: {ex.Message}"); + _logger.LogError(ex, "恢复未完成的任务时发生错误"); } } @@ -407,12 +410,12 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable } 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 { @@ -425,7 +428,7 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable } 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) { _signal.Dispose(); - Task.WhenAll(_processingTasks).Wait(5000); + try + { + Task.WhenAll(_processingTasks).Wait(5000); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "等待处理任务完成时超时"); + } } _isDisposed = true; diff --git a/Services/Configuration/ConfigService.cs b/Services/Configuration/ConfigService.cs index 1caa1aa..b54009a 100644 --- a/Services/Configuration/ConfigService.cs +++ b/Services/Configuration/ConfigService.cs @@ -37,29 +37,41 @@ public class ConfigService( return cachedValue; } - // 如果缓存中没有,从数据库获取 - await using var context = await contextFactory.CreateDbContextAsync(); - var config = await context.Configs.FirstOrDefaultAsync(c => c.Key == key); - - if (config == null) + try { - // 尝试从环境变量获取 - string envVarKey = key.ToUpper().Replace(".", "_").Replace("-", "_"); - string? envVarValue = Environment.GetEnvironmentVariable(envVarKey); + // 如果缓存中没有,从数据库获取 + await using var context = await contextFactory.CreateDbContextAsync(); + 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; } - - // 将配置值添加到缓存 - memoryCache.Set($"config:{key}", config.Value, _cacheExpiration); - - return config.Value; } public async Task GetValueAsync(string key, T? defaultValue = default) diff --git a/Services/Initializer/DatabaseInitializer.cs b/Services/Initializer/DatabaseInitializer.cs index 5bf5b5b..f511577 100644 --- a/Services/Initializer/DatabaseInitializer.cs +++ b/Services/Initializer/DatabaseInitializer.cs @@ -1,5 +1,6 @@ using Foxel.Models.DataBase; using Foxel.Services.Configuration; +using Foxel.Services.Logging; using Microsoft.EntityFrameworkCore; namespace Foxel.Services.Initializer; @@ -14,6 +15,9 @@ public class DatabaseInitializer( public async Task InitializeAsync() { + // 在初始化期间禁用数据库日志记录 + DatabaseLogger.SetDatabaseReady(false); + logger.LogInformation("开始检查数据库初始化状态..."); // 执行数据库迁移 @@ -24,6 +28,8 @@ public class DatabaseInitializer( configService[InitializationFlag] == "true") { logger.LogInformation("数据库已完成初始化,跳过初始化步骤"); + // 启用数据库日志记录 + DatabaseLogger.SetDatabaseReady(true); return; } @@ -86,6 +92,9 @@ public class DatabaseInitializer( await configService.SetConfigAsync(InitializationFlag, "true", "系统初始化完成标志"); logger.LogInformation("数据库配置初始化完成"); + + // 初始化完成后启用数据库日志记录 + DatabaseLogger.SetDatabaseReady(true); } private async Task MigrateDatabaseAsync() diff --git a/Services/Logging/DatabaseLogger.cs b/Services/Logging/DatabaseLogger.cs new file mode 100644 index 0000000..ab451b1 --- /dev/null +++ b/Services/Logging/DatabaseLogger.cs @@ -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 state) where TState : notnull => null!; + + public bool IsEnabled(LogLevel logLevel) => logLevel >= config.MinLevel; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func 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>(); + var httpContextAccessor = scope.ServiceProvider.GetService(); + + 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 IsDatabaseAvailableAsync(MyDbContext context) + { + try + { + await context.Database.ExecuteSqlRawAsync("SELECT 1 FROM \"Logs\" LIMIT 1"); + return true; + } + catch + { + return false; + } + } + + private static string? SerializeState(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(); + } + } +} \ No newline at end of file diff --git a/Services/Logging/DatabaseLoggerConfiguration.cs b/Services/Logging/DatabaseLoggerConfiguration.cs new file mode 100644 index 0000000..a406866 --- /dev/null +++ b/Services/Logging/DatabaseLoggerConfiguration.cs @@ -0,0 +1,7 @@ +namespace Foxel.Services.Logging; + +public class DatabaseLoggerConfiguration +{ + public LogLevel MinLevel { get; set; } = LogLevel.Information; + public bool Enabled { get; set; } = true; +} diff --git a/Services/Logging/DatabaseLoggerProvider.cs b/Services/Logging/DatabaseLoggerProvider.cs new file mode 100644 index 0000000..8d91823 --- /dev/null +++ b/Services/Logging/DatabaseLoggerProvider.cs @@ -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 config) + { + _serviceProvider = serviceProvider; + _config = config.Value; + } + + public ILogger CreateLogger(string categoryName) + { + return new DatabaseLogger(categoryName, _serviceProvider, _config); + } + + public void Dispose() { } +} diff --git a/Services/Management/ILogManagementService.cs b/Services/Management/ILogManagementService.cs new file mode 100644 index 0000000..1c86f43 --- /dev/null +++ b/Services/Management/ILogManagementService.cs @@ -0,0 +1,20 @@ +using Foxel.Models; +using Foxel.Models.Response.Log; + +namespace Foxel.Services.Management; + +public interface ILogManagementService +{ + Task> GetLogsAsync(int page, int pageSize, string? searchQuery = null, LogLevel? level = null, DateTime? startDate = null, DateTime? endDate = null); + Task GetLogByIdAsync(int id); + Task DeleteLogAsync(int id); + Task BatchDeleteLogsAsync(List ids); + Task ClearLogsByDateAsync(DateTime beforeDate); + Task ClearAllLogsAsync(); + + /// + /// 获取日志统计信息 + /// + /// 日志统计数据 + Task GetLogStatisticsAsync(); +} diff --git a/Services/Management/LogManagementService.cs b/Services/Management/LogManagementService.cs new file mode 100644 index 0000000..829385e --- /dev/null +++ b/Services/Management/LogManagementService.cs @@ -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 contextFactory) : ILogManagementService +{ + public async Task> 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 + { + Data = logs, + TotalCount = totalCount, + Page = page, + PageSize = pageSize + }; + } + + public async Task 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 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 BatchDeleteLogsAsync(List 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 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 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 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 + }; + } +} diff --git a/Services/Management/PictureManagementService.cs b/Services/Management/PictureManagementService.cs index 9e63987..6ac83af 100644 --- a/Services/Management/PictureManagementService.cs +++ b/Services/Management/PictureManagementService.cs @@ -3,12 +3,14 @@ using Foxel.Models.Response.Picture; using Foxel.Services.Configuration; using Foxel.Services.Storage; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace Foxel.Services.Management; public class PictureManagementService( IDbContextFactory contextFactory, - IStorageService storageService) : IPictureManagementService + IStorageService storageService, + ILogger logger) : IPictureManagementService { public async Task> GetPicturesAsync(int page = 1, int pageSize = 10, string? searchQuery = null, int? userId = null) { @@ -143,7 +145,7 @@ public class PictureManagementService( } catch (Exception ex) { - Console.WriteLine($"删除图片文件时出错:{ex.Message}"); + logger.LogWarning(ex, "删除图片文件时出错,图片ID: {PictureId}", id); } return true; } diff --git a/Services/Media/PictureService.cs b/Services/Media/PictureService.cs index 342a79c..cd933a7 100644 --- a/Services/Media/PictureService.cs +++ b/Services/Media/PictureService.cs @@ -11,6 +11,7 @@ using Foxel.Services.Storage; using Foxel.Services.VectorDB; using Foxel.Utils; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace Foxel.Services.Media; @@ -20,7 +21,8 @@ public class PictureService( IConfigService configuration, IBackgroundTaskQueue backgroundTaskQueue, IVectorDbService vectorDbService, - IStorageService storageService) + IStorageService storageService, + ILogger logger) : IPictureService { public async Task> GetPicturesAsync( @@ -60,7 +62,7 @@ public class PictureService( catch (Exception ex) { // 如果向量搜索失败,记录错误并回退到标准搜索 - Console.WriteLine($"向量搜索失败,回退到标准搜索: {ex.Message}"); + logger.LogWarning("向量搜索失败,回退到标准搜索: {Message}", ex.Message); // 如果是明确的配置错误,则向上抛出异常 if (ex.Message.Contains("请检查嵌入模型配置")) @@ -70,7 +72,6 @@ public class PictureService( } } - // 执行标准搜索(作为默认方法或向量搜索的回退选项) return await PerformStandardSearchAsync( dbContext, page, pageSize, searchQuery, tags, startDate, endDate, userId, sortBy, onlyWithGps, @@ -573,7 +574,7 @@ public class PictureService( } catch (Exception ex) { - Console.WriteLine($"生成缩略图失败: {ex.Message}"); + logger.LogError(ex, "生成缩略图失败"); } } @@ -769,7 +770,7 @@ public class PictureService( catch (Exception ex) { errorMsg = $"数据库记录已删除,但删除文件失败: {ex.Message}"; - Console.WriteLine($"删除图片文件时出错:{ex.Message}"); + logger.LogError(ex, "删除图片文件时出错"); } results[pictureId] = (true, errorMsg, userId); @@ -827,13 +828,13 @@ public class PictureService( else { // 记录获取到空向量的警告 - Console.WriteLine($"警告: 图片 {pictureId} 的嵌入向量为空,跳过向量更新"); + logger.LogWarning("图片 {PictureId} 的嵌入向量为空,跳过向量更新", pictureId); } } catch (Exception ex) { // 记录错误但不抛出异常,允许其他字段的更新继续进行 - Console.WriteLine($"更新图片 {pictureId} 的嵌入向量时出错: {ex.Message}"); + logger.LogError(ex, "更新图片 {PictureId} 的嵌入向量时出错", pictureId); // 不设置 picture.Embedding,保持原值不变 } } diff --git a/Services/Media/TagService.cs b/Services/Media/TagService.cs index 2f3c3e9..79f4412 100644 --- a/Services/Media/TagService.cs +++ b/Services/Media/TagService.cs @@ -3,10 +3,11 @@ using Foxel.Models; using Foxel.Models.DataBase; using Foxel.Models.Response.Tag; using Foxel.Services.Media; +using Microsoft.Extensions.Logging; namespace Foxel.Services; -public class TagService(IDbContextFactory contextFactory) : ITagService +public class TagService(IDbContextFactory contextFactory, ILogger logger) : ITagService { public async Task> GetFilteredTagsAsync( int page = 1, @@ -91,8 +92,7 @@ public class TagService(IDbContextFactory contextFactory) : ITagSer catch (Exception ex) { // 记录详细错误信息 - Console.WriteLine($"GetFilteredTagsAsync error: {ex.Message}"); - Console.WriteLine($"Stack trace: {ex.StackTrace}"); + logger.LogError(ex, "GetFilteredTagsAsync error"); throw; } diff --git a/Services/Storage/Providers/CosStorageProvider.cs b/Services/Storage/Providers/CosStorageProvider.cs index 2f3af2c..5cc9d70 100644 --- a/Services/Storage/Providers/CosStorageProvider.cs +++ b/Services/Storage/Providers/CosStorageProvider.cs @@ -6,17 +6,20 @@ using COSXML.Model.Tag; using COSXML.Transfer; using Foxel.Services.Attributes; using Foxel.Services.Configuration; +using Microsoft.Extensions.Logging; namespace Foxel.Services.Storage.Providers; public class CustomQCloudCredentialProvider : DefaultSessionQCloudCredentialProvider { private readonly IConfigService _configService; + private readonly ILogger _logger; - public CustomQCloudCredentialProvider(IConfigService configService) + public CustomQCloudCredentialProvider(IConfigService configService, ILogger logger) : base(null, null, 0L, null) { _configService = configService; + _logger = logger; Refresh(); } @@ -34,14 +37,14 @@ public class CustomQCloudCredentialProvider : DefaultSessionQCloudCredentialProv } catch (Exception ex) { - Console.WriteLine($"刷新临时密钥时出错: {ex.Message}"); + _logger.LogError(ex, "刷新临时密钥时出错"); throw; } } } [StorageProvider(StorageType.Cos)] -public class CosStorageProvider(IConfigService configService) : IStorageProvider +public class CosStorageProvider(IConfigService configService, ILogger logger) : IStorageProvider { private CosXml CreateClient() { @@ -51,7 +54,10 @@ public class CosStorageProvider(IConfigService configService) : IStorageProvider .SetDebugLog(true) .Build(); - var cosCredentialProvider = new CustomQCloudCredentialProvider(configService); + var cosCredentialProvider = new CustomQCloudCredentialProvider(configService, + logger.IsEnabled(LogLevel.Debug) ? + Microsoft.Extensions.Logging.LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger() : + Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); return new CosXmlServer(config, cosCredentialProvider); } @@ -93,17 +99,17 @@ public class CosStorageProvider(IConfigService configService) : IStorageProvider } catch (CosClientException clientEx) { - Console.WriteLine($"COS客户端异常: {clientEx}"); + logger.LogError(clientEx, "COS客户端异常"); throw; } catch (CosServerException serverEx) { - Console.WriteLine($"COS服务器异常: {serverEx.GetInfo()}"); + logger.LogError(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo()); throw; } catch (Exception ex) { - Console.WriteLine($"上传文件到腾讯云COS时出错: {ex.Message}"); + logger.LogError(ex, "上传文件到腾讯云COS时出错"); throw; } } @@ -121,15 +127,15 @@ public class CosStorageProvider(IConfigService configService) : IStorageProvider } catch (CosClientException clientEx) { - Console.WriteLine($"COS客户端异常: {clientEx}"); + logger.LogWarning(clientEx, "COS客户端异常"); } catch (CosServerException serverEx) { - Console.WriteLine($"COS服务器异常: {serverEx.GetInfo()}"); + logger.LogWarning(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo()); } 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) { - Console.WriteLine($"生成腾讯云COS文件URL时出错: {ex.Message}"); + logger.LogError(ex, "生成腾讯云COS文件URL时出错"); return "/images/unavailable.gif"; } } @@ -204,17 +210,17 @@ public class CosStorageProvider(IConfigService configService) : IStorageProvider } catch (CosClientException clientEx) { - Console.WriteLine($"COS客户端异常: {clientEx}"); + logger.LogError(clientEx, "COS客户端异常"); throw; } catch (CosServerException serverEx) { - Console.WriteLine($"COS服务器异常: {serverEx.GetInfo()}"); + logger.LogError(serverEx, "COS服务器异常: {ServerInfo}", serverEx.GetInfo()); throw; } catch (Exception ex) { - Console.WriteLine($"从腾讯云COS下载文件时出错: {ex.Message}"); + logger.LogError(ex, "从腾讯云COS下载文件时出错"); throw; } } diff --git a/Services/Storage/Providers/LocalStorageProvider.cs b/Services/Storage/Providers/LocalStorageProvider.cs index 6dcd3d3..c1ae41d 100644 --- a/Services/Storage/Providers/LocalStorageProvider.cs +++ b/Services/Storage/Providers/LocalStorageProvider.cs @@ -1,10 +1,11 @@ using Foxel.Services.Attributes; using Foxel.Services.Configuration; +using Microsoft.Extensions.Logging; namespace Foxel.Services.Storage.Providers; [StorageProvider(StorageType.Local)] -public class LocalStorageProvider(IConfigService configService) : IStorageProvider +public class LocalStorageProvider(IConfigService configService, ILogger logger) : IStorageProvider { private readonly string _baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Uploads"); diff --git a/Services/Storage/Providers/S3StorageProvider.cs b/Services/Storage/Providers/S3StorageProvider.cs index 966d280..dd83756 100644 --- a/Services/Storage/Providers/S3StorageProvider.cs +++ b/Services/Storage/Providers/S3StorageProvider.cs @@ -3,11 +3,12 @@ using Amazon.S3.Model; using Amazon.S3.Transfer; using Foxel.Services.Attributes; using Foxel.Services.Configuration; +using Microsoft.Extensions.Logging; namespace Foxel.Services.Storage.Providers; [StorageProvider(StorageType.S3)] -public class S3StorageProvider(IConfigService configService) : IStorageProvider +public class S3StorageProvider(IConfigService configService, ILogger logger) : IStorageProvider { private AmazonS3Client CreateClient() { @@ -63,7 +64,7 @@ public class S3StorageProvider(IConfigService configService) : IStorageProvider } catch (Exception ex) { - Console.WriteLine($"上传文件到S3时出错: {ex.Message}"); + logger.LogError(ex, "上传文件到S3时出错"); throw; } } @@ -86,7 +87,7 @@ public class S3StorageProvider(IConfigService configService) : IStorageProvider } 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) { - Console.WriteLine($"生成S3文件URL时出错: {ex.Message}"); + logger.LogError(ex, "生成S3文件URL时出错"); return "/images/unavailable.gif"; } } @@ -159,7 +160,7 @@ public class S3StorageProvider(IConfigService configService) : IStorageProvider } catch (Exception ex) { - Console.WriteLine($"从S3下载文件时出错: {ex.Message}"); + logger.LogError(ex, "从S3下载文件时出错"); throw; } } diff --git a/Services/Storage/Providers/TelegramStorageProvider.cs b/Services/Storage/Providers/TelegramStorageProvider.cs index 1d921d0..6657093 100644 --- a/Services/Storage/Providers/TelegramStorageProvider.cs +++ b/Services/Storage/Providers/TelegramStorageProvider.cs @@ -4,11 +4,12 @@ using System.Text.Json.Serialization; using Foxel.Services.Attributes; using Foxel.Services.Configuration; using System.Net; +using Microsoft.Extensions.Logging; namespace Foxel.Services.Storage.Providers; [StorageProvider(StorageType.Telegram)] -public class TelegramStorageProvider(IConfigService configService) : IStorageProvider +public class TelegramStorageProvider(IConfigService configService, ILogger logger) : IStorageProvider { public async Task SaveAsync(Stream fileStream, string fileName, string contentType) { @@ -40,7 +41,7 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro if (!response.IsSuccessStatusCode) { 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}"); } @@ -90,7 +91,7 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro } catch (Exception ex) { - Console.WriteLine($"发送文件到 Telegram 时出错: {ex.Message}"); + logger.LogError(ex, "发送文件到 Telegram 时出错"); throw; } } @@ -114,7 +115,7 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro } 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) { - Console.WriteLine($"生成 Telegram 文件 URL 时出错: {ex.Message}"); + logger.LogError(ex, "生成 Telegram 文件 URL 时出错"); return $"/images/unavailable.gif"; } } @@ -203,7 +204,7 @@ public class TelegramStorageProvider(IConfigService configService) : IStoragePro } catch (Exception ex) { - Console.WriteLine($"下载 Telegram 文件时出错: {ex.Message}"); + logger.LogError(ex, "下载 Telegram 文件时出错"); throw; } } diff --git a/Services/Storage/Providers/WebDAVStorageProvider.cs b/Services/Storage/Providers/WebDAVStorageProvider.cs index ded01fa..a08aabb 100644 --- a/Services/Storage/Providers/WebDAVStorageProvider.cs +++ b/Services/Storage/Providers/WebDAVStorageProvider.cs @@ -2,11 +2,12 @@ using System.Net.Http.Headers; using System.Text; using Foxel.Services.Attributes; using Foxel.Services.Configuration; +using Microsoft.Extensions.Logging; namespace Foxel.Services.Storage.Providers; [StorageProvider(StorageType.WebDAV)] -public class WebDavStorageProvider(IConfigService configService) : IStorageProvider +public class WebDavStorageProvider(IConfigService configService, ILogger logger) : IStorageProvider { private HttpClient CreateClient() { @@ -55,7 +56,7 @@ public class WebDavStorageProvider(IConfigService configService) : IStorageProvi } catch (Exception ex) { - Console.WriteLine($"上传文件到WebDAV时出错: {ex.Message}"); + logger.LogError(ex, "上传文件到WebDAV时出错"); throw; } } @@ -80,7 +81,7 @@ public class WebDavStorageProvider(IConfigService configService) : IStorageProvi } 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) { - Console.WriteLine($"生成WebDAV文件URL时出错: {ex.Message}"); + logger.LogError(ex, "生成WebDAV文件URL时出错"); return "/images/unavailable.gif"; } } @@ -144,7 +145,7 @@ public class WebDavStorageProvider(IConfigService configService) : IStorageProvi } catch (Exception ex) { - Console.WriteLine($"从WebDAV下载文件时出错: {ex.Message}"); + logger.LogError(ex, "从WebDAV下载文件时出错"); throw; } } @@ -210,7 +211,7 @@ public class WebDavStorageProvider(IConfigService configService) : IStorageProvi } catch (Exception ex) { - Console.WriteLine($"确保WebDAV目录存在时出错: {ex.Message}"); + logger.LogError(ex, "确保WebDAV目录存在时出错"); throw; } } diff --git a/Services/Storage/StorageService.cs b/Services/Storage/StorageService.cs index 2d218e8..36e86d1 100644 --- a/Services/Storage/StorageService.cs +++ b/Services/Storage/StorageService.cs @@ -1,5 +1,6 @@ using System.Reflection; using Foxel.Services.Attributes; +using Microsoft.Extensions.Logging; namespace Foxel.Services.Storage; @@ -9,11 +10,13 @@ namespace Foxel.Services.Storage; public class StorageService : IStorageService { private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; private readonly Dictionary _storageProviders = new(); - public StorageService(IServiceProvider serviceProvider) + public StorageService(IServiceProvider serviceProvider, ILogger logger) { _serviceProvider = serviceProvider; + _logger = logger; RegisterStorageProviders(); } @@ -47,7 +50,7 @@ public class StorageService : IStorageService } catch (Exception ex) { - Console.WriteLine($"扫描程序集 {assembly.FullName} 出错: {ex.Message}"); + _logger.LogWarning(ex, "扫描程序集 {AssemblyName} 时发生错误", assembly.FullName); // 继续扫描其他程序集 } } diff --git a/Utils/AiHelper.cs b/Utils/AiHelper.cs index a73a3fe..78ac52d 100644 --- a/Utils/AiHelper.cs +++ b/Utils/AiHelper.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Logging; + namespace Foxel.Utils; public static class AiHelper @@ -6,8 +8,9 @@ public static class AiHelper /// 从AI响应中提取标题和描述 /// /// AI生成的响应文本 + /// 日志记录器 /// 包含标题和描述的元组 - 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 description = "AI生成的描述"; @@ -83,7 +86,7 @@ public static class AiHelper } catch (Exception ex) { - Console.WriteLine($"解析AI响应时出错: {ex.Message}"); + logger?.LogWarning(ex, "解析AI响应时出错"); description = $"原始AI响应: {aiResponse}"; } diff --git a/Web/src/api/index.ts b/Web/src/api/index.ts index 8f63f0a..1c4b43c 100644 --- a/Web/src/api/index.ts +++ b/Web/src/api/index.ts @@ -88,3 +88,13 @@ export { rebuildVectors } from './vectorDbApi'; +// 导出LogManagement API +export { + getLogs, + getLogById, + deleteLog, + batchDeleteLogs, + clearLogs, + getLogStatistics +} from './logManagementApi'; + diff --git a/Web/src/api/logManagementApi.ts b/Web/src/api/logManagementApi.ts new file mode 100644 index 0000000..2068601 --- /dev/null +++ b/Web/src/api/logManagementApi.ts @@ -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> => { + 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; +}; + +// 根据ID获取单个日志 +export const getLogById = async (id: number): Promise> => { + return fetchApi( + `/management/log/get_log/${id}`, + { method: 'GET' } + ); +}; + +// 删除日志 +export const deleteLog = async (id: number): Promise> => { + return fetchApi( + '/management/log/delete_log', + { + method: 'POST', + body: JSON.stringify(id) + } + ); +}; + +// 批量删除日志 +export const batchDeleteLogs = async ( + ids: number[] +): Promise> => { + return fetchApi( + '/management/log/batch_delete_logs', + { + method: 'POST', + body: JSON.stringify(ids) + } + ); +}; + +// 清空日志 +export const clearLogs = async ( + request: ClearLogsRequest +): Promise> => { + return fetchApi( + '/management/log/clear_logs', + { + method: 'POST', + body: JSON.stringify(request) + } + ); +}; + +// 获取日志统计信息 +export const getLogStatistics = async (): Promise> => { + return fetchApi( + '/management/log/get_statistics', + { method: 'GET' } + ); +}; diff --git a/Web/src/api/types.ts b/Web/src/api/types.ts index a4587c6..76ba1ea 100644 --- a/Web/src/api/types.ts +++ b/Web/src/api/types.ts @@ -315,3 +315,56 @@ export interface UserDetailResponse { createdAt: Date; 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; +} diff --git a/Web/src/pages/admin/log/Index.tsx b/Web/src/pages/admin/log/Index.tsx new file mode 100644 index 0000000..9e9d23b --- /dev/null +++ b/Web/src/pages/admin/log/Index.tsx @@ -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 = { + 0: 'Trace', + 1: 'Debug', + 2: 'Information', + 3: 'Warning', + 4: 'Error', + 5: 'Critical' +}; + +// 日志级别颜色映射 +const LOG_LEVEL_COLORS: Record = { + Trace: 'default', + Debug: 'blue', + Information: 'green', + Warning: 'orange', + Error: 'red', + Critical: 'magenta' +}; + +// 日志级别图标映射 +const LOG_LEVEL_ICONS: Record = { + Trace: , + Debug: , + Information: , + Warning: , + Error: , + Critical: +}; + +const AdminLogManagement: React.FC = () => { + const {isMobile} = useOutletContext<{ isMobile: boolean; isAdminPanel?: boolean }>(); + + // 状态管理 + const [loading, setLoading] = useState(false); + const [logs, setLogs] = useState([]); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + 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({}); + const [searchText, setSearchText] = useState(''); + const [selectedLevel, setSelectedLevel] = useState(); + const [dateRange, setDateRange] = useState<[Dayjs, Dayjs] | null>(null); + + // 日志详情抽屉 + const [detailDrawerOpen, setDetailDrawerOpen] = useState(false); + const [selectedLog, setSelectedLog] = useState(null); + const [detailLoading, setDetailLoading] = useState(false); + + // 统计信息 + const [logStats, setLogStats] = useState({ + 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: , + 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: , + 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>(() => [ + { + title: '时间', + dataIndex: 'timestamp', + key: 'timestamp', + width: 160, + render: (timestamp: Date) => ( + + {dayjs(timestamp).format('MM-DD HH:mm:ss')} + + ), + sorter: true, + }, + { + title: '级别', + dataIndex: 'level', + key: 'level', + width: 120, + render: (level: LogLevel | number) => { + const levelString = getLogLevelString(level); + return ( + + {levelString} + + ); + }, + 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) => ( + + {category} + + ), + }, + { + title: '消息', + dataIndex: 'message', + key: 'message', + ellipsis: true, + render: (message: string) => ( + + {message} + + ), + }, + { + title: '请求信息', + key: 'request', + width: 120, + responsive: ['lg'], + render: (_, record) => { + if (record.requestPath) { + return ( + + + {record.requestMethod} {record.statusCode} + + + {record.requestPath} + + + ); + } + return '-'; + }, + }, + { + title: 'IP地址', + dataIndex: 'ipAddress', + key: 'ipAddress', + width: 120, + responsive: ['xl'], + render: (ip: string) => ip || '-', + }, + { + title: '操作', + key: 'action', + width: 120, + render: (_, record) => ( + + + + + + + + + + {/* 操作按钮 */} + + + + + + + + + + + + + + {/* 数据表格 */} + { + setPagination(prev => ({...prev, current: page, pageSize: pageSize || 20})); + fetchLogs({...filters, page, pageSize}); + }, + }} + loading={loading} + size={isMobile ? 'small' : 'middle'} + scroll={isMobile ? {x: 800} : undefined} + /> + + + {/* 日志详情抽屉 */} + setDetailDrawerOpen(false)} + > + {selectedLog && ( + + + + + 时间: + + + {dayjs(selectedLog.timestamp).format('YYYY-MM-DD HH:mm:ss')} + + + + 级别: + + + + {getLogLevelString(selectedLog.level)} + + + + + 分类: + + + {selectedLog.category} + + + {selectedLog.eventId != null && ( + <> + + 事件ID: + + + {selectedLog.eventId} + + + )} + + + + + + {selectedLog.message} + + + + {selectedLog.exception && ( + + + {selectedLog.exception} + + } + type="error" + showIcon + /> + + )} + + {(selectedLog.requestPath || selectedLog.ipAddress) && ( + + + {selectedLog.requestMethod && ( + <> + + 请求方法: + + + {selectedLog.requestMethod} + + + )} + + {selectedLog.requestPath && ( + <> + + 请求路径: + + + {selectedLog.requestPath} + + + )} + + {selectedLog.statusCode && ( + <> + + 状态码: + + + = 400 ? 'error' : 'success'} + text={selectedLog.statusCode} + /> + + + )} + + {selectedLog.ipAddress && ( + <> + + IP地址: + + + {selectedLog.ipAddress} + + + )} + + {selectedLog.userId && ( + <> + + 用户ID: + + + {selectedLog.userId} + + + )} + + + )} + + {selectedLog.properties && ( + +
+                  {selectedLog.properties}
+                
+
+ )} + + )} + + + ); +}; + +export default AdminLogManagement; diff --git a/Web/src/routes/index.tsx b/Web/src/routes/index.tsx index 60149da..802d30c 100644 --- a/Web/src/routes/index.tsx +++ b/Web/src/routes/index.tsx @@ -7,7 +7,8 @@ import { SettingOutlined, CompassOutlined, DashboardOutlined, - UserOutlined + UserOutlined, + LogoutOutlined } from '@ant-design/icons'; 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 PictureManagement from '../pages/admin/pictures/Index'; import UserDetail from '../pages/admin/users/UserDetail'; +import AdminLogManagement from '../pages/admin/log/Index'; export interface RouteConfig { path: string; @@ -168,6 +170,17 @@ const routes: RouteConfig[] = [ breadcrumb: { title: '图片管理' } + }, + { + path: 'log', + key: 'admin-log', + icon: , + label: '日志中心', + element: , + area: 'admin', + breadcrumb: { + title: '日志中心' + } }, { path: 'system',