Files
digital-pilates/services/hrvMonitor.ts
richarjiang f43cfe7ac6 fix(ios): 修复HealthKit类型安全性并优化HRV通知频率
将HealthKit数据类型从强制解包改为可选类型,避免潜在的运行时崩溃。所有数据类型访问现在都通过guard语句进行安全检查,当类型不可用时返回明确的错误信息。同时修复了活动摘要日期计算错误,确保每个摘要使用正确的日期。

HRV压力通知的最小间隔从4小时缩短至2小时,并移除了每日一次的限制,允许更及时的压力状态提醒。

BREAKING CHANGE: HealthKit数据类型API现在可能返回"TYPE_NOT_AVAILABLE"错误,调用方需要处理此新错误类型
2025-11-19 09:23:42 +08:00

167 lines
5.1 KiB
TypeScript
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.

import { analyzeHRVData, fetchHRVWithStatus } from '@/utils/health';
import AsyncStorage from '@/utils/kvStore';
import { logger } from '@/utils/logger';
import { convertHrvToStressIndex, getStressLevelInfo, StressLevel } from '@/utils/stress';
import { NativeEventEmitter, NativeModules } from 'react-native';
import { sendHRVStressNotification } from './hrvNotificationService';
const { HealthKitManager } = NativeModules;
const HRV_EVENT_NAME = 'hrvUpdate';
const HRV_NOTIFICATION_STATE_KEY = '@hrv_stress_notification_state';
const MIN_NOTIFICATION_INTERVAL_HOURS = 2;
interface HrvEventData {
timestamp: number;
type: 'hrv_data_updated' | string;
}
interface NotificationState {
lastSentAt: number;
lastStressLevel: StressLevel;
}
class HRVMonitorService {
private eventEmitter: NativeEventEmitter | null = null;
private eventSubscription: any = null;
private isInitialized = false;
private lastProcessedTime = 0;
private debounceDelay = 3000;
async initialize(): Promise<boolean> {
if (this.isInitialized) {
logger.info('[HRVMonitor] Already initialized');
return true;
}
try {
this.eventEmitter = new NativeEventEmitter(HealthKitManager);
this.eventSubscription = this.eventEmitter.addListener(
HRV_EVENT_NAME,
this.handleHRVUpdate.bind(this)
);
if (HealthKitManager.startHRVObserver) {
await HealthKitManager.startHRVObserver();
} else {
logger.warn('[HRVMonitor] Native startHRVObserver not available');
return false;
}
this.isInitialized = true;
logger.info('[HRVMonitor] Initialized successfully');
return true;
} catch (error) {
logger.error('[HRVMonitor] Initialization failed:', error);
return false;
}
}
async stop(): Promise<void> {
if (!this.isInitialized) return;
try {
if (this.eventSubscription) {
this.eventSubscription.remove();
this.eventSubscription = null;
}
if (HealthKitManager.stopHRVObserver) {
await HealthKitManager.stopHRVObserver();
}
this.isInitialized = false;
logger.info('[HRVMonitor] Stopped successfully');
} catch (error) {
logger.error('[HRVMonitor] Failed to stop:', error);
}
}
private async handleHRVUpdate(event: HrvEventData): Promise<void> {
logger.info('[HRVMonitor] HRV data updated:', event);
const now = Date.now();
if (now - this.lastProcessedTime < this.debounceDelay) {
logger.info('[HRVMonitor] Debouncing HRV update');
return;
}
this.lastProcessedTime = now;
try {
const canNotify = await this.canSendNotification(now);
if (!canNotify) {
logger.info('[HRVMonitor] Notification recently sent, skipping');
return;
}
const { hrvData, message } = await fetchHRVWithStatus(new Date());
if (!hrvData) {
logger.warn('[HRVMonitor] No HRV data available for analysis');
return;
}
const analysis = analyzeHRVData(hrvData);
const stressIndex = convertHrvToStressIndex(hrvData.value);
if (stressIndex == null) {
logger.warn('[HRVMonitor] Unable to derive stress index from HRV value:', hrvData.value);
return;
}
const stressInfo = getStressLevelInfo(stressIndex);
await sendHRVStressNotification({
hrvValue: Math.round(hrvData.value),
stressIndex,
stressLevel: stressInfo.level,
recordedAt: hrvData.recordedAt,
interpretation: analysis.interpretation,
recommendations: analysis.recommendations,
dataSource: analysis.dataSource,
message,
});
await this.markNotificationSent(now, stressInfo.level);
} catch (error) {
logger.error('[HRVMonitor] Failed to process HRV update:', error);
}
}
private async canSendNotification(now: number): Promise<boolean> {
try {
const stored = await AsyncStorage.getItem(HRV_NOTIFICATION_STATE_KEY);
if (!stored) {
return true;
}
const state: NotificationState = JSON.parse(stored);
const elapsed = now - state.lastSentAt;
const minIntervalMs = MIN_NOTIFICATION_INTERVAL_HOURS * 60 * 60 * 1000;
// 只检查最小间隔时间2小时不再限制每天只能发送一次
if (elapsed < minIntervalMs) {
const hoursLeft = ((minIntervalMs - elapsed) / (1000 * 60 * 60)).toFixed(1);
logger.info(`[HRVMonitor] Cooldown active, ${hoursLeft}h remaining`);
return false;
}
return true;
} catch (error) {
logger.warn('[HRVMonitor] Failed to read notification state:', error);
return true;
}
}
private async markNotificationSent(timestamp: number, level: StressLevel): Promise<void> {
try {
const state: NotificationState = {
lastSentAt: timestamp,
lastStressLevel: level,
};
await AsyncStorage.setItem(HRV_NOTIFICATION_STATE_KEY, JSON.stringify(state));
} catch (error) {
logger.warn('[HRVMonitor] Failed to persist notification state:', error);
}
}
}
export const hrvMonitorService = new HRVMonitorService();