feat(logManagement): implement log management service

This commit is contained in:
shiyu
2025-06-06 11:39:39 +08:00
parent a73752bcc8
commit a95651b04a
34 changed files with 1644 additions and 108 deletions

View 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);
}
}
}

View 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;
}
}

View File

@@ -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
View 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格式存储额外属性
}

View File

@@ -0,0 +1,7 @@
namespace Foxel.Models.Request.Log;
public class ClearLogsRequest
{
public DateTime? BeforeDate { get; set; }
public bool ClearAll { get; set; } = false;
}

View 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; }
}

View 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; }
}

View File

@@ -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!;
}

View File

@@ -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();

View File

@@ -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>();
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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()

View 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();
}
}
}

View 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;
}

View 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() { }
}

View 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();
}

View 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
};
}
}

View File

@@ -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;
}

View File

@@ -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保持原值不变
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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");

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
// 继续扫描其他程序集
}
}

View File

@@ -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}";
}

View File

@@ -88,3 +88,13 @@ export {
rebuildVectors
} from './vectorDbApi';
// 导出LogManagement API
export {
getLogs,
getLogById,
deleteLog,
batchDeleteLogs,
clearLogs,
getLogStatistics
} from './logManagementApi';

View 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' }
);
};

View File

@@ -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;
}

View 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;

View File

@@ -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',