Files
Foxel/Services/Configuration/ConfigService.cs

248 lines
7.6 KiB
C#

using System.Text.Json;
using Foxel.Models.DataBase;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
namespace Foxel.Services.Configuration;
public class ConfigService(
IDbContextFactory<MyDbContext> contextFactory,
IMemoryCache memoryCache,
ILogger<ConfigService> logger)
: IConfigService
{
// 用于存储需要标记为私密的配置键
private static readonly HashSet<string> _secretKeys = new(StringComparer.OrdinalIgnoreCase)
{
"AI:ApiKey",
"Authentication:GitHubClientSecret",
"Storage:TelegramStorageBotToken",
"Storage:S3StorageAccessKey",
"Storage:S3StorageSecretKey",
"Storage:CosStorageSecretId",
"Storage:CosStorageSecretKey",
"Storage:WebDAVPassword",
};
// 缓存过期时间
private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(30);
public string this[string key] => GetValueAsync(key).GetAwaiter().GetResult() ?? string.Empty;
public async Task<string?> GetValueAsync(string key)
{
// 尝试从缓存获取配置值
if (memoryCache.TryGetValue($"config:{key}", out string? cachedValue))
{
return cachedValue;
}
try
{
// 如果缓存中没有,从数据库获取
await using var context = await contextFactory.CreateDbContextAsync();
var config = await context.Configs.FirstOrDefaultAsync(c => c.Key == key);
if (config == null)
{
// 尝试从环境变量获取
string envVarKey = key.ToUpper().Replace(".", "_").Replace("-", "_");
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;
}
}
public async Task<T?> GetValueAsync<T>(string key, T? defaultValue = default)
{
var value = await GetValueAsync(key);
if (string.IsNullOrEmpty(value))
return defaultValue;
try
{
return JsonSerializer.Deserialize<T>(value);
}
catch (Exception ex)
{
logger.LogError(ex, "无法将配置值反序列化为所需类型: {Type}", typeof(T).Name);
return defaultValue;
}
}
public async Task<Config?> GetConfigAsync(string key)
{
await using var context = await contextFactory.CreateDbContextAsync();
var config = await context.Configs.FirstOrDefaultAsync(c => c.Key == key);
// 如果配置是私密的,返回值设为空字符串
if (config?.IsSecret == true || _secretKeys.Contains(config.Key))
{
var displayConfig = new Config
{
Id = config.Id,
Key = config.Key,
Value = string.Empty,
Description = config.Description,
IsSecret = true,
CreatedAt = config.CreatedAt,
UpdatedAt = config.UpdatedAt
};
return displayConfig;
}
return config;
}
public async Task<List<Config>> GetAllConfigsAsync()
{
await using var context = await contextFactory.CreateDbContextAsync();
var configs = await context.Configs.OrderBy(c => c.Key).ToListAsync();
foreach (var config in configs.Where(c => c.IsSecret))
{
config.Value = string.Empty;
}
return configs;
}
public async Task<Config> SetConfigAsync(string key, string value, string? description = null)
{
if (string.IsNullOrWhiteSpace(key))
throw new ArgumentException("配置键不能为空", nameof(key));
await using var context = await contextFactory.CreateDbContextAsync();
var config = await context.Configs.FirstOrDefaultAsync(c => c.Key == key);
if (config == null)
{
config = new Config
{
Key = key,
Value = value,
Description = description ?? string.Empty,
IsSecret = _secretKeys.Contains(key), // 如果键在私密列表中,则设为私密
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
context.Configs.Add(config);
}
else
{
// 如果键在私密列表中,则设为私密
if (_secretKeys.Contains(key))
{
config.IsSecret = true;
}
if (!(config.IsSecret && string.IsNullOrEmpty(value)))
{
config.Value = value;
}
if (description != null)
{
config.Description = description;
}
config.UpdatedAt = DateTime.UtcNow;
}
await context.SaveChangesAsync();
if (!config.IsSecret)
{
memoryCache.Set($"config:{key}", value, _cacheExpiration);
}
return config;
}
public async Task<bool> DeleteConfigAsync(string key)
{
await using var context = await contextFactory.CreateDbContextAsync();
var config = await context.Configs.FirstOrDefaultAsync(c => c.Key == key);
if (config == null)
return false;
context.Configs.Remove(config);
await context.SaveChangesAsync();
memoryCache.Remove($"config:{key}");
return true;
}
public async Task<bool> ExistsAsync(string key)
{
await using var context = await contextFactory.CreateDbContextAsync();
return await context.Configs.AnyAsync(c => c.Key == key);
}
public async Task<Dictionary<string, string>> BackupConfigsAsync()
{
var configs = await GetAllConfigsAsync();
var backup = new Dictionary<string, string>();
foreach (var config in configs)
{
backup[config.Key] = config.Value;
}
return backup;
}
public async Task<bool> RestoreConfigsAsync(Dictionary<string, string> configBackup)
{
if (configBackup == null || configBackup.Count == 0)
return false;
try
{
await using var context = await contextFactory.CreateDbContextAsync();
await using var transaction = await context.Database.BeginTransactionAsync();
try
{
foreach (var (key, value) in configBackup)
{
await SetConfigAsync(key, value);
}
await transaction.CommitAsync();
return true;
}
catch (Exception)
{
await transaction.RollbackAsync();
throw;
}
}
catch (Exception ex)
{
logger.LogError(ex, "恢复配置时出错");
return false;
}
}
}