using System.Security.Cryptography; namespace MeloongCore; public interface IConfigProvider { /// /// 将数据写入缓存。 /// 这不会写入数据,还需要调用 以保存。 /// void SetToCache(string key, T? value, bool encrypted); /// /// 将指定数据从缓存中移除。 /// 这不会写入数据,还需要调用 以保存。 /// void RemoveFromCache(string key); T? Read(string key, T? defaultValue, bool encrypted); bool HasValue(string key); void DiscardCache(); void Save(); } public class JsonConfigProvider : IConfigProvider { private readonly string filePath; public JsonConfigProvider(string filePath) { this.filePath = filePath; InitJson(); } // 原始文件缓存 private Lazy json; // 反序列化后的结果缓存 private readonly ConcurrentDictionary cache = new(); private void InitJson() { json = new(() => { try { return FileUtils.Exists(filePath) ? ((JObject?) FileUtils.ReadAsJson(filePath) ?? []) : []; } catch (Exception ex) { Logger.Error(ex, $"读取配置文件失败({filePath})", LogBehavior.Alert); return []; } }); } public void SetToCache(string key, T? value, bool encrypted) { cache[key] = value; if (value is null) { // 直接在 JSON 中表示为 null json.Value[key] = JValue.CreateNull(); } else if (encrypted) { // 需要加密,在 JSON 中保存密文字符串 json.Value[key] = CryptographyUtils.AesEncrypt(value is string str ? str : JsonConvert.SerializeObject(value)); // TODO: 加密改为使用识别码 } else { // 用 JToken 保留原始结构 json.Value[key] = JToken.FromObject(value); } } public T? Read(string key, T? defaultValue, bool encrypted) { if (cache.TryGetValue(key, out var cachedValue)) return (T?) cachedValue; // 读取缓存 if (!json.Value.TryGetValue(key, out var entry)) return defaultValue; // 未找到该键 try { T? result; if (entry.Type is JTokenType.Null or JTokenType.Undefined) { result = default; } else if (encrypted) { string plainText = CryptographyUtils.AesDecrypt(entry.Value())!; result = typeof(T) == typeof(string) ? (T) (object) plainText : JsonConvert.DeserializeObject(plainText); } else { result = entry.ToObject(); } cache[key] = result; return result; } catch (CryptographicException ex) { Logger.Error(ex, $"解密配置失败,该配置将被重置({key})", LogBehavior.Alert); RemoveFromCache(key); Save(); return defaultValue; } } public bool HasValue(string key) => json.Value.ContainsKey(key); public void RemoveFromCache(string key) { json.Value.Remove(key); cache.TryRemove(key, out _); } public void DiscardCache() { InitJson(); cache.Clear(); } public void Save() => FileUtils.Write(filePath, json.Value.ToString(Formatting.Indented)); } public static class ConfigUtils { public static readonly JsonConfigProvider AppData = new(Path.Combine(Paths.AppDataThenName, "config.json")); /// /// 从 secret.json 中读取特定密钥,如果未找到对应密钥则抛出异常。 /// public static string GetSecret(string key) => secret.Read(key, null, false) ?? throw new KeyNotFoundException($"Secret not found: {key}"); private static readonly JsonConfigProvider secret = new(Path.Combine(Paths.AppData, "secret.json")); } public class ConfigEntry(string key, T? defaultValue, IConfigProvider? defaultProvider = null, bool encrypted = false) { /// /// 当设置项的值实际被改变时触发,参数为新值。 /// public event Action? Changed; public IConfigProvider DefaultProvider => defaultProvider ?? ConfigUtils.AppData; public void Set(T? value, IConfigProvider? providerOverride = null) { var provider = providerOverride ?? DefaultProvider; T? current = Get(provider); if ((current is null && value is null) || (current is not null && value is not null && current.Equals(value))) return; // 值未改变,无需更新 provider.SetToCache(key, value, encrypted); provider.Save(); Changed?.Invoke(value, provider); } public void Reset(IConfigProvider? providerOverride = null) { var provider = providerOverride ?? DefaultProvider; if (!provider.HasValue(key)) return; // 值未设置,无需重置 provider.RemoveFromCache(key); provider.Save(); Changed?.Invoke(defaultValue, provider); } public T? Get(IConfigProvider? providerOverride = null) => (providerOverride ?? DefaultProvider).Read(key, defaultValue, encrypted); public bool HasValue(IConfigProvider? providerOverride = null) => (providerOverride ?? DefaultProvider).HasValue(key); public void Save(IConfigProvider? providerOverride = null) => (providerOverride ?? DefaultProvider).Save(); public static implicit operator T?(ConfigEntry entry) => entry.Get(); }