wanshenmean
2026-03-13 0b3d9467bfd081f607c7b17642e6c017aa32678d
feat: implement file-based persistence service

- Add IPersistenceService interface for config and data persistence
- Add FilePersistenceService saving to Data/instance-{id}/ directory
- Add InstanceDataModel for JSON serialization
- Support config and memory data save/load operations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
已添加3个文件
402 ■■■■■ 文件已修改
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Interfaces/IPersistenceService.cs 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Persistence/FilePersistenceService.cs 280 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Persistence/Models/InstanceDataModel.cs 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Interfaces/IPersistenceService.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using WIDESEAWCS_S7Simulator.Core.Entities;
using WIDESEAWCS_S7Simulator.Core.Interfaces;
namespace WIDESEAWCS_S7Simulator.Core.Persistence
{
    /// <summary>
    /// æŒä¹…化服务接口
    /// æä¾›å®žä¾‹é…ç½®å’Œå†…存数据的持久化功能
    /// </summary>
    public interface IPersistenceService
    {
        /// <summary>
        /// ä¿å­˜å®žä¾‹é…ç½®
        /// </summary>
        /// <param name="config">实例配置</param>
        Task SaveInstanceConfigAsync(InstanceConfig config);
        /// <summary>
        /// åŠ è½½å®žä¾‹é…ç½®
        /// </summary>
        /// <param name="instanceId">实例ID</param>
        /// <returns>实例配置</returns>
        Task<InstanceConfig> LoadInstanceConfigAsync(string instanceId);
        /// <summary>
        /// åŠ è½½æ‰€æœ‰å®žä¾‹é…ç½®
        /// </summary>
        /// <returns>实例配置列表</returns>
        Task<List<InstanceConfig>> LoadAllInstanceConfigsAsync();
        /// <summary>
        /// åˆ é™¤å®žä¾‹é…ç½®
        /// </summary>
        /// <param name="instanceId">实例ID</param>
        Task DeleteInstanceConfigAsync(string instanceId);
        /// <summary>
        /// ä¿å­˜å†…存数据
        /// </summary>
        /// <param name="instanceId">实例ID</param>
        /// <param name="memoryStore">内存存储</param>
        Task SaveMemoryDataAsync(string instanceId, IMemoryStore memoryStore);
        /// <summary>
        /// åŠ è½½å†…å­˜æ•°æ®
        /// </summary>
        /// <param name="instanceId">实例ID</param>
        /// <param name="memoryStore">内存存储</param>
        Task LoadMemoryDataAsync(string instanceId, IMemoryStore memoryStore);
    }
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Persistence/FilePersistenceService.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,280 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using WIDESEAWCS_S7Simulator.Core.Entities;
using WIDESEAWCS_S7Simulator.Core.Enums;
using WIDESEAWCS_S7Simulator.Core.Interfaces;
using WIDESEAWCS_S7Simulator.Core.Persistence.Models;
namespace WIDESEAWCS_S7Simulator.Core.Persistence
{
    /// <summary>
    /// æ–‡ä»¶æŒä¹…化服务实现
    /// å°†å®žä¾‹é…ç½®å’Œå†…存数据保存到本地JSON文件
    /// </summary>
    public class FilePersistenceService : IPersistenceService
    {
        /// <summary>
        /// æ•°æ®ç›®å½•路径
        /// </summary>
        private readonly string _dataPath;
        /// <summary>
        /// JSON序列化选项
        /// </summary>
        private readonly JsonSerializerOptions _jsonOptions;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="dataPath">数据目录路径</param>
        public FilePersistenceService(string dataPath = "Data")
        {
            _dataPath = dataPath;
            _jsonOptions = new JsonSerializerOptions
            {
                WriteIndented = true,
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
            };
            // ç¡®ä¿æ•°æ®ç›®å½•存在
            if (!Directory.Exists(_dataPath))
            {
                Directory.CreateDirectory(_dataPath);
            }
        }
        /// <summary>
        /// ä¿å­˜å®žä¾‹é…ç½®
        /// </summary>
        public async Task SaveInstanceConfigAsync(InstanceConfig config)
        {
            if (config == null)
                throw new ArgumentNullException(nameof(config));
            var instanceDir = GetInstanceDirectory(config.Id);
            if (!Directory.Exists(instanceDir))
            {
                Directory.CreateDirectory(instanceDir);
            }
            var configPath = Path.Combine(instanceDir, "config.json");
            var model = ToDataModel(config);
            var json = JsonSerializer.Serialize(model, _jsonOptions);
            await File.WriteAllTextAsync(configPath, json);
        }
        /// <summary>
        /// åŠ è½½å®žä¾‹é…ç½®
        /// </summary>
        public async Task<InstanceConfig> LoadInstanceConfigAsync(string instanceId)
        {
            if (string.IsNullOrWhiteSpace(instanceId))
                throw new ArgumentException("实例ID不能为空", nameof(instanceId));
            var configPath = Path.Combine(GetInstanceDirectory(instanceId), "config.json");
            if (!File.Exists(configPath))
                throw new FileNotFoundException($"实例配置文件不存在: {configPath}");
            var json = await File.ReadAllTextAsync(configPath);
            var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions);
            if (model == null)
                throw new InvalidOperationException("无法反序列化实例配置");
            return ToEntity(model);
        }
        /// <summary>
        /// åŠ è½½æ‰€æœ‰å®žä¾‹é…ç½®
        /// </summary>
        public async Task<List<InstanceConfig>> LoadAllInstanceConfigsAsync()
        {
            var configs = new List<InstanceConfig>();
            if (!Directory.Exists(_dataPath))
                return configs;
            var instanceDirs = Directory.GetDirectories(_dataPath)
                .Where(d => Path.GetFileName(d).StartsWith("instance-"))
                .ToList();
            foreach (var dir in instanceDirs)
            {
                var configPath = Path.Combine(dir, "config.json");
                if (File.Exists(configPath))
                {
                    try
                    {
                        var json = await File.ReadAllTextAsync(configPath);
                        var model = JsonSerializer.Deserialize<InstanceDataModel>(json, _jsonOptions);
                        if (model != null)
                        {
                            configs.Add(ToEntity(model));
                        }
                    }
                    catch (Exception)
                    {
                        // è·³è¿‡æ— æ³•加载的配置文件
                        continue;
                    }
                }
            }
            return configs;
        }
        /// <summary>
        /// åˆ é™¤å®žä¾‹é…ç½®
        /// </summary>
        public Task DeleteInstanceConfigAsync(string instanceId)
        {
            if (string.IsNullOrWhiteSpace(instanceId))
                throw new ArgumentException("实例ID不能为空", nameof(instanceId));
            var instanceDir = GetInstanceDirectory(instanceId);
            if (Directory.Exists(instanceDir))
            {
                Directory.Delete(instanceDir, recursive: true);
            }
            return Task.CompletedTask;
        }
        /// <summary>
        /// ä¿å­˜å†…存数据
        /// </summary>
        public async Task SaveMemoryDataAsync(string instanceId, IMemoryStore memoryStore)
        {
            if (string.IsNullOrWhiteSpace(instanceId))
                throw new ArgumentException("实例ID不能为空", nameof(instanceId));
            if (memoryStore == null)
                throw new ArgumentNullException(nameof(memoryStore));
            var instanceDir = GetInstanceDirectory(instanceId);
            if (!Directory.Exists(instanceDir))
            {
                Directory.CreateDirectory(instanceDir);
            }
            var memoryPath = Path.Combine(instanceDir, "memory.json");
            var exportedData = memoryStore.Export();
            // å°†å­—节数组转换为Base64字符串以便JSON序列化
            var memoryDataModel = new MemoryDataModel
            {
                MemoryData = exportedData.ToDictionary(
                    kvp => kvp.Key,
                    kvp => Convert.ToBase64String(kvp.Value)
                )
            };
            var json = JsonSerializer.Serialize(memoryDataModel, _jsonOptions);
            await File.WriteAllTextAsync(memoryPath, json);
        }
        /// <summary>
        /// åŠ è½½å†…å­˜æ•°æ®
        /// </summary>
        public async Task LoadMemoryDataAsync(string instanceId, IMemoryStore memoryStore)
        {
            if (string.IsNullOrWhiteSpace(instanceId))
                throw new ArgumentException("实例ID不能为空", nameof(instanceId));
            if (memoryStore == null)
                throw new ArgumentNullException(nameof(memoryStore));
            var memoryPath = Path.Combine(GetInstanceDirectory(instanceId), "memory.json");
            if (!File.Exists(memoryPath))
                return; // å†…存文件不存在,跳过加载
            var json = await File.ReadAllTextAsync(memoryPath);
            var memoryDataModel = JsonSerializer.Deserialize<MemoryDataModel>(json, _jsonOptions);
            if (memoryDataModel?.MemoryData == null)
                return;
            // å°†Base64字符串转换回字节数组
            var importedData = new Dictionary<string, byte[]>();
            foreach (var kvp in memoryDataModel.MemoryData)
            {
                try
                {
                    importedData[kvp.Key] = Convert.FromBase64String(kvp.Value);
                }
                catch (FormatException)
                {
                    // è·³è¿‡æ— æ•ˆçš„Base64数据
                    continue;
                }
            }
            memoryStore.Import(importedData);
        }
        /// <summary>
        /// èŽ·å–å®žä¾‹ç›®å½•è·¯å¾„
        /// </summary>
        private string GetInstanceDirectory(string instanceId)
        {
            return Path.Combine(_dataPath, $"instance-{instanceId}");
        }
        /// <summary>
        /// å°†å®žä½“转换为数据模型
        /// </summary>
        private InstanceDataModel ToDataModel(InstanceConfig config)
        {
            return new InstanceDataModel
            {
                Id = config.Id,
                Name = config.Name,
                PlcType = config.PLCType.ToString(),
                Port = config.Port,
                ActivationKey = config.ActivationKey,
                AutoStart = config.AutoStart,
                MemoryConfig = new MemoryRegionConfigModel
                {
                    MRegionSize = config.MemoryConfig.MRegionSize,
                    DBBlockCount = config.MemoryConfig.DBBlockCount,
                    DBBlockSize = config.MemoryConfig.DBBlockSize,
                    IRegionSize = config.MemoryConfig.IRegionSize,
                    QRegionSize = config.MemoryConfig.QRegionSize,
                    TRegionCount = config.MemoryConfig.TRegionCount,
                    CRegionCount = config.MemoryConfig.CRegionCount
                }
            };
        }
        /// <summary>
        /// å°†æ•°æ®æ¨¡åž‹è½¬æ¢ä¸ºå®žä½“
        /// </summary>
        private InstanceConfig ToEntity(InstanceDataModel model)
        {
            return new InstanceConfig
            {
                Id = model.Id,
                Name = model.Name,
                PLCType = Enum.Parse<SiemensPLCType>(model.PlcType),
                Port = model.Port,
                ActivationKey = model.ActivationKey,
                AutoStart = model.AutoStart,
                MemoryConfig = new MemoryRegionConfig
                {
                    MRegionSize = model.MemoryConfig.MRegionSize,
                    DBBlockCount = model.MemoryConfig.DBBlockCount,
                    DBBlockSize = model.MemoryConfig.DBBlockSize,
                    IRegionSize = model.MemoryConfig.IRegionSize,
                    QRegionSize = model.MemoryConfig.QRegionSize,
                    TRegionCount = model.MemoryConfig.TRegionCount,
                    CRegionCount = model.MemoryConfig.CRegionCount
                }
            };
        }
    }
}
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Core/Persistence/Models/InstanceDataModel.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
namespace WIDESEAWCS_S7Simulator.Core.Persistence.Models
{
    /// <summary>
    /// å®žä¾‹æ•°æ®æ¨¡åž‹ï¼ˆç”¨äºŽJSON序列化)
    /// </summary>
    public class InstanceDataModel
    {
        /// <summary>
        /// å®žä¾‹ID
        /// </summary>
        public string Id { get; set; } = string.Empty;
        /// <summary>
        /// å®žä¾‹åç§°
        /// </summary>
        public string Name { get; set; } = string.Empty;
        /// <summary>
        /// PLC类型
        /// </summary>
        public string PlcType { get; set; } = string.Empty;
        /// <summary>
        /// ç›‘听端口
        /// </summary>
        public int Port { get; set; }
        /// <summary>
        /// æ¿€æ´»å¯†é’¥
        /// </summary>
        public string ActivationKey { get; set; } = string.Empty;
        /// <summary>
        /// è‡ªåŠ¨å¯åŠ¨
        /// </summary>
        public bool AutoStart { get; set; }
        /// <summary>
        /// å†…存区域配置
        /// </summary>
        public MemoryRegionConfigModel MemoryConfig { get; set; } = new();
    }
    /// <summary>
    /// å†…存区域配置模型
    /// </summary>
    public class MemoryRegionConfigModel
    {
        public int MRegionSize { get; set; } = 1024;
        public int DBBlockCount { get; set; } = 100;
        public int DBBlockSize { get; set; } = 1024;
        public int IRegionSize { get; set; } = 256;
        public int QRegionSize { get; set; } = 256;
        public int TRegionCount { get; set; } = 64;
        public int CRegionCount { get; set; } = 64;
    }
    /// <summary>
    /// å†…存数据模型
    /// </summary>
    public class MemoryDataModel
    {
        /// <summary>
        /// å†…存区域数据字典(区域类型 -> Base64编码的字节数组)
        /// </summary>
        public Dictionary<string, string> MemoryData { get; set; } = new();
    }
}