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

@@ -1,5 +1,4 @@
import { fetchHRVForDate } from '@/utils/health';
import dayjs from 'dayjs';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useState } from 'react';
@@ -11,26 +10,6 @@ interface StressMeterProps {
}
export function StressMeter({ curDate }: StressMeterProps) {
// 格式化更新时间显示
const formatUpdateTime = (date: Date): string => {
const now = dayjs();
const updateTime = dayjs(date);
const diffMinutes = now.diff(updateTime, 'minute');
const diffHours = now.diff(updateTime, 'hour');
const diffDays = now.diff(updateTime, 'day');
if (diffMinutes < 1) {
return '刚刚更新';
} else if (diffMinutes < 60) {
return `${diffMinutes}分钟前更新`;
} else if (diffHours < 24) {
return `${diffHours}小时前更新`;
} else if (diffDays < 7) {
return `${diffDays}天前更新`;
} else {
return updateTime.format('MM-DD HH:mm');
}
};
// 将HRV值转换为压力指数0-100
// HRV值范围30-110ms映射到压力指数100-0
@@ -58,7 +37,7 @@ export function StressMeter({ curDate }: StressMeterProps) {
const data = await fetchHRVForDate(curDate)
if (data) {
setHrvValue(data)
setHrvValue(Math.round(data.value))
}
} catch (error) {
@@ -138,7 +117,7 @@ export function StressMeter({ curDate }: StressMeterProps) {
visible={showStressModal}
onClose={() => setShowStressModal(false)}
hrvValue={hrvValue}
// updateTime={updateTime || new Date()}
updateTime={new Date()}
/>
</>
);

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

View File

@@ -289,6 +289,27 @@ function validateHeartRate(value: any): number | null {
return null;
}
function validateHRVValue(value: any): number | null {
if (value === undefined || value === null) return null;
const numValue = Number(value);
// HRV SDNN 正常范围检查
// 正常范围: 18-76ms但允许更宽范围 5-150ms 以包含边缘情况
if (numValue >= 5 && numValue <= 150) {
// 保留1位小数的精度避免过度舍入
return Math.round(numValue * 10) / 10;
}
// 记录异常值用于调试
console.warn('HRV数据超出合理范围:', {
value: numValue,
expectedRange: '5-150ms',
normalRange: '18-76ms'
});
return null;
}
// 健康数据获取函数
export async function fetchStepCount(date: Date): Promise<number> {
try {
@@ -487,7 +508,7 @@ export async function fetchBasalEnergyBurned(options: HealthDataOptions): Promis
}
}
async function fetchHeartRateVariability(options: HealthDataOptions): Promise<number | null> {
async function fetchHeartRateVariability(options: HealthDataOptions): Promise<HRVData | null> {
try {
console.log('=== 开始获取HRV数据 ===');
console.log('查询选项:', options);
@@ -496,14 +517,67 @@ async function fetchHeartRateVariability(options: HealthDataOptions): Promise<nu
console.log('HRV API调用结果:', result);
if (result && result.data && Array.isArray(result.data) && result.data.length > 0) {
const hrvValue = result.data[0].value;
logSuccess('HRV数据', result);
return Math.round(hrvValue); // Value already in ms from native
} else {
logWarning('HRV', '为空或格式错误');
console.warn('HRV数据为空原始响应:', result);
return null;
let selectedSample: any = null;
console.log('result~~~', result);
// 优先使用优化后的最佳质量值对应的样本
if (result.bestQualityValue && typeof result.bestQualityValue === 'number') {
const qualityValue = validateHRVValue(result.bestQualityValue);
if (qualityValue !== null) {
// 找到对应的最佳质量样本
selectedSample = result.data[result.data.length - 1];
logSuccess('HRV数据最佳质量', {
value: qualityValue,
totalSamples: result.data.length,
recordedAt: selectedSample.endDate
});
}
}
// 如果没有找到最佳质量样本,使用第一个有效样本
if (!selectedSample) {
for (const sample of result.data) {
const sampleValue = validateHRVValue(sample.value);
if (sampleValue !== null) {
selectedSample = sample;
console.log('使用有效HRV样本:', {
value: sampleValue,
source: sample.source?.name,
recordedAt: sample.endDate
});
break;
}
}
}
// 构建完整的HRV数据对象
if (selectedSample) {
const validatedValue = validateHRVValue(selectedSample.value);
if (validatedValue !== null) {
const hrvData: HRVData = {
value: validatedValue,
recordedAt: selectedSample.startDate,
endDate: selectedSample.endDate,
source: {
name: selectedSample.source?.name || 'Unknown',
bundleIdentifier: selectedSample.source?.bundleIdentifier || ''
},
isManualMeasurement: selectedSample.isManualMeasurement || false,
qualityScore: selectedSample.qualityScore,
sampleId: selectedSample.id
};
logSuccess('HRV完整数据', hrvData);
return hrvData;
}
}
}
logWarning('HRV', '为空或格式错误');
console.warn('HRV数据为空或无效原始响应:', result);
return null;
} catch (error) {
logError('HRV数据', error);
console.error('HRV获取错误详情:', error);
@@ -657,18 +731,18 @@ export async function fetchTodayHealthData(): Promise<TodayHealthData> {
return fetchHealthDataForDate(dayjs().toDate());
}
export async function fetchHRVForDate(date: Date): Promise<number | null> {
export async function fetchHRVForDate(date: Date): Promise<HRVData | null> {
console.log('开始获取指定日期HRV数据...', date);
const options = createDateRange(date);
return fetchHeartRateVariability(options);
}
export async function fetchTodayHRV(): Promise<number | null> {
export async function fetchTodayHRV(): Promise<HRVData | null> {
return fetchHRVForDate(dayjs().toDate());
}
// 获取最近几小时内的实时HRV数据
export async function fetchRecentHRV(hoursBack: number = 2): Promise<number | null> {
export async function fetchRecentHRV(hoursBack: number = 2): Promise<HRVData | null> {
console.log(`开始获取最近${hoursBack}小时内的HRV数据...`);
const now = new Date();
@@ -697,18 +771,63 @@ export async function testHRVDataFetch(date: Date = dayjs().toDate()): Promise<v
// 测试不同时间范围的HRV数据
const options = createDateRange(date);
// 获取今日HRV
// 获取今日HRV(带详细分析)
console.log('--- 测试今日HRV ---');
const result = await HealthKitManager.getHeartRateVariabilitySamples(options);
console.log('原始HRV API响应:', result);
if (result && result.data && Array.isArray(result.data)) {
console.log(`获取到 ${result.data.length} 个HRV样本`);
// 分析数据质量
result.data.forEach((sample: any, index: number) => {
console.log(`样本 ${index + 1}:`, {
value: sample.value,
source: sample.source?.name,
bundleId: sample.source?.bundleIdentifier,
isManual: sample.isManualMeasurement,
qualityScore: sample.qualityScore,
startDate: sample.startDate,
endDate: sample.endDate
});
});
if (result.bestQualityValue !== undefined) {
console.log('最佳质量HRV值:', result.bestQualityValue);
}
}
// 使用优化后的方法获取HRV
const todayHRV = await fetchHeartRateVariability(options);
console.log('今日HRV结果:', todayHRV);
console.log('最终HRV结果:', todayHRV);
// 获取最近2小时HRV
console.log('--- 测试最近2小时HRV ---');
const recentHRV = await fetchRecentHRV(2);
console.log('最近2小时HRV结果:', recentHRV);
// 获取指定日期HRV
console.log('--- 测试指定日期HRV ---');
const dateHRV = await fetchHRVForDate(date);
console.log('指定日期HRV结果:', dateHRV);
// 提供数据解释
if (todayHRV) {
console.log('--- HRV数据解读 ---');
console.log(`HRV值: ${todayHRV.value}ms`);
console.log(`记录时间: ${todayHRV.recordedAt}`);
console.log(`数据来源: ${todayHRV.source.name}`);
console.log(`手动测量: ${todayHRV.isManualMeasurement ? '是' : '否'}`);
if (todayHRV.value >= 18 && todayHRV.value <= 76) {
console.log('✅ HRV值在正常范围内 (18-76ms)');
} else if (todayHRV.value < 18) {
console.log('⚠️ HRV值偏低可能表示压力或疲劳状态');
} else if (todayHRV.value > 76) {
console.log('📈 HRV值较高通常表示良好的恢复状态');
}
}
console.log('=== HRV数据测试完成 ===');
} catch (error) {
console.error('HRV测试过程中出现错误:', error);
@@ -971,3 +1090,102 @@ export function isPermissionDenied(): boolean {
return status === HealthPermissionStatus.Denied;
}
// HRV数据结构
export interface HRVData {
value: number;
recordedAt: string; // ISO string format
endDate: string; // ISO string format
source: {
name: string;
bundleIdentifier: string;
};
isManualMeasurement: boolean;
qualityScore?: number;
sampleId?: string;
}
// HRV数据质量分析和解读
export interface HRVAnalysis {
value: number;
quality: 'excellent' | 'good' | 'fair' | 'poor';
interpretation: string;
recommendations: string[];
dataSource: string;
isManualMeasurement: boolean;
recordedAt: string;
}
export function analyzeHRVData(hrvData: HRVData): HRVAnalysis {
const { value: hrvValue, source, isManualMeasurement, recordedAt } = hrvData;
const sourceName = source.name;
let quality: HRVAnalysis['quality'];
let interpretation: string;
let recommendations: string[] = [];
// 质量评估基于数值范围和数据来源
if (hrvValue >= 18 && hrvValue <= 76) {
if (isManualMeasurement) {
quality = 'excellent';
interpretation = 'HRV值在正常范围内且来自高质量测量';
} else {
quality = 'good';
interpretation = 'HRV值在正常范围内';
}
} else if (hrvValue >= 10 && hrvValue < 18) {
quality = 'fair';
interpretation = 'HRV值偏低可能表示压力、疲劳或恢复不足';
recommendations.push('考虑增加休息和恢复时间');
recommendations.push('评估近期的压力水平和睡眠质量');
} else if (hrvValue > 76 && hrvValue <= 100) {
quality = isManualMeasurement ? 'excellent' : 'good';
interpretation = 'HRV值较高通常表示良好的心血管健康和恢复状态';
recommendations.push('保持当前的生活方式和训练强度');
} else if (hrvValue < 10) {
quality = 'poor';
interpretation = 'HRV值异常低建议关注身体状态或数据准确性';
recommendations.push('建议使用手动测量(如呼吸应用)获得更准确的数据');
recommendations.push('如持续偏低,建议咨询医疗专业人士');
} else if (hrvValue > 100) {
quality = 'fair';
interpretation = 'HRV值异常高可能需要验证数据准确性';
recommendations.push('建议重复测量确认数据准确性');
} else {
quality = 'poor';
interpretation = 'HRV数据超出预期范围';
recommendations.push('建议使用标准化的测量方法');
}
// 根据数据来源添加建议
if (!isManualMeasurement) {
recommendations.push('推荐使用呼吸应用进行手动HRV测量以获得更准确的数据');
}
return {
value: hrvValue,
quality,
interpretation,
recommendations,
dataSource: sourceName,
isManualMeasurement,
recordedAt
};
}
// 获取HRV数据并提供分析
export async function fetchHRVWithAnalysis(date: Date): Promise<{ hrvData: HRVData | null; analysis: HRVAnalysis | null }> {
try {
const hrvData = await fetchHRVForDate(date);
if (hrvData) {
const analysis = analyzeHRVData(hrvData);
return { hrvData, analysis };
}
return { hrvData: null, analysis: null };
} catch (error) {
console.error('获取HRV分析数据失败:', error);
return { hrvData: null, analysis: null };
}
}