using System; using System.Collections.Generic; using System.IO; using System.Globalization; using System.Text; using UnityEngine; using LitJson; using GeneralTools; /// /// 数据持久化服务实现 /// 使用JSON格式存储应用数据,INI格式存储系统配置 /// public class DataPersistenceService : IDataPersistenceService { private readonly string _settingsPath; private readonly string _userPrefsPath; private readonly string _appConfigPath; private readonly string _configIniPath; private readonly string _streamingConfigIniPath; public DataPersistenceService() { // 设置存储路径 var dataPath = Application.persistentDataPath; _settingsPath = Path.Combine(dataPath, "settings.json"); _userPrefsPath = Path.Combine(dataPath, "UserPrefs"); _appConfigPath = Path.Combine(dataPath, "appconfig.json"); _configIniPath = Path.Combine(dataPath, "config.ini"); _streamingConfigIniPath = Path.Combine(Application.streamingAssetsPath, "config.ini"); // 确保目录存在 EnsureDirectoryExists(_userPrefsPath); TryCopyIniToPersistent(); } public bool SaveSettings(SystemSettings settings) { bool jsonSaved = false; bool iniSaved = false; try { var json = JsonMapper.ToJson(settings); jsonSaved = WriteTextAtomically(_settingsPath, json); } catch (Exception ex) { Debug.LogError($"保存JSON设置失败: {ex.Message}"); } iniSaved = SaveToIniFile(settings); if (jsonSaved || iniSaved) { Debug.Log($"设置已保存 - JSON: {jsonSaved}, INI: {iniSaved}"); return true; } Debug.LogError("保存设置失败:JSON和INI都未成功写入"); return false; } public SystemSettings LoadSettings() { if (TryLoadJsonSettings(out var settings, out var jsonSourcePath)) { ApplyNewestIniOverrides(settings, jsonSourcePath); return settings; } if (TryLoadIniSettings(_configIniPath, "持久化INI配置", out settings)) { return settings; } if (TryLoadIniSettings(_configIniPath + ".bak", "持久化INI备份配置", out settings)) { return settings; } if (File.Exists(_streamingConfigIniPath)) { settings = LoadFromIniFile(_streamingConfigIniPath); Debug.Log($"从默认INI配置加载设置成功: {_streamingConfigIniPath}"); return settings; } Debug.Log("使用默认设置"); return new SystemSettings(); } public bool SaveUserPreferences(string userId, UserPreferences preferences) { try { var filePath = Path.Combine(_userPrefsPath, $"{userId}_prefs.json"); var json = JsonMapper.ToJson(preferences); File.WriteAllText(filePath, json); Debug.Log($"用户偏好已保存: {userId}"); return true; } catch (Exception ex) { Debug.LogError($"保存用户偏好失败: {ex.Message}"); return false; } } public UserPreferences LoadUserPreferences(string userId) { try { var filePath = Path.Combine(_userPrefsPath, $"{userId}_prefs.json"); if (File.Exists(filePath)) { var json = File.ReadAllText(filePath); return JsonMapper.ToObject(json); } } catch (Exception ex) { Debug.LogError($"加载用户偏好失败: {ex.Message}"); } return new UserPreferences { userId = userId }; } public bool SaveAppConfig(AppConfig config) { try { var json = JsonMapper.ToJson(config); File.WriteAllText(_appConfigPath, json); return true; } catch (Exception ex) { Debug.LogError($"保存应用配置失败: {ex.Message}"); return false; } } public AppConfig LoadAppConfig() { try { if (File.Exists(_appConfigPath)) { var json = File.ReadAllText(_appConfigPath); return JsonMapper.ToObject(json); } } catch (Exception ex) { Debug.LogError($"加载应用配置失败: {ex.Message}"); } return new AppConfig(); } public bool ExportData(string filePath, ExportDataType dataType) { try { var exportData = new { ExportTime = DateTime.Now, DataType = dataType.ToString(), Data = GetExportData(dataType) }; var json = JsonMapper.ToJson(exportData); File.WriteAllText(filePath, json); Debug.Log($"数据导出成功: {filePath}"); return true; } catch (Exception ex) { Debug.LogError($"导出数据失败: {ex.Message}"); return false; } } public void ClearAllData() { try { if (File.Exists(_settingsPath)) File.Delete(_settingsPath); if (File.Exists(_appConfigPath)) File.Delete(_appConfigPath); if (Directory.Exists(_userPrefsPath)) Directory.Delete(_userPrefsPath, true); Debug.Log("所有持久化数据已清除"); } catch (Exception ex) { Debug.LogError($"清除数据失败: {ex.Message}"); } } private bool SaveToIniFile(SystemSettings settings) { try { var sb = new StringBuilder(); AppendIniSection(sb, "Display", ("brightness", settings.brightness.ToString("F1", CultureInfo.InvariantCulture))); AppendIniSection(sb, "Audio", ("volume", settings.volume.ToString(CultureInfo.InvariantCulture)), ("muteDurationMinutes", settings.muteDurationMinutes.ToString(CultureInfo.InvariantCulture))); AppendIniSection(sb, "SerialPort", ("serialPortName", settings.serialPortName ?? string.Empty), ("serialBaudRate", settings.serialBaudRate.ToString(CultureInfo.InvariantCulture)), ("enableSerialCommunication", settings.enableSerialCommunication.ToString())); AppendIniSection(sb, "BFI", ("bfiLowThreshold", settings.bfiLowThreshold.ToString("F1", CultureInfo.InvariantCulture)), ("bfiHighThreshold", settings.bfiHighThreshold.ToString("F1", CultureInfo.InvariantCulture)), ("bfiAlarmPriority", settings.bfiAlarmPriority.ToString(CultureInfo.InvariantCulture)), ("enableBFIAlarm", settings.enableBFIAlarm.ToString())); AppendIniSection(sb, "Time", ("useCustomSystemTime", settings.useCustomSystemTime.ToString()), ("customTimeOffsetTicks", settings.customTimeOffsetTicks.ToString(CultureInfo.InvariantCulture)), ("systemTime", settings.systemTime == DateTime.MinValue ? string.Empty : settings.systemTime.ToString("o", CultureInfo.InvariantCulture))); AppendIniSection(sb, "Network", ("mode", settings.network.Mode.ToString()), ("ipv4", settings.network.IPv4 ?? string.Empty), ("mask", settings.network.Mask ?? string.Empty), ("gateway", settings.network.Gateway ?? string.Empty), ("dns1", settings.network.Dns1 ?? string.Empty), ("dns2", settings.network.Dns2 ?? string.Empty)); AppendIniSection(sb, "System", ("autoBackup", settings.autoBackup.ToString()), ("dataRetentionDays", settings.dataRetentionDays.ToString(CultureInfo.InvariantCulture))); bool saved = WriteTextAtomically(_configIniPath, sb.ToString()); if (saved) { IniFile.ClearCache(_configIniPath); } return saved; } catch (Exception ex) { Debug.LogError($"保存到INI文件失败: {ex.Message}"); return false; } } private SystemSettings LoadFromIniFile(string iniPath) { var settings = new SystemSettings(); try { IniFile.ClearCache(iniPath); // 显示设置,使用公开的方法 var brightness = IniFile.ReadIniData("Display", "brightness", "50", iniPath); if (float.TryParse(brightness, NumberStyles.Float, CultureInfo.InvariantCulture, out float b)) settings.brightness = b; // 音频设置 var volume = IniFile.ReadIniData("Audio", "volume", "50", iniPath); if (int.TryParse(volume, NumberStyles.Integer, CultureInfo.InvariantCulture, out int v)) settings.volume = v; var muteDuration = IniFile.ReadIniData("Audio", "muteDurationMinutes", "3", iniPath); if (int.TryParse(muteDuration, NumberStyles.Integer, CultureInfo.InvariantCulture, out int md)) settings.muteDurationMinutes = md; var serialPortName = IniFile.ReadIniData("SerialPort", "serialPortName", settings.serialPortName, iniPath); if (!string.IsNullOrWhiteSpace(serialPortName)) settings.serialPortName = serialPortName; var serialBaudRate = IniFile.ReadIniData("SerialPort", "serialBaudRate", settings.serialBaudRate.ToString(), iniPath); if (int.TryParse(serialBaudRate, NumberStyles.Integer, CultureInfo.InvariantCulture, out int sbr)) settings.serialBaudRate = sbr; var enableSerial = IniFile.ReadIniData("SerialPort", "enableSerialCommunication", settings.enableSerialCommunication.ToString(), iniPath); if (bool.TryParse(enableSerial, out bool esc)) settings.enableSerialCommunication = esc; var bfiLowThreshold = IniFile.ReadIniData("BFI", "bfiLowThreshold", settings.bfiLowThreshold.ToString(CultureInfo.InvariantCulture), iniPath); if (float.TryParse(bfiLowThreshold, NumberStyles.Float, CultureInfo.InvariantCulture, out float bfiLow)) settings.bfiLowThreshold = bfiLow; var bfiHighThreshold = IniFile.ReadIniData("BFI", "bfiHighThreshold", settings.bfiHighThreshold.ToString(CultureInfo.InvariantCulture), iniPath); if (float.TryParse(bfiHighThreshold, NumberStyles.Float, CultureInfo.InvariantCulture, out float bfiHigh)) settings.bfiHighThreshold = bfiHigh; var bfiAlarmPriority = IniFile.ReadIniData("BFI", "bfiAlarmPriority", settings.bfiAlarmPriority.ToString(), iniPath); if (int.TryParse(bfiAlarmPriority, NumberStyles.Integer, CultureInfo.InvariantCulture, out int priority)) settings.bfiAlarmPriority = priority; var enableBfiAlarm = IniFile.ReadIniData("BFI", "enableBFIAlarm", settings.enableBFIAlarm.ToString(), iniPath); if (bool.TryParse(enableBfiAlarm, out bool enableAlarm)) settings.enableBFIAlarm = enableAlarm; var useCustomSystemTime = IniFile.ReadIniData("Time", "useCustomSystemTime", settings.useCustomSystemTime.ToString(), iniPath); if (bool.TryParse(useCustomSystemTime, out bool customTime)) settings.useCustomSystemTime = customTime; var customTimeOffsetTicks = IniFile.ReadIniData("Time", "customTimeOffsetTicks", settings.customTimeOffsetTicks.ToString(), iniPath); if (long.TryParse(customTimeOffsetTicks, NumberStyles.Integer, CultureInfo.InvariantCulture, out long offsetTicks)) settings.customTimeOffsetTicks = offsetTicks; var systemTime = IniFile.ReadIniData("Time", "systemTime", "", iniPath); if (!string.IsNullOrWhiteSpace(systemTime) && DateTime.TryParse(systemTime, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime parsedTime)) { settings.systemTime = parsedTime; } // 网络设置 var networkMode = IniFile.ReadIniData("Network", "mode", "Dhcp", iniPath); if (Enum.TryParse(networkMode, out NetworkMode nm)) settings.network.Mode = nm; settings.network.IPv4 = IniFile.ReadIniData("Network", "ipv4", "", iniPath); settings.network.Mask = IniFile.ReadIniData("Network", "mask", "", iniPath); settings.network.Gateway = IniFile.ReadIniData("Network", "gateway", "", iniPath); settings.network.Dns1 = IniFile.ReadIniData("Network", "dns1", "", iniPath); settings.network.Dns2 = IniFile.ReadIniData("Network", "dns2", "", iniPath); // 系统设置 var autoBackup = IniFile.ReadIniData("System", "autoBackup", "true", iniPath); if (bool.TryParse(autoBackup, out bool ab)) settings.autoBackup = ab; var dataRetention = IniFile.ReadIniData("System", "dataRetentionDays", "30", iniPath); if (int.TryParse(dataRetention, NumberStyles.Integer, CultureInfo.InvariantCulture, out int dr)) settings.dataRetentionDays = dr; } catch (Exception ex) { Debug.LogError($"从INI文件加载设置失败 ({iniPath}): {ex.Message}"); } return settings; } private static void AppendIniSection(StringBuilder builder, string sectionName, params (string Key, string Value)[] values) { builder.Append('[').Append(sectionName).AppendLine("]"); foreach (var pair in values) { builder.Append(pair.Key).Append('=').AppendLine(pair.Value ?? string.Empty); } builder.AppendLine(); } private void ApplyIniOverrides(SystemSettings settings, string iniPath) { if (settings == null || string.IsNullOrWhiteSpace(iniPath) || !File.Exists(iniPath)) { return; } IniFile.ClearCache(iniPath); const string missing = "__DCX_SETTING_MISSING__"; string brightness = IniFile.ReadIniData("Display", "brightness", missing, iniPath); if (brightness != missing && float.TryParse(brightness, NumberStyles.Float, CultureInfo.InvariantCulture, out float b)) settings.brightness = b; string volume = IniFile.ReadIniData("Audio", "volume", missing, iniPath); if (volume != missing && int.TryParse(volume, out int v)) settings.volume = v; string muteDuration = IniFile.ReadIniData("Audio", "muteDurationMinutes", missing, iniPath); if (muteDuration != missing && int.TryParse(muteDuration, out int md)) settings.muteDurationMinutes = md; string serialPortName = IniFile.ReadIniData("SerialPort", "serialPortName", missing, iniPath); if (serialPortName != missing && !string.IsNullOrWhiteSpace(serialPortName)) settings.serialPortName = serialPortName; string serialBaudRate = IniFile.ReadIniData("SerialPort", "serialBaudRate", missing, iniPath); if (serialBaudRate != missing && int.TryParse(serialBaudRate, out int sbr)) settings.serialBaudRate = sbr; string enableSerial = IniFile.ReadIniData("SerialPort", "enableSerialCommunication", missing, iniPath); if (enableSerial != missing && bool.TryParse(enableSerial, out bool esc)) settings.enableSerialCommunication = esc; string bfiLowThreshold = IniFile.ReadIniData("BFI", "bfiLowThreshold", missing, iniPath); if (bfiLowThreshold != missing && float.TryParse(bfiLowThreshold, NumberStyles.Float, CultureInfo.InvariantCulture, out float bfiLow)) settings.bfiLowThreshold = bfiLow; string bfiHighThreshold = IniFile.ReadIniData("BFI", "bfiHighThreshold", missing, iniPath); if (bfiHighThreshold != missing && float.TryParse(bfiHighThreshold, NumberStyles.Float, CultureInfo.InvariantCulture, out float bfiHigh)) settings.bfiHighThreshold = bfiHigh; string bfiAlarmPriority = IniFile.ReadIniData("BFI", "bfiAlarmPriority", missing, iniPath); if (bfiAlarmPriority != missing && int.TryParse(bfiAlarmPriority, out int priority)) settings.bfiAlarmPriority = priority; string enableBfiAlarm = IniFile.ReadIniData("BFI", "enableBFIAlarm", missing, iniPath); if (enableBfiAlarm != missing && bool.TryParse(enableBfiAlarm, out bool enableAlarm)) settings.enableBFIAlarm = enableAlarm; string useCustomSystemTime = IniFile.ReadIniData("Time", "useCustomSystemTime", missing, iniPath); if (useCustomSystemTime != missing && bool.TryParse(useCustomSystemTime, out bool customTime)) settings.useCustomSystemTime = customTime; string customTimeOffsetTicks = IniFile.ReadIniData("Time", "customTimeOffsetTicks", missing, iniPath); if (customTimeOffsetTicks != missing && long.TryParse(customTimeOffsetTicks, out long offsetTicks)) settings.customTimeOffsetTicks = offsetTicks; string systemTime = IniFile.ReadIniData("Time", "systemTime", missing, iniPath); if (systemTime != missing && !string.IsNullOrWhiteSpace(systemTime) && DateTime.TryParse(systemTime, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime parsedTime)) { settings.systemTime = parsedTime; } string networkMode = IniFile.ReadIniData("Network", "mode", missing, iniPath); if (networkMode != missing && Enum.TryParse(networkMode, out NetworkMode nm)) settings.network.Mode = nm; string ipv4 = IniFile.ReadIniData("Network", "ipv4", missing, iniPath); if (ipv4 != missing) settings.network.IPv4 = ipv4; string mask = IniFile.ReadIniData("Network", "mask", missing, iniPath); if (mask != missing) settings.network.Mask = mask; string gateway = IniFile.ReadIniData("Network", "gateway", missing, iniPath); if (gateway != missing) settings.network.Gateway = gateway; string dns1 = IniFile.ReadIniData("Network", "dns1", missing, iniPath); if (dns1 != missing) settings.network.Dns1 = dns1; string dns2 = IniFile.ReadIniData("Network", "dns2", missing, iniPath); if (dns2 != missing) settings.network.Dns2 = dns2; string autoBackup = IniFile.ReadIniData("System", "autoBackup", missing, iniPath); if (autoBackup != missing && bool.TryParse(autoBackup, out bool ab)) settings.autoBackup = ab; string dataRetention = IniFile.ReadIniData("System", "dataRetentionDays", missing, iniPath); if (dataRetention != missing && int.TryParse(dataRetention, out int dr)) settings.dataRetentionDays = dr; } private bool WriteTextAtomically(string filePath, string content) { var tempPath = string.IsNullOrWhiteSpace(filePath) ? null : filePath + ".tmp"; try { var directory = Path.GetDirectoryName(filePath); if (!string.IsNullOrWhiteSpace(directory)) { Directory.CreateDirectory(directory); } var backupPath = filePath + ".bak"; byte[] bytes = new UTF8Encoding(false).GetBytes(content ?? string.Empty); DeleteFileIfExists(tempPath); WriteBytesAndFlush(tempPath, bytes); if (File.Exists(filePath)) { try { CopyFileSynced(filePath, backupPath); } catch (Exception ex) { Debug.LogWarning($"备份旧配置文件失败,将继续写入新配置: {filePath}, {ex.Message}"); } DeleteFileIfExists(filePath); } File.Move(tempPath, filePath); var fileInfo = new FileInfo(filePath); if (!fileInfo.Exists || fileInfo.Length != bytes.Length) { throw new IOException($"写入校验失败,期望 {bytes.Length} 字节,实际 {(fileInfo.Exists ? fileInfo.Length : 0)} 字节"); } return true; } catch (Exception ex) { Debug.LogError($"原子写入失败: {filePath}, {ex.Message}"); return false; } finally { if (!string.IsNullOrWhiteSpace(tempPath)) { DeleteFileIfExists(tempPath); } } } private void TryCopyIniToPersistent() { try { if (File.Exists(_configIniPath) || File.Exists(_settingsPath)) { return; } if (File.Exists(_streamingConfigIniPath)) { File.Copy(_streamingConfigIniPath, _configIniPath, true); } } catch (Exception ex) { Debug.LogWarning($"复制默认INI到持久化目录失败: {ex.Message}"); } } private bool TryLoadJsonSettings(out SystemSettings settings, out string sourcePath) { settings = null; sourcePath = null; string[] candidates = { _settingsPath, _settingsPath + ".bak", _settingsPath + ".tmp" }; foreach (string candidate in GetExistingPathsNewestFirst(candidates)) { if (TryLoadJsonSettings(candidate, out settings)) { sourcePath = candidate; return true; } } return false; } private IEnumerable GetExistingPathsNewestFirst(IEnumerable paths) { var existingPaths = new List(); foreach (string path in paths) { if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) { existingPaths.Add(path); } } existingPaths.Sort((left, right) => GetLastWriteTimeUtcSafe(right).CompareTo(GetLastWriteTimeUtcSafe(left))); return existingPaths; } private bool TryLoadJsonSettings(string path, out SystemSettings settings) { settings = null; if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) { return false; } try { var json = File.ReadAllText(path, Encoding.UTF8); if (string.IsNullOrWhiteSpace(json)) { throw new InvalidDataException("文件为空"); } settings = JsonMapper.ToObject(json); if (settings == null) { throw new InvalidDataException("反序列化结果为空"); } Debug.Log($"从JSON配置加载设置成功: {path}"); return true; } catch (Exception ex) { Debug.LogWarning($"从JSON配置加载设置失败: {path}, {ex.Message}"); settings = null; return false; } } private void ApplyNewestIniOverrides(SystemSettings settings, string jsonSourcePath) { string iniPath = GetNewestExistingPath(_configIniPath, _configIniPath + ".bak"); if (iniPath == null) { return; } if (GetLastWriteTimeUtcSafe(iniPath) >= GetLastWriteTimeUtcSafe(jsonSourcePath)) { ApplyIniOverrides(settings, iniPath); Debug.Log($"已合并INI覆盖项: {iniPath}"); } } private bool TryLoadIniSettings(string path, string sourceName, out SystemSettings settings) { settings = null; if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) { return false; } settings = LoadFromIniFile(path); Debug.Log($"从{sourceName}加载设置成功: {path}"); return settings != null; } private string GetNewestExistingPath(string firstPath, string secondPath) { bool firstExists = !string.IsNullOrWhiteSpace(firstPath) && File.Exists(firstPath); bool secondExists = !string.IsNullOrWhiteSpace(secondPath) && File.Exists(secondPath); if (firstExists && secondExists) { return GetLastWriteTimeUtcSafe(secondPath) > GetLastWriteTimeUtcSafe(firstPath) ? secondPath : firstPath; } if (firstExists) { return firstPath; } return secondExists ? secondPath : null; } private DateTime GetLastWriteTimeUtcSafe(string path) { try { return !string.IsNullOrWhiteSpace(path) && File.Exists(path) ? File.GetLastWriteTimeUtc(path) : DateTime.MinValue; } catch { return DateTime.MinValue; } } private void WriteBytesAndFlush(string filePath, byte[] bytes) { using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read, 4096)) { stream.Write(bytes, 0, bytes.Length); FlushFileStream(stream); } } private void CopyFileSynced(string sourcePath, string targetPath) { if (string.IsNullOrWhiteSpace(sourcePath) || !File.Exists(sourcePath)) { return; } byte[] bytes = File.ReadAllBytes(sourcePath); WriteBytesAndFlush(targetPath, bytes); } private void FlushFileStream(FileStream stream) { try { stream.Flush(true); } catch { stream.Flush(); } } private void DeleteFileIfExists(string path) { try { if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) { File.Delete(path); } } catch (Exception ex) { Debug.LogWarning($"删除文件失败: {path}, {ex.Message}"); } } private object GetExportData(ExportDataType dataType) { switch (dataType) { case ExportDataType.Settings: return LoadSettings(); case ExportDataType.UserData: // 导出所有用户数据 return "用户数据导出功能待实现"; case ExportDataType.AlarmRecords: // 导出报警记录 var records = DCSAlarmManager.Instance?.GetAlarmRecords(1000); return records != null ? records : "无报警记录"; case ExportDataType.PatientInfo: // 导出病人信息 var patient = ServiceLocator.Get()?.GetCurrentPatient(); return patient != null ? patient : "无病人信息"; case ExportDataType.SystemLogs: return "系统日志导出功能待实现"; case ExportDataType.All: return new { Settings = LoadSettings(), AppConfig = LoadAppConfig(), SystemInfo = new { UnityVersion = Application.unityVersion, Platform = Application.platform.ToString(), SystemTime = DateTime.Now } }; default: return null; } } private void EnsureDirectoryExists(string path) { if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } } }