DCS/ruiyiweiUX/Assets/Scripts/Managers/BFIHistoryManager.cs

2394 lines
76 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEngine;
/// <summary>
/// BFI测试历史记录管理服务
/// 负责保存、加载和管理历史测试记录
/// </summary>
public class BFIHistoryManager : MonoBehaviour
{
private const int MIN_HISTORY_RECORD_LIMIT = 1000;
private const string HISTORY_FILE_NAME = "bfi_history.json";
private const string HISTORY_BACKUP_FILE_NAME = "bfi_history_backup.json";
private const string HISTORY_RECORDS_DIR_NAME = "bfi_history_records";
private const string CURRENT_RECORDING_JOURNAL_FILE_NAME = "bfi_current_recording.jsonl";
private const int HISTORY_INDEX_VERSION = 2;
private const string HISTORY_STORAGE_MODE_PER_RECORD = "PerRecordFiles";
private const string RECORD_BACKUP_EXTENSION = ".bak";
private static readonly string[] HISTORY_CANDIDATE_FILE_NAMES = { HISTORY_FILE_NAME, HISTORY_BACKUP_FILE_NAME };
private static BFIHistoryManager _instance;
public static BFIHistoryManager Instance
{
get
{
if (_instance == null)
{
var go = new GameObject("BFIHistoryManager");
_instance = go.AddComponent<BFIHistoryManager>();
DontDestroyOnLoad(go);
}
return _instance;
}
}
[Header("配置")]
[SerializeField] private int maxHistoryRecords = 1000; // 最大历史记录数
[SerializeField] private bool autoSave = true; // 自动保存
[SerializeField] private float saveInterval = 60f; // 保存间隔(秒)
private List<BFITestRecord> historyRecords;
private string historyFilePath;
private string historyMirrorFilePath;
private string historyRecordsDirectoryPath;
private float lastSaveTime;
private bool historyFileLoadFailed;
private bool androidStoragePermissionRequested;
private bool historyIndexDirty;
// 事件
public event System.Action<BFITestRecord> OnRecordAdded;
public event System.Action<BFITestRecord> OnRecordUpdated;
public event System.Action<string> OnRecordRemoved;
public event System.Action OnHistoryLoaded;
// public bool LastSaveSucceeded { get; private set; }
private void Awake()
{
if (_instance == null)
{
_instance = this;
DontDestroyOnLoad(gameObject);
Initialize();
}
else if (_instance != this)
{
Destroy(gameObject);
}
}
private void Initialize()
{
maxHistoryRecords = Mathf.Max(maxHistoryRecords, MIN_HISTORY_RECORD_LIMIT);
historyRecords = new List<BFITestRecord>();
// 历史索引是导出面板的数据源主文件使用Unity持久化目录和报警记录一致。
// historyMirrorFilePath仅用于清理旧版本曾写入Download的镜像文件。
historyFilePath = ResolveHistoryFilePath();
historyMirrorFilePath = ResolveHistoryMirrorFilePath();
historyRecordsDirectoryPath = GetRecordFilesDirectory(historyFilePath);
EnsureHistoryDirectoryExists();
LoadHistoryRecords(true, true);
Debug.Log($"BFI历史管理器已初始化历史文件路径: {historyFilePath}, 单条记录目录: {historyRecordsDirectoryPath}");
#if UNITY_ANDROID && !UNITY_EDITOR
AndroidFileLogger.Instance?.LogInfo("BFIHistory", $"历史文件路径: {historyFilePath}");
AndroidFileLogger.Instance?.LogInfo("BFIHistory", $"单条记录目录: {historyRecordsDirectoryPath}");
#endif
}
private string ResolveHistoryFilePath()
{
return Path.Combine(Application.persistentDataPath, HISTORY_FILE_NAME);
}
private string ResolveHistoryMirrorFilePath()
{
#if UNITY_ANDROID && !UNITY_EDITOR
return Path.Combine(GetAndroidPublicDownloadDirectory(), "DCX_Export", HISTORY_FILE_NAME);
#else
return null;
#endif
}
public string GetHistoryFilePath()
{
return historyFilePath;
}
public string GetHistoryMirrorFilePath()
{
return historyMirrorFilePath;
}
public string GetHistoryRecordsDirectoryPath()
{
return historyRecordsDirectoryPath;
}
#if UNITY_ANDROID && !UNITY_EDITOR
private string GetAndroidPublicDownloadDirectory()
{
try
{
using (var environment = new AndroidJavaClass("android.os.Environment"))
using (var downloadDir = environment.CallStatic<AndroidJavaObject>(
"getExternalStoragePublicDirectory",
environment.GetStatic<string>("DIRECTORY_DOWNLOADS")))
{
string path = downloadDir?.Call<string>("getAbsolutePath");
if (!string.IsNullOrEmpty(path))
{
return path;
}
}
}
catch (Exception ex)
{
Debug.LogWarning($"获取Android公共Download目录失败回退persistentDataPath: {ex.Message}");
}
return Application.persistentDataPath;
}
#endif
private void EnsureHistoryDirectoryExists()
{
EnsureHistoryDirectoryExists(historyFilePath, false);
EnsureDirectoryExists(historyRecordsDirectoryPath);
}
private bool EnsureDirectoryExists(string directory)
{
try
{
if (string.IsNullOrEmpty(directory))
{
return false;
}
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"创建目录失败: {directory}, {ex.Message}");
return false;
}
}
private bool EnsureHistoryDirectoryExists(string filePath, bool requestExternalPermission)
{
try
{
if (string.IsNullOrEmpty(filePath))
{
return false;
}
string dir = Path.GetDirectoryName(filePath);
#if UNITY_ANDROID && !UNITY_EDITOR
if (requestExternalPermission)
{
EnsureAndroidDownloadPermission(dir);
}
#endif
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"创建历史记录目录失败: {filePath}, {ex.Message}");
return false;
}
}
#if UNITY_ANDROID && !UNITY_EDITOR
private void EnsureAndroidDownloadPermission(string directory)
{
if (string.IsNullOrEmpty(directory) || !directory.Contains("/Download"))
{
return;
}
try
{
using (var buildVersion = new AndroidJavaClass("android.os.Build$VERSION"))
{
int sdkInt = buildVersion.GetStatic<int>("SDK_INT");
if (sdkInt >= 30 && !AndroidExportPermissionManager.HasManageExternalStoragePermission())
{
Debug.LogWarning("BFI历史索引镜像保存到Download需要管理外部存储权限主索引仍保存在应用内部目录");
if (!androidStoragePermissionRequested)
{
androidStoragePermissionRequested = true;
AndroidExportPermissionManager.RequestManageExternalStoragePermission();
}
}
else if (sdkInt <= 29 && !AndroidExportPermissionManager.HasWriteExternalStoragePermission())
{
Debug.LogWarning("BFI历史索引镜像保存到Download需要写入外部存储权限主索引仍保存在应用内部目录");
if (!androidStoragePermissionRequested)
{
androidStoragePermissionRequested = true;
AndroidExportPermissionManager.RequestWriteExternalStoragePermission();
}
}
}
}
catch (Exception ex)
{
Debug.LogWarning($"检查Download写入权限失败: {ex.Message}");
}
}
#endif
private void Update()
{
// 自动保存
if (autoSave && Time.time - lastSaveTime > saveInterval)
{
if (!historyIndexDirty || SaveHistoryIndex())
{
historyIndexDirty = false;
}
lastSaveTime = Time.time;
}
}
/// <summary>
/// 添加新的测试记录
/// </summary>
public bool AddTestRecord(BFITestRecord record)
{
if (record == null)
{
Debug.LogWarning("尝试添加空的测试记录");
return false;
}
try
{
if (historyRecords == null)
{
historyRecords = new List<BFITestRecord>();
}
NormalizeRecordForStorage(record);
// 检查是否已存在
var existingRecord = historyRecords.FirstOrDefault(r => r.TestId == record.TestId);
if (existingRecord != null)
{
if (!ShouldReplaceRecord(existingRecord, record))
{
bool existingPersisted = IsRecordPersisted(existingRecord.TestId, existingRecord.DataPoints?.Count ?? 0);
if (!existingPersisted)
{
existingPersisted = SaveHistoryRecordFile(existingRecord);
}
SaveHistoryIndex();
Debug.Log($"忽略较旧的BFI记录快照: {record.TestName}");
return existingPersisted;
}
}
// 关键原则:先写实体文件,写成功后才进入内存历史,避免界面显示“假记录”。
bool recordSaved = SaveHistoryRecordFile(record);
if (!recordSaved)
{
Debug.LogError($"BFI记录实体文件保存失败已拒绝加入内存历史: {record.TestName}");
return false;
}
if (existingRecord != null)
{
int index = historyRecords.IndexOf(existingRecord);
historyRecords[index] = record;
OnRecordUpdated?.Invoke(record);
Debug.Log($"更新测试记录: {record.TestName}");
}
else
{
// 添加新记录
historyRecords.Add(record);
OnRecordAdded?.Invoke(record);
Debug.Log($"添加新测试记录: {record.TestName}");
// 检查记录数量限制
if (historyRecords.Count > maxHistoryRecords)
{
RemoveOldestRecord();
}
}
// 刷新轻量索引。记录实体文件已经先保存,索引失败也不会丢失记录本体。
bool indexSaved = SaveHistoryIndex();
if (!indexSaved)
{
Debug.LogWarning($"BFI历史索引保存失败但记录实体文件已保存: {record.TestName}");
}
return recordSaved;
}
catch (Exception ex)
{
Debug.LogError($"添加测试记录失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 保存自动记录中的中间快照。实时逐点日志已经承担断电恢复职责,
/// 这里避免每次快照都重写索引和反读整条记录降低安卓板主线程IO压力。
/// </summary>
public bool AddTestRecordSnapshot(BFITestRecord record)
{
if (record == null)
{
return false;
}
try
{
if (historyRecords == null)
{
historyRecords = new List<BFITestRecord>();
}
NormalizeRecordForStorage(record);
var existingRecord = historyRecords.FirstOrDefault(r => r.TestId == record.TestId);
if (existingRecord != null && !ShouldReplaceRecord(existingRecord, record))
{
return true;
}
bool recordSaved = SaveHistoryRecordFile(record, verifyAfterWrite: false, verboseLog: false);
if (!recordSaved)
{
return false;
}
if (existingRecord != null)
{
int index = historyRecords.IndexOf(existingRecord);
historyRecords[index] = record;
OnRecordUpdated?.Invoke(record);
}
else
{
historyRecords.Add(record);
OnRecordAdded?.Invoke(record);
if (historyRecords.Count > maxHistoryRecords)
{
RemoveOldestRecord();
}
}
historyIndexDirty = true;
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"保存BFI记录快照失败: {record?.TestName}, {ex.Message}");
return false;
}
}
/// <summary>
/// 移除测试记录
/// </summary>
public bool RemoveTestRecord(string testId)
{
try
{
var record = historyRecords.FirstOrDefault(r => r.TestId == testId);
if (record != null)
{
historyRecords.Remove(record);
DeleteHistoryRecordFile(record);
OnRecordRemoved?.Invoke(testId);
SaveHistoryIndex();
Debug.Log($"移除测试记录: {record.TestName}");
return true;
}
return false;
}
catch (Exception ex)
{
Debug.LogError($"移除测试记录失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 获取所有历史记录
/// </summary>
public List<BFITestRecord> GetAllRecords()
{
return GetRecordsSortedByRecentFirst(historyRecords);
}
/// <summary>
/// 根据ID获取记录
/// </summary>
public BFITestRecord GetRecord(string testId)
{
return historyRecords.FirstOrDefault(r => r.TestId == testId);
}
public BFITestRecord GetFullRecord(string testId)
{
if (string.IsNullOrEmpty(testId))
{
return null;
}
var cachedRecord = GetRecord(testId);
if (cachedRecord != null && cachedRecord.DataPoints != null && cachedRecord.DataPoints.Count > 0)
{
return cachedRecord;
}
string defaultRecordFilePath = GetHistoryRecordFilePath(testId);
string recordFilePath = !string.IsNullOrEmpty(cachedRecord?.FilePath)
? cachedRecord.FilePath
: defaultRecordFilePath;
BFITestRecord bestRecord = cachedRecord;
if (TryReadHistoryRecord(recordFilePath, out var record))
{
bestRecord = ChooseRecordWithMoreData(bestRecord, record);
if (HasDataPoints(record))
{
AddOrReplaceRecoveredRecordToList(historyRecords, record);
return record;
}
}
if (!string.Equals(recordFilePath, defaultRecordFilePath, StringComparison.OrdinalIgnoreCase) &&
TryReadHistoryRecord(defaultRecordFilePath, out record))
{
bestRecord = ChooseRecordWithMoreData(bestRecord, record);
if (HasDataPoints(record))
{
AddOrReplaceRecoveredRecordToList(historyRecords, record);
return record;
}
}
if (TryFindBestHistoryRecordById(testId, out var recoveredRecord, out var recoveredPath))
{
recoveredRecord.FilePath = recoveredPath;
AddOrReplaceRecoveredRecordToList(historyRecords, recoveredRecord);
if (!string.Equals(recoveredPath, defaultRecordFilePath, StringComparison.OrdinalIgnoreCase))
{
SaveHistoryRecordFile(recoveredRecord, verifyAfterWrite: true, verboseLog: false);
}
Debug.Log($"已为BFI导出恢复完整记录: {recoveredRecord.TestName}, 点数: {recoveredRecord.DataPoints?.Count ?? 0}");
return recoveredRecord;
}
if (bestRecord != null)
{
AddOrReplaceRecoveredRecordToList(historyRecords, bestRecord);
}
Debug.LogWarning($"未找到完整BFI记录明细: ID={testId}, 文件={recordFilePath}");
return bestRecord;
}
/// <summary>
/// 根据日期范围获取记录
/// </summary>
public List<BFITestRecord> GetRecordsByDateRange(DateTime startDate, DateTime endDate)
{
return GetRecordsSortedByRecentFirst(historyRecords.Where(r => r.StartTime >= startDate && r.StartTime <= endDate));
}
/// <summary>
/// 根据患者ID获取记录
/// </summary>
public List<BFITestRecord> GetRecordsByPatient(string patientId)
{
return GetRecordsSortedByRecentFirst(historyRecords.Where(r => r.PatientId == patientId));
}
/// <summary>
/// 搜索记录
/// </summary>
public List<BFITestRecord> SearchRecords(string keyword)
{
if (string.IsNullOrEmpty(keyword))
return GetAllRecords();
keyword = keyword.ToLower();
return GetRecordsSortedByRecentFirst(historyRecords.Where(r =>
r.TestName.ToLower().Contains(keyword) ||
r.PatientName?.ToLower().Contains(keyword) == true ||
r.PatientId?.ToLower().Contains(keyword) == true
));
}
/// <summary>
/// 获取最近的记录
/// </summary>
public List<BFITestRecord> GetRecentRecords(int count = 10)
{
return GetRecordsSortedByRecentFirst(historyRecords).Take(count).ToList();
}
private List<BFITestRecord> GetRecordsSortedByRecentFirst(IEnumerable<BFITestRecord> records)
{
return (records ?? Enumerable.Empty<BFITestRecord>())
.Where(r => r != null)
.OrderByDescending(r => r.StartTime)
.ThenByDescending(r => r.EndTime)
.ToList();
}
/// <summary>
/// 清空历史记录
/// </summary>
public void ClearHistory()
{
try
{
historyRecords.Clear();
historyFileLoadFailed = false;
historyIndexDirty = false;
DeleteAllHistoryStorageFiles();
SaveHistoryRecords();
OnHistoryLoaded?.Invoke();
Debug.Log("历史记录已清空");
}
catch (Exception ex)
{
Debug.LogError($"清空历史记录失败: {ex.Message}");
}
}
/// <summary>
/// 保存历史记录到文件
/// </summary>
public bool SaveHistoryRecords()
{
try
{
if (historyFileLoadFailed && (historyRecords == null || historyRecords.Count == 0))
{
Debug.LogWarning("历史文件加载失败,跳过空历史保存,避免覆盖已有索引");
return false;
}
EnsureHistoryDirectoryExists();
bool allRecordsSaved = true;
if (historyRecords != null)
{
foreach (var record in historyRecords)
{
if (record == null)
{
continue;
}
NormalizeRecordForStorage(record);
int pointCount = record.DataPoints?.Count ?? 0;
if (!IsRecordPersisted(record.TestId, pointCount))
{
allRecordsSaved &= SaveHistoryRecordFile(record);
}
}
}
bool indexSaved = SaveHistoryIndex();
historyFileLoadFailed = false;
// LastSaveSucceeded = true;
Debug.Log($"历史记录已保存,共 {historyRecords?.Count ?? 0} 条记录,单条文件目录: {historyRecordsDirectoryPath}");
return allRecordsSaved && indexSaved;
}
catch (Exception ex)
{
// LastSaveSucceeded = false;
Debug.LogError($"保存历史记录失败: {ex.Message}");
return false;
}
}
private bool SaveHistoryIndex()
{
try
{
EnsureHistoryDirectoryExists();
var indexEntries = new List<BFIHistoryRecordIndexEntry>();
if (historyRecords != null)
{
foreach (var record in historyRecords.Where(r => r != null))
{
NormalizeRecordForStorage(record);
indexEntries.Add(CreateIndexEntry(record));
}
}
var wrapper = new BFIHistoryWrapper
{
Version = HISTORY_INDEX_VERSION,
StorageMode = HISTORY_STORAGE_MODE_PER_RECORD,
Records = new List<BFITestRecord>(),
RecordFiles = indexEntries
};
string json = JsonUtility.ToJson(wrapper, true);
WriteAllTextAtomic(historyFilePath, json);
historyIndexDirty = false;
return true;
}
catch (Exception ex)
{
Debug.LogError($"保存BFI历史索引失败: {ex.Message}");
return false;
}
}
private BFIHistoryRecordIndexEntry CreateIndexEntry(BFITestRecord record)
{
string defaultFilePath = GetHistoryRecordFilePath(record);
string filePath = string.IsNullOrEmpty(record.FilePath) ? defaultFilePath : record.FilePath;
string fileName = GetIndexEntryFileName(filePath, defaultFilePath);
int pointCount = record.DataPoints?.Count ?? 0;
bool summaryOnlyRecord = pointCount == 0 && (record.Statistics?.Count ?? 0) > 0;
if (summaryOnlyRecord)
{
pointCount = record.Statistics.Count;
}
return new BFIHistoryRecordIndexEntry
{
TestId = record.TestId,
TestName = record.TestName,
PatientId = record.PatientId,
PatientName = record.PatientName,
FileName = fileName,
StartTimeTicks = record.StartTimeTicks,
EndTimeTicks = record.EndTimeTicks,
PointCount = pointCount,
MinValue = record.Statistics?.MinValue ?? 0f,
MaxValue = record.Statistics?.MaxValue ?? 0f,
Average = record.Statistics?.Average ?? 0f
};
}
private string GetIndexEntryFileName(string filePath, string defaultFilePath)
{
try
{
if (string.IsNullOrEmpty(filePath))
{
return Path.GetFileName(defaultFilePath);
}
string directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory) &&
!string.IsNullOrEmpty(historyRecordsDirectoryPath) &&
string.Equals(
Path.GetFullPath(directory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
Path.GetFullPath(historyRecordsDirectoryPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
StringComparison.OrdinalIgnoreCase))
{
return Path.GetFileName(filePath);
}
return Path.IsPathRooted(filePath) ? filePath : Path.GetFileName(filePath);
}
catch
{
return Path.GetFileName(defaultFilePath);
}
}
private bool SaveHistoryRecordFile(BFITestRecord record, bool verifyAfterWrite = true, bool verboseLog = true)
{
try
{
if (record == null)
{
return false;
}
NormalizeRecordForStorage(record);
string filePath = GetHistoryRecordFilePath(record);
record.FilePath = filePath;
if ((record.DataPoints == null || record.DataPoints.Count == 0) &&
(record.Statistics?.Count ?? 0) > 0)
{
if (TryFindBestHistoryRecordById(record.TestId, out var recoveredRecord, out var recoveredPath))
{
recoveredRecord.FilePath = recoveredPath;
MergeRecordMetadata(recoveredRecord, record);
if (!string.Equals(recoveredPath, filePath, StringComparison.OrdinalIgnoreCase))
{
return SaveHistoryRecordFile(recoveredRecord, verifyAfterWrite, verboseLog);
}
MergeRecordMetadata(record, recoveredRecord);
return true;
}
Debug.LogWarning($"跳过保存仅摘要BFI记录实体文件避免覆盖明细数据: {record.TestName}, {filePath}");
return IsRecordPersisted(record.TestId, 1);
}
if (TryPromoteExistingTempRecordFile(record, filePath))
{
return true;
}
string json = JsonUtility.ToJson(record, true);
WriteAllTextAtomic(filePath, json);
if (!verifyAfterWrite)
{
return true;
}
bool persisted = IsRecordPersisted(record.TestId, record.DataPoints?.Count ?? 0);
if (!persisted)
{
Debug.LogWarning($"BFI记录写入后校验失败: {record.TestName}, {filePath}");
}
else if (verboseLog)
{
Debug.Log($"BFI记录实体文件已保存: {filePath}");
#if UNITY_ANDROID && !UNITY_EDITOR
AndroidFileLogger.Instance?.LogInfo("BFIHistory", $"记录实体文件已保存: {filePath}, 点数: {record.DataPoints?.Count ?? 0}");
#endif
}
return persisted;
}
catch (Exception ex)
{
Debug.LogError($"保存BFI单条历史记录失败: {record?.TestName}, {ex.Message}");
return false;
}
}
private bool TryPromoteExistingTempRecordFile(BFITestRecord record, string filePath)
{
try
{
if (record == null || string.IsNullOrEmpty(filePath))
{
return false;
}
string tempPath = filePath + ".tmp";
if (!File.Exists(tempPath))
{
return false;
}
if (!TryReadHistoryRecordFile(tempPath, out var tempRecord) ||
!string.Equals(tempRecord.TestId, record.TestId, StringComparison.Ordinal))
{
return false;
}
int tempPointCount = tempRecord.DataPoints?.Count ?? 0;
int recordPointCount = record.DataPoints?.Count ?? 0;
if (tempPointCount < recordPointCount)
{
return false;
}
if (File.Exists(filePath))
{
if (IsRecordPersisted(record.TestId, recordPointCount))
{
return false;
}
TryCopyFile(filePath, filePath + RECORD_BACKUP_EXTENSION);
File.Copy(tempPath, filePath, true);
}
else
{
File.Move(tempPath, filePath);
}
bool persisted = IsRecordPersisted(record.TestId, recordPointCount);
if (persisted)
{
DeleteFileIfExists(tempPath);
Debug.Log($"已将断电遗留BFI临时记录转正: {filePath}");
#if UNITY_ANDROID && !UNITY_EDITOR
AndroidFileLogger.Instance?.LogInfo("BFIHistory", $"已将断电遗留BFI临时记录转正: {filePath}");
#endif
}
return persisted;
}
catch (Exception ex)
{
Debug.LogWarning($"转正BFI临时记录失败将尝试重新写入: {filePath}, {ex.Message}");
return false;
}
}
public bool IsRecordPersisted(string testId, int minPointCount = 0)
{
try
{
if (string.IsNullOrEmpty(testId))
{
return false;
}
string filePath = GetHistoryRecordFilePath(testId);
if (!TryReadHistoryRecord(filePath, out var savedRecord))
{
return false;
}
if (!string.Equals(savedRecord.TestId, testId, StringComparison.Ordinal))
{
return false;
}
return (savedRecord.DataPoints?.Count ?? 0) >= minPointCount;
}
catch (Exception ex)
{
Debug.LogWarning($"确认BFI记录持久化失败: {testId}, {ex.Message}");
return false;
}
}
private bool ShouldReplaceRecord(BFITestRecord existingRecord, BFITestRecord candidateRecord)
{
if (existingRecord == null)
{
return true;
}
if (candidateRecord == null)
{
return false;
}
NormalizeRecordForStorage(existingRecord);
NormalizeRecordForStorage(candidateRecord);
int existingPointCount = existingRecord.DataPoints?.Count ?? 0;
int candidatePointCount = candidateRecord.DataPoints?.Count ?? 0;
if (candidatePointCount != existingPointCount)
{
return candidatePointCount > existingPointCount;
}
return candidateRecord.EndTime >= existingRecord.EndTime;
}
private bool HasDataPoints(BFITestRecord record)
{
return record?.DataPoints != null && record.DataPoints.Count > 0;
}
private BFITestRecord ChooseRecordWithMoreData(BFITestRecord current, BFITestRecord candidate)
{
if (current == null)
{
return candidate;
}
if (candidate == null)
{
return current;
}
int currentPointCount = current.DataPoints?.Count ?? 0;
int candidatePointCount = candidate.DataPoints?.Count ?? 0;
if (candidatePointCount != currentPointCount)
{
return candidatePointCount > currentPointCount ? candidate : current;
}
int currentSummaryCount = current.Statistics?.Count ?? 0;
int candidateSummaryCount = candidate.Statistics?.Count ?? 0;
if (candidateSummaryCount != currentSummaryCount)
{
return candidateSummaryCount > currentSummaryCount ? candidate : current;
}
return candidate.EndTime > current.EndTime ? candidate : current;
}
private bool TryFindBestHistoryRecordById(string testId, out BFITestRecord bestRecord, out string bestRecordPath)
{
bestRecord = null;
bestRecordPath = null;
if (string.IsNullOrEmpty(testId))
{
return false;
}
try
{
int bestPointCount = 0;
foreach (var recordFilePath in FindHistoryRecordFiles(true))
{
if (!TryReadHistoryRecordFile(recordFilePath, out var candidate) ||
!string.Equals(candidate.TestId, testId, StringComparison.Ordinal))
{
continue;
}
int candidatePointCount = candidate.DataPoints?.Count ?? 0;
if (candidatePointCount <= 0)
{
continue;
}
if (bestRecord == null || candidatePointCount > bestPointCount)
{
bestRecord = candidate;
bestRecordPath = recordFilePath;
bestPointCount = candidatePointCount;
}
}
}
catch (Exception ex)
{
Debug.LogWarning($"搜索完整BFI记录失败: {testId}, {ex.Message}");
bestRecord = null;
bestRecordPath = null;
}
return bestRecord != null;
}
private bool AreAllHistoryRecordsPersistedAsFiles()
{
if (historyRecords == null || historyRecords.Count == 0)
{
return true;
}
foreach (var record in historyRecords)
{
if (record == null)
{
continue;
}
NormalizeRecordForStorage(record);
if (!IsRecordPersisted(record.TestId, record.DataPoints?.Count ?? 0))
{
return false;
}
}
return true;
}
private void NormalizeRecordForStorage(BFITestRecord record)
{
if (record == null)
{
return;
}
if (record.DataPoints == null)
{
record.DataPoints = new List<BFIDataPoint>();
}
if (record.Statistics == null)
{
record.Statistics = new BFIStatistics();
}
bool hasSummaryStatisticsWithoutPoints =
record.DataPoints.Count == 0 &&
record.Statistics.Count > 0;
BFIStatistics preservedSummaryStatistics = hasSummaryStatisticsWithoutPoints
? CloneBFIStatistics(record.Statistics)
: null;
record.RestoreAfterDeserialization();
if (hasSummaryStatisticsWithoutPoints && record.DataPoints.Count == 0)
{
record.Statistics = preservedSummaryStatistics;
}
if (string.IsNullOrWhiteSpace(record.TestId))
{
record.TestId = GenerateStableRecordId(record);
}
if (string.IsNullOrWhiteSpace(record.TestName))
{
record.TestName = $"BFI测试_{record.StartTime:yyyyMMdd_HHmmss}";
}
record.PrepareForSerialization();
}
private BFIStatistics CloneBFIStatistics(BFIStatistics source)
{
if (source == null)
{
return new BFIStatistics();
}
return new BFIStatistics
{
Count = source.Count,
MinValue = source.MinValue,
MaxValue = source.MaxValue,
Average = source.Average,
StandardDeviation = source.StandardDeviation,
Duration = source.Duration,
LastUpdated = source.LastUpdated
};
}
private string GenerateStableRecordId(BFITestRecord record)
{
long startTicks = record.StartTimeTicks > 0 ? record.StartTimeTicks : record.StartTime.Ticks;
long endTicks = record.EndTimeTicks;
int pointCount = record.DataPoints?.Count ?? 0;
long firstPointTicks = 0;
long lastPointTicks = 0;
float firstBfi = 0f;
float lastBfi = 0f;
if (record.DataPoints != null && record.DataPoints.Count > 0)
{
var firstPoint = record.DataPoints.FirstOrDefault(p => p != null);
var lastPoint = record.DataPoints.LastOrDefault(p => p != null);
firstPointTicks = firstPoint?.TimestampTicks ?? 0;
lastPointTicks = lastPoint?.TimestampTicks ?? 0;
firstBfi = firstPoint?.BFI ?? 0f;
lastBfi = lastPoint?.BFI ?? 0f;
}
string identity = string.Join("|",
record.TestName ?? string.Empty,
record.PatientId ?? string.Empty,
record.PatientName ?? string.Empty,
startTicks.ToString(),
endTicks.ToString(),
pointCount.ToString(),
firstPointTicks.ToString(),
lastPointTicks.ToString(),
firstBfi.ToString("R"),
lastBfi.ToString("R"));
return $"legacy_{ComputeStableHash(identity):x16}";
}
private ulong ComputeStableHash(string value)
{
unchecked
{
const ulong offset = 14695981039346656037UL;
const ulong prime = 1099511628211UL;
ulong hash = offset;
foreach (char c in value ?? string.Empty)
{
hash ^= c;
hash *= prime;
}
return hash;
}
}
private string GetHistoryRecordFilePath(BFITestRecord record)
{
NormalizeRecordForStorage(record);
return GetHistoryRecordFilePath(record.TestId);
}
private string GetHistoryRecordFilePath(string testId)
{
if (string.IsNullOrEmpty(historyRecordsDirectoryPath))
{
historyRecordsDirectoryPath = GetRecordFilesDirectory(historyFilePath);
}
string safeId = SanitizeFileComponent(string.IsNullOrEmpty(testId) ? Guid.NewGuid().ToString() : testId);
return Path.Combine(historyRecordsDirectoryPath, $"{safeId}.json");
}
private string SanitizeFileComponent(string value)
{
if (string.IsNullOrEmpty(value))
{
return "unknown";
}
var invalidChars = new HashSet<char>(Path.GetInvalidFileNameChars());
var builder = new StringBuilder(value.Length);
foreach (char c in value)
{
if (invalidChars.Contains(c))
{
builder.Append('_');
}
else
{
builder.Append(c);
}
}
return builder.Length == 0 ? "unknown" : builder.ToString();
}
private void DeleteHistoryRecordFile(BFITestRecord record)
{
if (record == null || string.IsNullOrEmpty(record.TestId))
{
return;
}
DeleteHistoryRecordFile(record.TestId);
}
private void DeleteHistoryRecordFile(string testId)
{
string filePath = GetHistoryRecordFilePath(testId);
DeleteFileIfExists(filePath);
DeleteFileIfExists(filePath + RECORD_BACKUP_EXTENSION);
DeleteFileIfExists(filePath + ".tmp");
}
private void AddRecordsToMergeMap(Dictionary<string, BFITestRecord> recordsById, IEnumerable<BFITestRecord> records)
{
if (records == null)
{
return;
}
foreach (var record in records)
{
AddOrReplaceRecoveredRecord(recordsById, record);
}
}
private void DeleteAllHistoryStorageFiles()
{
var pathsToDelete = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
AddHistoryFileDeleteTargets(pathsToDelete, historyFilePath);
AddHistoryFileDeleteTargets(pathsToDelete, historyMirrorFilePath);
var indexPaths = new List<string>();
AddIndexPath(indexPaths, historyFilePath);
AddIndexPath(indexPaths, historyMirrorFilePath);
foreach (var indexPath in FindHistoryIndexFiles(true))
{
AddIndexPath(indexPaths, indexPath);
AddHistoryFileDeleteTargets(pathsToDelete, indexPath);
}
foreach (var filePath in pathsToDelete)
{
DeleteFileIfExists(filePath);
}
foreach (var indexPath in indexPaths)
{
DeleteRecordDirectoryForIndex(indexPath);
}
DeleteAllRecordDirectoriesFromSearchRoots();
}
private void AddIndexPath(List<string> indexPaths, string indexPath)
{
if (string.IsNullOrEmpty(indexPath))
{
return;
}
if (!indexPaths.Any(path => string.Equals(path, indexPath, StringComparison.OrdinalIgnoreCase)))
{
indexPaths.Add(indexPath);
}
}
private void AddHistoryFileDeleteTargets(HashSet<string> pathsToDelete, string indexFilePath)
{
if (string.IsNullOrEmpty(indexFilePath))
{
return;
}
pathsToDelete.Add(indexFilePath);
pathsToDelete.Add(indexFilePath + ".tmp");
pathsToDelete.Add(indexFilePath + RECORD_BACKUP_EXTENSION);
string directory = Path.GetDirectoryName(indexFilePath);
if (!string.IsNullOrEmpty(directory))
{
pathsToDelete.Add(Path.Combine(directory, HISTORY_BACKUP_FILE_NAME));
pathsToDelete.Add(Path.Combine(directory, CURRENT_RECORDING_JOURNAL_FILE_NAME));
}
}
private void DeleteRecordDirectoryForIndex(string indexFilePath)
{
try
{
string recordsDirectory = GetRecordFilesDirectory(indexFilePath);
if (!string.IsNullOrEmpty(recordsDirectory) && Directory.Exists(recordsDirectory))
{
Directory.Delete(recordsDirectory, true);
Debug.Log($"已删除BFI单条记录目录: {recordsDirectory}");
}
}
catch (Exception ex)
{
Debug.LogWarning($"删除BFI单条记录目录失败: {indexFilePath}, {ex.Message}");
}
}
private void DeleteAllRecordDirectoriesFromSearchRoots()
{
foreach (var root in GetHistoryIndexSearchRoots())
{
DeleteRecordDirectory(root);
TryDeleteRecordDirectoriesOneLevel(root);
}
}
private void TryDeleteRecordDirectoriesOneLevel(string root)
{
try
{
if (string.IsNullOrEmpty(root) || !Directory.Exists(root))
{
return;
}
foreach (var directory in Directory.GetDirectories(root))
{
DeleteRecordDirectory(directory);
}
}
catch (Exception ex)
{
Debug.LogWarning($"搜索待删除BFI单条记录目录失败: {root}, {ex.Message}");
}
}
private void DeleteRecordDirectory(string root)
{
try
{
if (string.IsNullOrEmpty(root))
{
return;
}
string recordsDirectory = Path.Combine(root, HISTORY_RECORDS_DIR_NAME);
if (Directory.Exists(recordsDirectory))
{
Directory.Delete(recordsDirectory, true);
Debug.Log($"已删除BFI单条记录目录: {recordsDirectory}");
}
}
catch (Exception ex)
{
Debug.LogWarning($"删除BFI单条记录目录失败: {root}, {ex.Message}");
}
}
private void DeleteFileIfExists(string filePath)
{
try
{
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
{
File.Delete(filePath);
Debug.Log($"已删除BFI历史文件: {filePath}");
}
}
catch (Exception ex)
{
Debug.LogWarning($"删除BFI历史文件失败: {filePath}, {ex.Message}");
}
}
private void WriteAllTextAtomic(string filePath, string content)
{
string directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
string tempPath = filePath + ".tmp";
string backupPath = filePath + RECORD_BACKUP_EXTENSION;
byte[] bytes = Encoding.UTF8.GetBytes(content ?? string.Empty);
EnsureEnoughFreeSpace(filePath, bytes.Length);
DeleteFileIfExists(tempPath);
using (var stream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.Read, 4096))
{
stream.Write(bytes, 0, bytes.Length);
FlushFileStream(stream);
}
if (File.Exists(filePath))
{
try
{
TryCopyFile(filePath, backupPath);
File.Delete(filePath);
}
catch (Exception ex)
{
Debug.LogWarning($"备份旧BFI历史文件失败将继续尝试覆盖: {filePath}, {ex.Message}");
DeleteFileIfExists(filePath);
}
}
File.Move(tempPath, filePath);
}
private void FlushFileStream(FileStream stream)
{
try
{
stream.Flush(true);
}
catch
{
stream.Flush();
}
}
private void TryCopyFile(string sourcePath, string targetPath)
{
try
{
if (!string.IsNullOrEmpty(sourcePath) && File.Exists(sourcePath))
{
File.Copy(sourcePath, targetPath, true);
}
}
catch (Exception ex)
{
Debug.LogWarning($"备份BFI历史文件失败: {sourcePath}, {ex.Message}");
}
}
private void EnsureEnoughFreeSpace(string filePath, long requiredBytes)
{
#if UNITY_ANDROID && !UNITY_EDITOR
// Android 的 /storage/emulated/0 路径在部分系统上通过 DriveInfo 会返回 0MB
// 但实际 FileStream 仍然可以写入。这里不做硬拦截,真实空间不足交给实际写入报错。
return;
#else
try
{
string root = Path.GetPathRoot(Path.GetFullPath(filePath));
if (string.IsNullOrEmpty(root))
{
return;
}
var drive = new DriveInfo(root);
const long safetyReserveBytes = 10L * 1024L * 1024L;
long availableFreeSpace = drive.AvailableFreeSpace;
if (availableFreeSpace <= 0)
{
Debug.LogWarning($"无法可靠获取BFI历史存储剩余空间将继续尝试写入: {filePath}");
return;
}
if (drive.IsReady && availableFreeSpace < requiredBytes + safetyReserveBytes)
{
throw new IOException($"BFI历史存储空间不足剩余 {availableFreeSpace / 1024 / 1024}MB需要至少 {(requiredBytes + safetyReserveBytes) / 1024 / 1024}MB");
}
}
catch (IOException)
{
throw;
}
catch (Exception ex)
{
Debug.LogWarning($"检查BFI历史存储空间失败将继续尝试写入: {ex.Message}");
}
#endif
}
private string GetRecordFilesDirectory(string indexFilePath)
{
if (string.IsNullOrEmpty(indexFilePath))
{
return null;
}
string indexDirectory = Path.GetDirectoryName(indexFilePath);
return string.IsNullOrEmpty(indexDirectory) ? null : Path.Combine(indexDirectory, HISTORY_RECORDS_DIR_NAME);
}
/// <summary>
/// 从文件加载历史记录
/// </summary>
public void LoadHistoryRecords(bool notifyListeners = true, bool recoverMissingIndex = true)
{
try
{
maxHistoryRecords = Mathf.Max(maxHistoryRecords, MIN_HISTORY_RECORD_LIMIT);
if (!File.Exists(historyFilePath))
{
historyRecords = SafeRecoverHistoryRecordsFromIndexes(null, recoverMissingIndex);
historyFileLoadFailed = false;
Debug.Log($"历史索引文件不存在,{(historyRecords.Count > 0 ? "" : "")}");
if (historyRecords.Count > 0)
{
SaveHistoryRecords();
}
if (notifyListeners)
{
OnHistoryLoaded?.Invoke();
}
return;
}
if (TryReadHistoryRecords(historyFilePath, out var loadedRecords))
{
historyRecords = loadedRecords;
historyFileLoadFailed = false;
Debug.Log($"历史索引已加载,共 {historyRecords.Count} 条记录");
}
else
{
if (historyRecords == null)
{
historyRecords = new List<BFITestRecord>();
}
historyFileLoadFailed = false;
Debug.Log($"历史索引为空或无法解析,保留内存记录 {historyRecords.Count} 条");
}
int beforeCount = historyRecords.Count;
historyRecords = SafeRecoverHistoryRecordsFromIndexes(historyRecords, recoverMissingIndex);
if (historyRecords.Count != beforeCount || !AreAllHistoryRecordsPersistedAsFiles())
{
Debug.Log($"历史记录文件恢复完成,恢复前 {beforeCount} 条,恢复后 {historyRecords.Count} 条");
SaveHistoryRecords();
}
if (notifyListeners)
{
OnHistoryLoaded?.Invoke();
}
}
catch (Exception ex)
{
Debug.LogError($"加载历史记录失败: {ex.Message}");
historyRecords = SafeRecoverHistoryRecordsFromIndexes(null, recoverMissingIndex);
historyFileLoadFailed = historyRecords.Count == 0;
if (historyRecords.Count > 0)
{
SaveHistoryRecords();
if (notifyListeners)
{
OnHistoryLoaded?.Invoke();
}
}
}
}
private List<BFITestRecord> SafeRecoverHistoryRecordsFromIndexes(List<BFITestRecord> baseRecords, bool includeSearchRoots)
{
try
{
return RecoverHistoryRecordsFromIndexes(baseRecords, includeSearchRoots);
}
catch (Exception ex)
{
Debug.LogError($"恢复BFI历史索引失败: {ex.Message}");
return baseRecords != null ? new List<BFITestRecord>(baseRecords) : new List<BFITestRecord>();
}
}
private List<BFITestRecord> RecoverHistoryRecordsFromIndexes(List<BFITestRecord> baseRecords, bool includeSearchRoots)
{
var mergedRecords = new Dictionary<string, BFITestRecord>();
if (baseRecords != null)
{
foreach (var record in baseRecords)
{
AddOrReplaceRecoveredRecord(mergedRecords, record);
}
}
foreach (var indexPath in FindHistoryIndexFiles(includeSearchRoots))
{
if (!TryReadHistoryRecords(indexPath, out var recoveredRecords))
{
continue;
}
foreach (var record in recoveredRecords)
{
AddOrReplaceRecoveredRecord(mergedRecords, record);
}
}
foreach (var recordFilePath in FindHistoryRecordFiles(includeSearchRoots))
{
if (TryReadHistoryRecord(recordFilePath, out var recoveredRecord))
{
AddOrReplaceRecoveredRecord(mergedRecords, recoveredRecord);
}
}
return mergedRecords.Values
.OrderByDescending(r => r.StartTime)
.Take(maxHistoryRecords)
.OrderBy(r => r.StartTime)
.ToList();
}
private bool TryReadHistoryRecords(string filePath, out List<BFITestRecord> records)
{
records = new List<BFITestRecord>();
try
{
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
{
return false;
}
string json = File.ReadAllText(filePath);
if (string.IsNullOrWhiteSpace(json))
{
return false;
}
var wrapper = JsonUtility.FromJson<BFIHistoryWrapper>(json);
if (wrapper == null)
{
return false;
}
bool recognizedIndex = false;
if (wrapper.Records != null)
{
recognizedIndex = true;
foreach (var record in wrapper.Records.Where(record => record != null))
{
NormalizeRecordForStorage(record);
records.Add(record);
}
}
if (wrapper.RecordFiles != null)
{
recognizedIndex = true;
foreach (var entry in wrapper.RecordFiles.Where(entry => entry != null))
{
string recordPath = ResolveIndexedRecordPath(filePath, entry);
if (TryReadHistoryRecord(recordPath, out var record))
{
AddOrReplaceRecoveredRecordToList(records, record);
}
}
}
if (!recognizedIndex)
{
return false;
}
Debug.Log($"读取BFI历史索引成功: {filePath}, 记录数: {records.Count}");
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"读取BFI历史索引失败: {filePath}, {ex.Message}");
records = new List<BFITestRecord>();
return false;
}
}
private bool TryReadHistoryRecordSummaries(string filePath, out List<BFITestRecord> records, out bool repairedSummaries)
{
records = new List<BFITestRecord>();
repairedSummaries = false;
try
{
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
{
return false;
}
string json = File.ReadAllText(filePath);
if (string.IsNullOrWhiteSpace(json))
{
return false;
}
var wrapper = JsonUtility.FromJson<BFIHistoryWrapper>(json);
if (wrapper == null)
{
return false;
}
if (wrapper.RecordFiles != null && wrapper.RecordFiles.Count > 0)
{
foreach (var entry in wrapper.RecordFiles.Where(entry => entry != null))
{
var summary = CreateSummaryRecordFromIndexEntry(entry, filePath);
if (summary != null)
{
if (NeedsSummaryRepair(summary) && TryRepairSummaryFromRecordFile(summary, out var repairedSummary))
{
summary = repairedSummary;
repairedSummaries = true;
}
AddOrReplaceRecoveredRecordToList(records, summary);
}
}
Debug.Log($"读取BFI历史摘要成功: {filePath}, 记录数: {records.Count}");
return true;
}
if (wrapper.Records != null)
{
foreach (var record in wrapper.Records.Where(record => record != null))
{
NormalizeRecordForStorage(record);
records.Add(record);
}
Debug.Log($"读取旧版BFI历史摘要成功: {filePath}, 记录数: {records.Count}");
return true;
}
return false;
}
catch (Exception ex)
{
Debug.LogWarning($"读取BFI历史摘要失败: {filePath}, {ex.Message}");
records = new List<BFITestRecord>();
repairedSummaries = false;
return false;
}
}
private bool NeedsSummaryRepair(BFITestRecord summary)
{
if (summary == null)
{
return false;
}
if (summary.DataPoints != null && summary.DataPoints.Count > 0)
{
return false;
}
var stats = summary.Statistics;
if (stats == null || stats.Count <= 0)
{
return true;
}
return Mathf.Approximately(stats.MinValue, 0f) &&
Mathf.Approximately(stats.MaxValue, 0f) &&
Mathf.Approximately(stats.Average, 0f);
}
private bool TryRepairSummaryFromRecordFile(BFITestRecord summary, out BFITestRecord repairedSummary)
{
repairedSummary = null;
string recordPath = summary?.FilePath;
if (string.IsNullOrEmpty(recordPath))
{
return false;
}
if (!TryReadHistoryRecord(recordPath, out var fullRecord) ||
fullRecord?.DataPoints == null ||
fullRecord.DataPoints.Count == 0)
{
return false;
}
fullRecord.FilePath = recordPath;
repairedSummary = CreateSummaryRecordFromFullRecord(fullRecord);
Debug.Log($"已从实体文件修复BFI历史摘要: {fullRecord.TestName}, 点数: {fullRecord.DataPoints.Count}");
return true;
}
private BFITestRecord CreateSummaryRecordFromFullRecord(BFITestRecord fullRecord)
{
if (fullRecord == null)
{
return null;
}
NormalizeRecordForStorage(fullRecord);
return new BFITestRecord
{
TestId = fullRecord.TestId,
TestName = fullRecord.TestName,
PatientId = fullRecord.PatientId ?? string.Empty,
PatientName = fullRecord.PatientName ?? string.Empty,
PatientGender = fullRecord.PatientGender ?? string.Empty,
PatientAge = fullRecord.PatientAge,
PatientHeight = fullRecord.PatientHeight,
PatientWeight = fullRecord.PatientWeight,
StartTime = fullRecord.StartTime,
StartTimeTicks = fullRecord.StartTimeTicks,
EndTime = fullRecord.EndTime,
EndTimeTicks = fullRecord.EndTimeTicks,
FilePath = fullRecord.FilePath,
DataPoints = new List<BFIDataPoint>(),
Statistics = CloneBFIStatistics(fullRecord.Statistics)
};
}
private BFITestRecord CreateSummaryRecordFromIndexEntry(BFIHistoryRecordIndexEntry entry, string indexFilePath)
{
if (entry == null)
{
return null;
}
long startTicks = entry.StartTimeTicks > 0 ? entry.StartTimeTicks : DateTime.Now.Ticks;
long endTicks = entry.EndTimeTicks > 0 ? entry.EndTimeTicks : 0;
var startTime = new DateTime(startTicks, DateTimeKind.Local);
var endTime = endTicks > 0 ? new DateTime(endTicks, DateTimeKind.Local) : DateTime.MinValue;
var record = new BFITestRecord
{
TestId = string.IsNullOrEmpty(entry.TestId) ? Guid.NewGuid().ToString() : entry.TestId,
TestName = string.IsNullOrEmpty(entry.TestName) ? $"BFI测试_{startTime:yyyyMMdd_HHmmss}" : entry.TestName,
PatientId = entry.PatientId ?? string.Empty,
PatientName = entry.PatientName ?? string.Empty,
StartTime = startTime,
StartTimeTicks = startTicks,
EndTime = endTime,
EndTimeTicks = endTicks,
FilePath = ResolveIndexedRecordPath(indexFilePath, entry),
DataPoints = new List<BFIDataPoint>(),
Statistics = new BFIStatistics
{
Count = entry.PointCount,
MinValue = entry.MinValue,
MaxValue = entry.MaxValue,
Average = entry.Average,
Duration = endTime > startTime ? (endTime - startTime).TotalMinutes : 0,
LastUpdated = DateTime.Now
}
};
return record;
}
private void AddOrReplaceRecoveredRecordToList(List<BFITestRecord> records, BFITestRecord record)
{
if (records == null || record == null)
{
return;
}
NormalizeRecordForStorage(record);
var existingRecord = records.FirstOrDefault(r => r != null && r.TestId == record.TestId);
if (existingRecord == null)
{
records.Add(record);
return;
}
if (ShouldReplaceRecord(existingRecord, record))
{
int index = records.IndexOf(existingRecord);
records[index] = record;
return;
}
MergeRecordMetadata(existingRecord, record);
}
private void MergeRecordMetadata(BFITestRecord target, BFITestRecord source)
{
if (target == null || source == null)
{
return;
}
if (string.IsNullOrEmpty(target.FilePath) && !string.IsNullOrEmpty(source.FilePath))
{
target.FilePath = source.FilePath;
}
bool targetHasFullData = target.DataPoints != null && target.DataPoints.Count > 0;
bool sourceHasSummaryStats = source.Statistics != null && source.Statistics.Count > 0;
if (!targetHasFullData && sourceHasSummaryStats)
{
target.Statistics = CloneBFIStatistics(source.Statistics);
}
}
private string ResolveIndexedRecordPath(string indexFilePath, BFIHistoryRecordIndexEntry entry)
{
if (entry == null)
{
return null;
}
if (!string.IsNullOrEmpty(entry.FileName) && Path.IsPathRooted(entry.FileName))
{
return entry.FileName;
}
string recordsDirectory = GetRecordFilesDirectory(indexFilePath);
if (string.IsNullOrEmpty(recordsDirectory))
{
recordsDirectory = historyRecordsDirectoryPath;
}
string fileName = entry.FileName;
if (string.IsNullOrEmpty(fileName))
{
fileName = $"{SanitizeFileComponent(entry.TestId)}.json";
}
return Path.Combine(recordsDirectory, fileName);
}
private bool TryReadHistoryRecord(string filePath, out BFITestRecord record)
{
if (TryReadHistoryRecordFile(filePath, out record))
{
return true;
}
if (!string.IsNullOrEmpty(filePath) &&
!filePath.EndsWith(RECORD_BACKUP_EXTENSION, StringComparison.OrdinalIgnoreCase))
{
string backupPath = filePath + RECORD_BACKUP_EXTENSION;
if (TryReadHistoryRecordFile(backupPath, out record))
{
Debug.LogWarning($"使用BFI单条记录备份恢复: {backupPath}");
return true;
}
}
return false;
}
private bool TryReadHistoryRecordFile(string filePath, out BFITestRecord record)
{
record = null;
try
{
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
{
return false;
}
string json = File.ReadAllText(filePath);
if (string.IsNullOrWhiteSpace(json))
{
return false;
}
record = JsonUtility.FromJson<BFITestRecord>(json);
if (record == null)
{
return false;
}
record.RestoreAfterDeserialization();
NormalizeRecordForStorage(record);
return true;
}
catch (Exception ex)
{
Debug.LogWarning($"读取BFI单条记录失败: {filePath}, {ex.Message}");
record = null;
return false;
}
}
private void AddOrReplaceRecoveredRecord(Dictionary<string, BFITestRecord> recordsById, BFITestRecord record)
{
if (record == null)
{
return;
}
NormalizeRecordForStorage(record);
if (!recordsById.TryGetValue(record.TestId, out var existingRecord))
{
recordsById[record.TestId] = record;
return;
}
if (ShouldReplaceRecord(existingRecord, record))
{
recordsById[record.TestId] = record;
return;
}
MergeRecordMetadata(existingRecord, record);
}
private List<string> FindHistoryIndexFiles(bool includeSearchRoots)
{
var result = new List<string>();
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
AddHistoryIndexFilesFromRoot(Path.GetDirectoryName(historyFilePath), 0, result, visited);
if (includeSearchRoots)
{
foreach (var root in GetHistoryIndexSearchRoots())
{
AddHistoryIndexFilesFromRoot(root, 2, result, visited);
}
}
Debug.Log($"BFI历史索引搜索完成找到 {result.Count} 个候选文件");
return result;
}
private List<string> FindHistoryRecordFiles(bool includeSearchRoots)
{
var result = new List<string>();
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
AddHistoryRecordFilesFromDirectory(historyRecordsDirectoryPath, result, visited);
if (includeSearchRoots)
{
foreach (var root in GetHistoryIndexSearchRoots())
{
AddHistoryRecordFilesFromDirectory(Path.Combine(root, HISTORY_RECORDS_DIR_NAME), result, visited);
}
}
Debug.Log($"BFI单条历史记录搜索完成找到 {result.Count} 个候选文件");
return result;
}
private void AddHistoryRecordFilesFromDirectory(string directory, List<string> result, HashSet<string> visited)
{
if (string.IsNullOrEmpty(directory))
{
return;
}
try
{
if (!Directory.Exists(directory))
{
return;
}
string fullDirectory = Path.GetFullPath(directory);
if (!visited.Add(fullDirectory))
{
return;
}
AddHistoryRecordFilesByPattern(fullDirectory, "*.json", result);
AddHistoryRecordFilesByPattern(fullDirectory, "*.json" + RECORD_BACKUP_EXTENSION, result);
AddHistoryRecordFilesByPattern(fullDirectory, "*.json.tmp", result);
}
catch (Exception ex)
{
Debug.LogWarning($"搜索BFI单条历史记录目录失败: {directory}, {ex.Message}");
}
}
private void AddHistoryRecordFilesByPattern(string directory, string pattern, List<string> result)
{
foreach (var filePath in Directory.GetFiles(directory, pattern))
{
if (!result.Any(path => string.Equals(path, filePath, StringComparison.OrdinalIgnoreCase)))
{
result.Add(filePath);
}
}
}
public List<BFITestRecord> LoadHistoryRecordSummaries(bool recoverMissingIndex = false)
{
maxHistoryRecords = Mathf.Max(maxHistoryRecords, MIN_HISTORY_RECORD_LIMIT);
try
{
if (TryReadHistoryRecordSummaries(historyFilePath, out var summaries, out bool repairedSummaries))
{
historyRecords = GetRecordsSortedByRecentFirst(summaries)
.Take(maxHistoryRecords)
.ToList();
historyFileLoadFailed = false;
if (repairedSummaries)
{
Debug.Log("检测到BFI历史摘要缺少明细统计已从实体文件修复并刷新索引");
SaveHistoryIndex();
}
return GetAllRecords();
}
LoadHistoryRecords(false, recoverMissingIndex);
return GetAllRecords();
}
catch (Exception ex)
{
Debug.LogWarning($"加载BFI历史摘要失败回退完整加载: {ex.Message}");
LoadHistoryRecords(false, recoverMissingIndex);
return GetAllRecords();
}
}
private IEnumerable<string> GetHistoryIndexSearchRoots()
{
var roots = new List<string>();
AddSearchRoot(roots, Path.GetDirectoryName(historyFilePath));
if (!string.IsNullOrEmpty(historyMirrorFilePath))
{
AddSearchRoot(roots, Path.GetDirectoryName(historyMirrorFilePath));
}
AddSearchRoot(roots, Application.persistentDataPath);
AddSearchRoot(roots, Path.Combine(Application.dataPath, "..", "Exports"));
AddSearchRoot(roots, Path.Combine(Application.dataPath, "Exports"));
#if UNITY_ANDROID && !UNITY_EDITOR
AddSearchRoot(roots, AndroidExportPermissionManager.GetStableAppInternalDirectory());
AddSearchRoot(roots, GetAndroidFilesDirectory());
AddSearchRoot(roots, GetAndroidExternalFilesDirectory(null));
AddSearchRoot(roots, GetAndroidExternalFilesDirectory("Documents"));
string downloadDirectory = GetAndroidDownloadDirectory();
AddSearchRoot(roots, downloadDirectory);
if (!string.IsNullOrEmpty(downloadDirectory))
{
AddSearchRoot(roots, Path.Combine(downloadDirectory, "DCX_Export"));
}
#endif
return roots;
}
private void AddSearchRoot(List<string> roots, string root)
{
if (string.IsNullOrEmpty(root))
{
return;
}
try
{
string fullPath = Path.GetFullPath(root);
if (!roots.Any(existing => string.Equals(existing, fullPath, StringComparison.OrdinalIgnoreCase)))
{
roots.Add(fullPath);
}
}
catch
{
if (!roots.Contains(root))
{
roots.Add(root);
}
}
}
private void AddHistoryIndexFilesFromRoot(string root, int depth, List<string> result, HashSet<string> visited)
{
if (string.IsNullOrEmpty(root) || depth < 0)
{
return;
}
try
{
if (!Directory.Exists(root))
{
return;
}
string fullRoot = Path.GetFullPath(root);
if (!visited.Add(fullRoot))
{
return;
}
foreach (var candidateFileName in HISTORY_CANDIDATE_FILE_NAMES)
{
string directFile = Path.Combine(fullRoot, candidateFileName);
if (File.Exists(directFile) && !result.Any(path => string.Equals(path, directFile, StringComparison.OrdinalIgnoreCase)))
{
result.Add(directFile);
}
string backupFile = directFile + RECORD_BACKUP_EXTENSION;
if (File.Exists(backupFile) && !result.Any(path => string.Equals(path, backupFile, StringComparison.OrdinalIgnoreCase)))
{
result.Add(backupFile);
}
}
if (depth == 0)
{
return;
}
foreach (var directory in Directory.GetDirectories(fullRoot))
{
AddHistoryIndexFilesFromRoot(directory, depth - 1, result, visited);
}
}
catch (Exception ex)
{
Debug.LogWarning($"搜索BFI历史索引目录失败: {root}, {ex.Message}");
}
}
#if UNITY_ANDROID && !UNITY_EDITOR
private string GetAndroidFilesDirectory()
{
try
{
using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var activity = unityClass.GetStatic<AndroidJavaObject>("currentActivity"))
using (var context = activity.Call<AndroidJavaObject>("getApplicationContext"))
{
var filesDir = context.Call<AndroidJavaObject>("getFilesDir");
return filesDir?.Call<string>("getAbsolutePath");
}
}
catch (Exception ex)
{
Debug.LogWarning($"获取Android内部目录失败: {ex.Message}");
return null;
}
}
private string GetAndroidExternalFilesDirectory(string type)
{
try
{
using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
using (var activity = unityClass.GetStatic<AndroidJavaObject>("currentActivity"))
using (var context = activity.Call<AndroidJavaObject>("getApplicationContext"))
{
var externalDir = context.Call<AndroidJavaObject>("getExternalFilesDir", type);
return externalDir?.Call<string>("getAbsolutePath");
}
}
catch (Exception ex)
{
Debug.LogWarning($"获取Android外部应用目录失败: {ex.Message}");
return null;
}
}
private string GetAndroidDownloadDirectory()
{
try
{
using (var environment = new AndroidJavaClass("android.os.Environment"))
using (var externalStorage = environment.CallStatic<AndroidJavaObject>("getExternalStorageDirectory"))
{
string storagePath = externalStorage?.Call<string>("getAbsolutePath");
return string.IsNullOrEmpty(storagePath) ? null : Path.Combine(storagePath, "Download");
}
}
catch (Exception ex)
{
Debug.LogWarning($"获取Android Download目录失败: {ex.Message}");
return null;
}
}
#endif
/// <summary>
/// 移除最旧的记录
/// </summary>
private void RemoveOldestRecord()
{
if (historyRecords.Count == 0) return;
var oldestRecord = historyRecords.OrderBy(r => r.StartTime).First();
historyRecords.Remove(oldestRecord);
DeleteHistoryRecordFile(oldestRecord);
Debug.Log($"移除最旧的记录: {oldestRecord.TestName}");
}
/// <summary>
/// 获取统计信息
/// </summary>
public BFIHistoryStatistics GetHistoryStatistics()
{
var stats = new BFIHistoryStatistics();
if (historyRecords == null || historyRecords.Count == 0)
return stats;
stats.TotalRecords = historyRecords.Count;
stats.TotalTestTime = historyRecords.Sum(r => r.GetDurationMinutes());
var today = DateTime.Today;
stats.TodayRecords = historyRecords.Count(r => r.StartTime.Date == today);
var thisWeek = today.AddDays(-(int)today.DayOfWeek);
stats.WeekRecords = historyRecords.Count(r => r.StartTime >= thisWeek);
var thisMonth = new DateTime(today.Year, today.Month, 1);
stats.MonthRecords = historyRecords.Count(r => r.StartTime >= thisMonth);
return stats;
}
/// <summary>
/// 导出历史记录
/// </summary>
public bool ExportHistory(List<string> selectedRecordIds, ExportFormat format)
{
try
{
var selectedRecords = new List<BFITestRecord>();
if (selectedRecordIds == null || selectedRecordIds.Count == 0)
{
selectedRecords = GetAllRecords();
}
else
{
foreach (var id in selectedRecordIds)
{
var record = GetRecord(id);
if (record != null)
selectedRecords.Add(record);
}
}
if (selectedRecords.Count == 0)
{
Debug.LogWarning("没有可导出的记录");
return false;
}
var exportService = new DataExportService();
return exportService.ExportHistoryRecords(selectedRecords, format);
}
catch (Exception ex)
{
Debug.LogError($"导出历史记录失败: {ex.Message}");
return false;
}
}
private void OnDestroy()
{
if (autoSave && historyRecords != null)
{
SaveHistoryIndex();
}
}
private void OnApplicationPause(bool pauseStatus)
{
if (pauseStatus && autoSave && historyIndexDirty)
{
SaveHistoryIndex();
}
}
private void OnApplicationFocus(bool hasFocus)
{
if (!hasFocus && autoSave && historyIndexDirty)
{
SaveHistoryIndex();
}
else if (hasFocus && historyRecords != null && historyRecords.Count == 0)
{
LoadHistoryRecords(true, false);
}
}
}
/// <summary>
/// 历史记录包装类用于JSON序列化
/// </summary>
[System.Serializable]
public class BFIHistoryWrapper
{
public int Version;
public string StorageMode;
public List<BFITestRecord> Records;
public List<BFIHistoryRecordIndexEntry> RecordFiles;
}
[System.Serializable]
public class BFIHistoryRecordIndexEntry
{
public string TestId;
public string TestName;
public string PatientId;
public string PatientName;
public string FileName;
public long StartTimeTicks;
public long EndTimeTicks;
public int PointCount;
public float MinValue;
public float MaxValue;
public float Average;
}
/// <summary>
/// 历史记录统计信息
/// </summary>
[System.Serializable]
public class BFIHistoryStatistics
{
public int TotalRecords; // 总记录数
public double TotalTestTime; // 总测试时间(分钟)
public int TodayRecords; // 今日记录数
public int WeekRecords; // 本周记录数
public int MonthRecords; // 本月记录数
}