mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-27 10:11:55 +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<IUserManagementService, UserManagementService>();
|
||||
services.AddSingleton<IPictureManagementService, PictureManagementService>();
|
||||
services.AddSingleton<ILogManagementService, LogManagementService>();
|
||||
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
||||
services.AddHostedService<QueuedHostedService>();
|
||||
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<Album> Albums { 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 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();
|
||||
|
||||
@@ -5,29 +5,21 @@ using Foxel.Utils;
|
||||
|
||||
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 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>();
|
||||
|
||||
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<string>();
|
||||
}
|
||||
}
|
||||
@@ -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<string>();
|
||||
|
||||
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<string>();
|
||||
}
|
||||
}
|
||||
@@ -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<EmbeddingResponse>();
|
||||
if (embedResult?.Data == null || embedResult.Data.Length == 0)
|
||||
{
|
||||
Console.WriteLine("嵌入向量API返回空结果");
|
||||
logger.LogWarning("嵌入向量API返回空结果");
|
||||
return Array.Empty<float>();
|
||||
}
|
||||
|
||||
@@ -393,7 +385,7 @@ public class AiService : IAiService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"获取嵌入向量时出错: {ex.Message}");
|
||||
logger.LogError(ex, "获取嵌入向量时出错");
|
||||
return Array.Empty<float>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MyDbContext> dbContextFactory, IConfigService configuration)
|
||||
public class AuthService(IDbContextFactory<MyDbContext> dbContextFactory, IConfigService configuration, ILogger<AuthService> logger)
|
||||
: IAuthService
|
||||
{
|
||||
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)
|
||||
{
|
||||
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<MyDbContext> 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<MyDbContext> 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<MyDbContext> 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<MyDbContext> 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<MyDbContext> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,14 +22,17 @@ public sealed class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable
|
||||
private readonly SemaphoreSlim _signal;
|
||||
private readonly int _maxConcurrentTasks;
|
||||
private bool _isDisposed;
|
||||
private readonly ILogger<BackgroundTaskQueue> _logger;
|
||||
|
||||
public BackgroundTaskQueue(
|
||||
IServiceProvider serviceProvider,
|
||||
IDbContextFactory<MyDbContext> contextFactory,
|
||||
IConfigService configuration)
|
||||
IConfigService configuration,
|
||||
ILogger<BackgroundTaskQueue> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_contextFactory = contextFactory;
|
||||
_logger = logger;
|
||||
_activeTasks = new ConcurrentDictionary<Guid, PictureProcessingTask>();
|
||||
_pictureStatus = new ConcurrentDictionary<int, PictureProcessingStatus>();
|
||||
_processingTasks = new List<Task>();
|
||||
@@ -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;
|
||||
|
||||
@@ -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<T?> GetValueAsync<T>(string key, T? defaultValue = default)
|
||||
|
||||
@@ -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()
|
||||
|
||||
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.Storage;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Foxel.Services.Management;
|
||||
|
||||
public class PictureManagementService(
|
||||
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)
|
||||
{
|
||||
@@ -143,7 +145,7 @@ public class PictureManagementService(
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"删除图片文件时出错:{ex.Message}");
|
||||
logger.LogWarning(ex, "删除图片文件时出错,图片ID: {PictureId}", id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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<PictureService> logger)
|
||||
: IPictureService
|
||||
{
|
||||
public async Task<PaginatedResult<PictureResponse>> 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,保持原值不变
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MyDbContext> contextFactory) : ITagService
|
||||
public class TagService(IDbContextFactory<MyDbContext> contextFactory, ILogger<TagService> logger) : ITagService
|
||||
{
|
||||
public async Task<PaginatedResult<TagResponse>> GetFilteredTagsAsync(
|
||||
int page = 1,
|
||||
@@ -91,8 +92,7 @@ public class TagService(IDbContextFactory<MyDbContext> contextFactory) : ITagSer
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录详细错误信息
|
||||
Console.WriteLine($"GetFilteredTagsAsync error: {ex.Message}");
|
||||
Console.WriteLine($"Stack trace: {ex.StackTrace}");
|
||||
logger.LogError(ex, "GetFilteredTagsAsync error");
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -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<CustomQCloudCredentialProvider> _logger;
|
||||
|
||||
public CustomQCloudCredentialProvider(IConfigService configService)
|
||||
public CustomQCloudCredentialProvider(IConfigService configService, ILogger<CustomQCloudCredentialProvider> 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<CosStorageProvider> 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<CustomQCloudCredentialProvider>() :
|
||||
Microsoft.Extensions.Logging.Abstractions.NullLogger<CustomQCloudCredentialProvider>.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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<LocalStorageProvider> logger) : IStorageProvider
|
||||
{
|
||||
private readonly string _baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Uploads");
|
||||
|
||||
|
||||
@@ -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<S3StorageProvider> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TelegramStorageProvider> logger) : IStorageProvider
|
||||
{
|
||||
public async Task<string> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<WebDavStorageProvider> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<StorageService> _logger;
|
||||
private readonly Dictionary<StorageType, Type> _storageProviders = new();
|
||||
|
||||
public StorageService(IServiceProvider serviceProvider)
|
||||
public StorageService(IServiceProvider serviceProvider, ILogger<StorageService> 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);
|
||||
// 继续扫描其他程序集
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Foxel.Utils;
|
||||
|
||||
public static class AiHelper
|
||||
@@ -6,8 +8,9 @@ public static class AiHelper
|
||||
/// 从AI响应中提取标题和描述
|
||||
/// </summary>
|
||||
/// <param name="aiResponse">AI生成的响应文本</param>
|
||||
/// <param name="logger">日志记录器</param>
|
||||
/// <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 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}";
|
||||
}
|
||||
|
||||
|
||||
@@ -88,3 +88,13 @@ export {
|
||||
rebuildVectors
|
||||
} 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;
|
||||
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,
|
||||
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: <LogoutOutlined />,
|
||||
label: '日志中心',
|
||||
element: <AdminLogManagement />,
|
||||
area: 'admin',
|
||||
breadcrumb: {
|
||||
title: '日志中心'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'system',
|
||||
|
||||
Reference in New Issue
Block a user