From 6c2e89bc4f1f4746210654aefb3b15a5fb634494 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Thu, 29 Jan 2026 20:57:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(data):=20=E6=B7=BB=E5=8A=A0=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E8=AE=BE=E7=BD=AE=E4=BB=93=E5=BA=93=E5=92=8C=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E7=B1=BB=E5=9E=8B=E5=BA=8F=E5=88=97=E5=8C=96?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 UnifiedSettingsRepository 类实现单一文件存储所有设置数据 - 扩展 JsonSerializer 实现 IRuntimeTypeSerializer 接口支持运行时类型序列化 - 修改 DataRepositoryOptions 默认键名前缀为空字符串 - 实现异步加载、保存、删除和检查数据功能 - 添加数据事件通知机制支持 - 实现线程安全的数据缓存和文件操作 - [release ci] --- .../data/DataRepositoryOptions.cs | 2 +- .../data/UnifiedSettingsRepository.cs | 239 ++++++++++++++++++ GFramework.Game/serializer/JsonSerializer.cs | 38 ++- 3 files changed, 268 insertions(+), 11 deletions(-) create mode 100644 GFramework.Game/data/UnifiedSettingsRepository.cs diff --git a/GFramework.Game.Abstractions/data/DataRepositoryOptions.cs b/GFramework.Game.Abstractions/data/DataRepositoryOptions.cs index f8134e2..b12cd48 100644 --- a/GFramework.Game.Abstractions/data/DataRepositoryOptions.cs +++ b/GFramework.Game.Abstractions/data/DataRepositoryOptions.cs @@ -26,7 +26,7 @@ public class DataRepositoryOptions /// /// 键名前缀(如 "Game",生成的键为 "Game_SettingsData") /// - public string KeyPrefix { get; set; } = "Data"; + public string KeyPrefix { get; set; } = ""; /// /// 是否在保存时自动备份 diff --git a/GFramework.Game/data/UnifiedSettingsRepository.cs b/GFramework.Game/data/UnifiedSettingsRepository.cs new file mode 100644 index 0000000..2170c32 --- /dev/null +++ b/GFramework.Game/data/UnifiedSettingsRepository.cs @@ -0,0 +1,239 @@ +// Copyright (c) 2026 GeWuYou +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using GFramework.Core.Abstractions.serializer; +using GFramework.Core.Abstractions.storage; +using GFramework.Core.extensions; +using GFramework.Core.utility; +using GFramework.Game.Abstractions.data; +using GFramework.Game.Abstractions.data.events; + +namespace GFramework.Game.data; + +/// +/// 使用单一文件存储所有设置数据的仓库实现 +/// +public sealed class UnifiedSettingsRepository( + IStorage? storage, + IRuntimeTypeSerializer? serializer, + DataRepositoryOptions? options = null, + string fileName = "settings.json") + : AbstractContextUtility, IDataRepository +{ + private readonly Dictionary _cache = new(); + private readonly SemaphoreSlim _lock = new(1, 1); + private readonly DataRepositoryOptions _options = options ?? new DataRepositoryOptions(); + private bool _loaded; + private IRuntimeTypeSerializer? _serializer = serializer; + private IStorage? _storage = storage; + + private IStorage Storage => + _storage ?? throw new InvalidOperationException("IStorage not initialized."); + + private IRuntimeTypeSerializer Serializer => + _serializer ?? throw new InvalidOperationException("ISerializer not initialized."); + + // ========================= + // IDataRepository + // ========================= + + /// + /// 异步加载指定类型的数据 + /// + /// 要加载的数据类型,必须继承自IData接口并具有无参构造函数 + /// 加载的数据实例 + public async Task LoadAsync() where T : class, IData, new() + { + await EnsureLoadedAsync(); + + var key = GetTypeKey(typeof(T)); + + var result = _cache.TryGetValue(key, out var json) ? Serializer.Deserialize(json) : new T(); + + if (_options.EnableEvents) + this.SendEvent(new DataLoadedEvent(result)); + + return result; + } + + /// + /// 异步加载指定类型的数据(通过Type参数) + /// + /// 要加载的数据类型 + /// 加载的数据实例 + /// 当类型不符合要求时抛出异常 + public async Task LoadAsync(Type type) + { + if (!typeof(IData).IsAssignableFrom(type)) + throw new ArgumentException($"{type.Name} does not implement IData"); + + if (!type.IsClass || type.GetConstructor(Type.EmptyTypes) == null) + throw new ArgumentException($"{type.Name} must have parameterless ctor"); + + await EnsureLoadedAsync(); + + var key = GetTypeKey(type); + + IData result; + if (_cache.TryGetValue(key, out var json)) + { + result = (IData)Serializer.Deserialize(json, type); + } + else + { + result = (IData)Activator.CreateInstance(type)!; + } + + if (_options.EnableEvents) + this.SendEvent(new DataLoadedEvent(result)); + + return result; + } + + /// + /// 异步保存数据到存储 + /// + /// 要保存的数据类型 + /// 要保存的数据实例 + public async Task SaveAsync(T data) where T : class, IData + { + await EnsureLoadedAsync(); + + var key = GetTypeKey(typeof(T)); + _cache[key] = Serializer.Serialize(data); + + await SaveUnifiedFileAsync(); + + if (_options.EnableEvents) + this.SendEvent(new DataSavedEvent(data)); + } + + /// + /// 异步批量保存多个数据实例 + /// + /// 要保存的数据实例集合 + public async Task SaveAllAsync(IEnumerable dataList) + { + await EnsureLoadedAsync(); + + var list = dataList.ToList(); + foreach (var data in list) + { + var key = GetTypeKey(data.GetType()); + _cache[key] = Serializer.Serialize(data); + } + + await SaveUnifiedFileAsync(); + + if (_options.EnableEvents) + this.SendEvent(new DataBatchSavedEvent(list)); + } + + /// + /// 检查指定类型的数据是否存在 + /// + /// 要检查的数据类型 + /// 如果存在返回true,否则返回false + public async Task ExistsAsync() where T : class, IData + { + await EnsureLoadedAsync(); + return _cache.ContainsKey(GetTypeKey(typeof(T))); + } + + /// + /// 删除指定类型的数据 + /// + /// 要删除的数据类型 + public async Task DeleteAsync() where T : class, IData + { + await EnsureLoadedAsync(); + + _cache.Remove(GetTypeKey(typeof(T))); + await SaveUnifiedFileAsync(); + + if (_options.EnableEvents) + this.SendEvent(new DataDeletedEvent(typeof(T))); + } + + protected override void OnInit() + { + _storage ??= this.GetUtility()!; + _serializer ??= this.GetUtility()!; + } + + // ========================= + // Internals + // ========================= + + /// + /// 确保数据已从存储中加载到缓存 + /// + private async Task EnsureLoadedAsync() + { + if (_loaded) return; + + await _lock.WaitAsync(); + try + { + if (_loaded) return; + + if (await Storage.ExistsAsync(GetUnifiedKey())) + { + var data = await Storage.ReadAsync>(GetUnifiedKey()); + _cache.Clear(); + foreach (var (k, v) in data) + _cache[k] = v; + } + + _loaded = true; + } + finally + { + _lock.Release(); + } + } + + /// + /// 将缓存中的所有数据保存到统一文件 + /// + private async Task SaveUnifiedFileAsync() + { + await _lock.WaitAsync(); + try + { + await Storage.WriteAsync(GetUnifiedKey(), _cache); + } + finally + { + _lock.Release(); + } + } + + /// + /// 获取统一文件的存储键名 + /// + /// 完整的存储键名 + private string GetUnifiedKey() + { + var name = string.IsNullOrEmpty(_options.KeyPrefix) ? fileName : $"{_options.KeyPrefix}_{fileName}"; + return string.IsNullOrEmpty(_options.BasePath) ? name : $"{_options.BasePath.TrimEnd('/')}/{name}"; + } + + /// + /// 获取类型的唯一标识键 + /// + /// 要获取键的类型 + /// 类型的全名作为键 + private static string GetTypeKey(Type type) + => type.FullName!; // ⚠️ 刻意不用 AssemblyQualifiedName +} \ No newline at end of file diff --git a/GFramework.Game/serializer/JsonSerializer.cs b/GFramework.Game/serializer/JsonSerializer.cs index 925ce31..7ded055 100644 --- a/GFramework.Game/serializer/JsonSerializer.cs +++ b/GFramework.Game/serializer/JsonSerializer.cs @@ -6,28 +6,46 @@ namespace GFramework.Game.serializer; /// /// JSON序列化器实现类,用于将对象序列化为JSON字符串或将JSON字符串反序列化为对象 /// -public sealed class JsonSerializer : ISerializer +public sealed class JsonSerializer + : IRuntimeTypeSerializer { /// - /// 将指定的对象序列化为JSON字符串 + /// 将指定类型的对象序列化为JSON字符串 /// /// 要序列化的对象类型 /// 要序列化的对象实例 /// 序列化后的JSON字符串 public string Serialize(T value) - { - return JsonConvert.SerializeObject(value); - } + => JsonConvert.SerializeObject(value); /// /// 将JSON字符串反序列化为指定类型的对象 /// - /// 要反序列化的对象类型 - /// 包含JSON数据的字符串 + /// 要反序列化的目标类型 + /// 要反序列化的JSON字符串数据 /// 反序列化后的对象实例 /// 当无法反序列化数据时抛出 public T Deserialize(string data) - { - return JsonConvert.DeserializeObject(data) ?? throw new ArgumentException("Cannot deserialize data"); - } + => JsonConvert.DeserializeObject(data) + ?? throw new ArgumentException("Cannot deserialize data"); + + /// + /// 将对象序列化为JSON字符串(使用运行时类型) + /// + /// 要序列化的对象实例 + /// 对象的运行时类型 + /// 序列化后的JSON字符串 + public string Serialize(object obj, Type type) + => JsonConvert.SerializeObject(obj, type, null); + + /// + /// 将JSON字符串反序列化为指定类型的对象(使用运行时类型) + /// + /// 要反序列化的JSON字符串数据 + /// 反序列化目标类型 + /// 反序列化后的对象实例 + /// 当无法反序列化到指定类型时抛出 + public object Deserialize(string data, Type type) + => JsonConvert.DeserializeObject(data, type) + ?? throw new ArgumentException($"Cannot deserialize to {type.Name}"); } \ No newline at end of file