using System.IO; using Newtonsoft.Json; using Prism.Mvvm; using Store.Management.Framework.Configs; namespace Store.Management.Framework.Services; public interface ISettingsServiceBase where T : class { T Model { get; } string FilePath { get; } T GetDefaultModel(); void LoadSettings(); Task LoadSettingsAsync(); void SaveSettings(); Task SaveSettingsAsync(); } /// /// JSON 配置服务。缺失文件保留当前配置,加载失败保留旧模型并抛出异常。 /// 保存通过同目录临时文件替换目标;模型变更应在所属 UI 线程调用。 /// public abstract class JsonSettingsServiceBase : BindableBase, ISettingsServiceBase where T : class { private readonly SemaphoreSlim _gate = new(1, 1); private T? _model; /// 默认按模型类型名保存到用户配置目录,派生类可覆盖文件名或完整路径。 public virtual string FilePath => FolderConfig.GetSettingsFilePath(typeof(T).Name + ".json"); public abstract T GetDefaultModel(); public T Model { get => _model ??= GetDefaultModel() ?? throw new InvalidOperationException("默认配置不能为空。"); protected set { ArgumentNullException.ThrowIfNull(value); SetProperty(ref _model, value); } } public void LoadSettings() { T? loaded = LoadModelAsync().GetAwaiter().GetResult(); if (loaded is not null) Model = loaded; } public void SaveSettings() => SaveSettingsAsync().GetAwaiter().GetResult(); public async Task LoadSettingsAsync() { T? loaded = await LoadModelAsync(); if (loaded is not null) Model = loaded; } private async Task LoadModelAsync() { T? loaded = null; await _gate.WaitAsync().ConfigureAwait(false); try { string path = GetFullPath(); try { string json = await File.ReadAllTextAsync(path).ConfigureAwait(false); loaded = JsonConvert.DeserializeObject(json) ?? throw new JsonSerializationException($"配置文件不能为 null:{path}"); } catch (FileNotFoundException) { } catch (DirectoryNotFoundException) { } } finally { _gate.Release(); } return loaded; } public async Task SaveSettingsAsync() { // 在首次等待前读取模型,避免在后台线程初始化默认模型。 string json = JsonConvert.SerializeObject(Model, Formatting.Indented); await _gate.WaitAsync().ConfigureAwait(false); string? temporaryPath = null; try { string path = GetFullPath(); Directory.CreateDirectory(Path.GetDirectoryName(path)!); temporaryPath = path + "." + Guid.NewGuid().ToString("N") + ".tmp"; await File.WriteAllTextAsync(temporaryPath, json).ConfigureAwait(false); File.Move(temporaryPath, path, overwrite: true); } finally { try { if (temporaryPath is not null && File.Exists(temporaryPath)) File.Delete(temporaryPath); } finally { _gate.Release(); } } } private string GetFullPath() { ArgumentException.ThrowIfNullOrWhiteSpace(FilePath); return Path.GetFullPath(FilePath); } }