feat(health): 优化HRV数据质量分析与获取逻辑

- 新增HRV质量评分算法,综合评估数值有效性、数据源可靠性与元数据完整性
- 实现最佳质量HRV值自动选取,优先手动测量并过滤异常值
- 扩展TS类型定义,支持完整HRV数据结构及质量分析接口
- 移除StressMeter中未使用的时间格式化函数与注释代码
- 默认采样数提升至50条,增强质量分析准确性
This commit is contained in:
richarjiang
2025-09-24 18:29:58 +08:00
parent 6303795870
commit 83e534c4a7
3 changed files with 374 additions and 41 deletions

View File

@@ -623,7 +623,9 @@ class HealthKitManager: NSObject, RCTBridgeModule {
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
let limit = options["limit"] as? Int ?? HKObjectQueryNoLimit
// 50
let limit = options["limit"] as? Int ?? 50
let query = HKSampleQuery(sampleType: hrvType,
predicate: predicate,
@@ -639,6 +641,7 @@ class HealthKitManager: NSObject, RCTBridgeModule {
resolver([
"data": [],
"count": 0,
"bestQualityValue": nil,
"startDate": self?.dateToISOString(startDate) ?? "",
"endDate": self?.dateToISOString(endDate) ?? ""
])
@@ -646,22 +649,31 @@ class HealthKitManager: NSObject, RCTBridgeModule {
}
let hrvData = hrvSamples.map { sample in
[
let hrvValueMs = sample.quantity.doubleValue(for: HKUnit.secondUnit(with: .milli))
let sourceBundle = sample.sourceRevision.source.bundleIdentifier
return [
"id": sample.uuid.uuidString,
"startDate": self?.dateToISOString(sample.startDate) ?? "",
"endDate": self?.dateToISOString(sample.endDate) ?? "",
"value": sample.quantity.doubleValue(for: HKUnit.secondUnit(with: .milli)),
"value": hrvValueMs,
"source": [
"name": sample.sourceRevision.source.name,
"bundleIdentifier": sample.sourceRevision.source.bundleIdentifier
"bundleIdentifier": sourceBundle
],
"metadata": sample.metadata ?? [:]
"metadata": sample.metadata ?? [:],
"isManualMeasurement": self?.isManualHRVMeasurement(sourceBundle: sourceBundle, metadata: sample.metadata) ?? false,
"qualityScore": self?.calculateHRVQualityScore(value: hrvValueMs, sourceBundle: sourceBundle, metadata: sample.metadata) ?? 0
] as [String : Any]
}
// HRV
let bestQualityValue = self?.getBestQualityHRVValue(from: hrvData)
let result: [String: Any] = [
"data": hrvData,
"count": hrvData.count,
"bestQualityValue": bestQualityValue,
"startDate": self?.dateToISOString(startDate) ?? "",
"endDate": self?.dateToISOString(endDate) ?? ""
]
@@ -1031,6 +1043,130 @@ class HealthKitManager: NSObject, RCTBridgeModule {
return formatter.string(from: date)
}
// MARK: - HRV Quality Analysis Methods
/// /HRV
private func isManualHRVMeasurement(sourceBundle: String, metadata: [String: Any]?) -> Bool {
//
if sourceBundle.contains("Breathe") || sourceBundle.contains("breathe") {
return true
}
// HRV
let manualHRVApps = ["HRV4Training", "EliteHRV", "HRVLogger", "Stress & Anxiety Companion"]
if manualHRVApps.contains(where: { sourceBundle.contains($0) }) {
return true
}
//
if let metadata = metadata {
if let wasUserEntered = metadata[HKMetadataKeyWasUserEntered] as? Bool, wasUserEntered {
return true
}
}
return false
}
/// HRV
private func calculateHRVQualityScore(value: Double, sourceBundle: String, metadata: [String: Any]?) -> Int {
var score = 0
// 1. (0-40)
if value >= 10 && value <= 100 {
// SDNN
if value >= 18 && value <= 76 {
score += 40 //
} else if value >= 10 && value <= 18 {
score += 30 //
} else if value >= 76 && value <= 100 {
score += 35 //
}
} else if value > 0 && value < 10 {
score += 10 //
}
// 2. (0-35)
if isManualHRVMeasurement(sourceBundle: sourceBundle, metadata: metadata) {
score += 35 //
} else if sourceBundle.contains("com.apple.health") {
score += 20 //
} else if sourceBundle.contains("Watch") {
score += 25 // Apple Watch
} else {
score += 15 //
}
// 3. (0-25)
if let metadata = metadata {
var metadataScore = 0
//
if let wasUserEntered = metadata[HKMetadataKeyWasUserEntered] as? Bool, wasUserEntered {
metadataScore += 15
}
//
if metadata[HKMetadataKeyDeviceName] != nil {
metadataScore += 5
}
//
if metadata[HKMetadataKeyHeartRateMotionContext] != nil {
metadataScore += 5
}
score += metadataScore
}
return min(score, 100) // 100
}
/// HRV
private func getBestQualityHRVValue(from hrvData: [[String: Any]]) -> Double? {
guard !hrvData.isEmpty else { return nil }
//
let sortedData = hrvData.sorted { item1, item2 in
let quality1 = item1["qualityScore"] as? Int ?? 0
let quality2 = item2["qualityScore"] as? Int ?? 0
let isManual1 = item1["isManualMeasurement"] as? Bool ?? false
let isManual2 = item2["isManualMeasurement"] as? Bool ?? false
// > >
if isManual1 && !isManual2 {
return true
} else if !isManual1 && isManual2 {
return false
} else if quality1 != quality2 {
return quality1 > quality2
} else {
//
let date1 = item1["endDate"] as? String ?? ""
let date2 = item2["endDate"] as? String ?? ""
return date1 > date2
}
}
//
if let bestValue = sortedData.first?["value"] as? Double {
//
if bestValue >= 5 && bestValue <= 150 {
return bestValue
}
}
//
for data in sortedData {
if let value = data["value"] as? Double, value >= 10 && value <= 100 {
return value
}
}
//
return sortedData.first?["value"] as? Double
}
// MARK: - Hourly Data Methods
@objc