using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEngine;
///
/// BFI测试历史记录管理服务
/// 负责保存、加载和管理历史测试记录
///
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();
DontDestroyOnLoad(go);
}
return _instance;
}
}
[Header("配置")]
[SerializeField] private int maxHistoryRecords = 1000; // 最大历史记录数
[SerializeField] private bool autoSave = true; // 自动保存
[SerializeField] private float saveInterval = 60f; // 保存间隔(秒)
private List 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 OnRecordAdded;
public event System.Action OnRecordUpdated;
public event System.Action 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();
// 历史索引是导出面板的数据源,主文件使用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(
"getExternalStoragePublicDirectory",
environment.GetStatic("DIRECTORY_DOWNLOADS")))
{
string path = downloadDir?.Call("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("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;
}
}
///
/// 添加新的测试记录
///
public bool AddTestRecord(BFITestRecord record)
{
if (record == null)
{
Debug.LogWarning("尝试添加空的测试记录");
return false;
}
try
{
if (historyRecords == null)
{
historyRecords = new List();
}
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;
}
}
///
/// 保存自动记录中的中间快照。实时逐点日志已经承担断电恢复职责,
/// 这里避免每次快照都重写索引和反读整条记录,降低安卓板主线程IO压力。
///
public bool AddTestRecordSnapshot(BFITestRecord record)
{
if (record == null)
{
return false;
}
try
{
if (historyRecords == null)
{
historyRecords = new List();
}
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;
}
}
///
/// 移除测试记录
///
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;
}
}
///
/// 获取所有历史记录
///
public List GetAllRecords()
{
return GetRecordsSortedByRecentFirst(historyRecords);
}
///
/// 根据ID获取记录
///
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;
}
///
/// 根据日期范围获取记录
///
public List GetRecordsByDateRange(DateTime startDate, DateTime endDate)
{
return GetRecordsSortedByRecentFirst(historyRecords.Where(r => r.StartTime >= startDate && r.StartTime <= endDate));
}
///
/// 根据患者ID获取记录
///
public List GetRecordsByPatient(string patientId)
{
return GetRecordsSortedByRecentFirst(historyRecords.Where(r => r.PatientId == patientId));
}
///
/// 搜索记录
///
public List 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
));
}
///
/// 获取最近的记录
///
public List GetRecentRecords(int count = 10)
{
return GetRecordsSortedByRecentFirst(historyRecords).Take(count).ToList();
}
private List GetRecordsSortedByRecentFirst(IEnumerable records)
{
return (records ?? Enumerable.Empty())
.Where(r => r != null)
.OrderByDescending(r => r.StartTime)
.ThenByDescending(r => r.EndTime)
.ToList();
}
///
/// 清空历史记录
///
public void ClearHistory()
{
try
{
historyRecords.Clear();
historyFileLoadFailed = false;
historyIndexDirty = false;
DeleteAllHistoryStorageFiles();
SaveHistoryRecords();
OnHistoryLoaded?.Invoke();
Debug.Log("历史记录已清空");
}
catch (Exception ex)
{
Debug.LogError($"清空历史记录失败: {ex.Message}");
}
}
///
/// 保存历史记录到文件
///
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();
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(),
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();
}
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(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 recordsById, IEnumerable records)
{
if (records == null)
{
return;
}
foreach (var record in records)
{
AddOrReplaceRecoveredRecord(recordsById, record);
}
}
private void DeleteAllHistoryStorageFiles()
{
var pathsToDelete = new HashSet(StringComparer.OrdinalIgnoreCase);
AddHistoryFileDeleteTargets(pathsToDelete, historyFilePath);
AddHistoryFileDeleteTargets(pathsToDelete, historyMirrorFilePath);
var indexPaths = new List();
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 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 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);
}
///
/// 从文件加载历史记录
///
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();
}
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 SafeRecoverHistoryRecordsFromIndexes(List baseRecords, bool includeSearchRoots)
{
try
{
return RecoverHistoryRecordsFromIndexes(baseRecords, includeSearchRoots);
}
catch (Exception ex)
{
Debug.LogError($"恢复BFI历史索引失败: {ex.Message}");
return baseRecords != null ? new List(baseRecords) : new List();
}
}
private List RecoverHistoryRecordsFromIndexes(List baseRecords, bool includeSearchRoots)
{
var mergedRecords = new Dictionary();
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 records)
{
records = new List();
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(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();
return false;
}
}
private bool TryReadHistoryRecordSummaries(string filePath, out List records, out bool repairedSummaries)
{
records = new List();
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(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();
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(),
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(),
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 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(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 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 FindHistoryIndexFiles(bool includeSearchRoots)
{
var result = new List();
var visited = new HashSet(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 FindHistoryRecordFiles(bool includeSearchRoots)
{
var result = new List();
var visited = new HashSet(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 result, HashSet 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 result)
{
foreach (var filePath in Directory.GetFiles(directory, pattern))
{
if (!result.Any(path => string.Equals(path, filePath, StringComparison.OrdinalIgnoreCase)))
{
result.Add(filePath);
}
}
}
public List 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 GetHistoryIndexSearchRoots()
{
var roots = new List();
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 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 result, HashSet 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("currentActivity"))
using (var context = activity.Call("getApplicationContext"))
{
var filesDir = context.Call("getFilesDir");
return filesDir?.Call("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("currentActivity"))
using (var context = activity.Call("getApplicationContext"))
{
var externalDir = context.Call("getExternalFilesDir", type);
return externalDir?.Call("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("getExternalStorageDirectory"))
{
string storagePath = externalStorage?.Call("getAbsolutePath");
return string.IsNullOrEmpty(storagePath) ? null : Path.Combine(storagePath, "Download");
}
}
catch (Exception ex)
{
Debug.LogWarning($"获取Android Download目录失败: {ex.Message}");
return null;
}
}
#endif
///
/// 移除最旧的记录
///
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}");
}
///
/// 获取统计信息
///
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;
}
///
/// 导出历史记录
///
public bool ExportHistory(List selectedRecordIds, ExportFormat format)
{
try
{
var selectedRecords = new List();
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);
}
}
}
///
/// 历史记录包装类(用于JSON序列化)
///
[System.Serializable]
public class BFIHistoryWrapper
{
public int Version;
public string StorageMode;
public List Records;
public List 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;
}
///
/// 历史记录统计信息
///
[System.Serializable]
public class BFIHistoryStatistics
{
public int TotalRecords; // 总记录数
public double TotalTestTime; // 总测试时间(分钟)
public int TodayRecords; // 今日记录数
public int WeekRecords; // 本周记录数
public int MonthRecords; // 本月记录数
}