将HealthKit数据类型从强制解包改为可选类型,避免潜在的运行时崩溃。所有数据类型访问现在都通过guard语句进行安全检查,当类型不可用时返回明确的错误信息。同时修复了活动摘要日期计算错误,确保每个摘要使用正确的日期。 HRV压力通知的最小间隔从4小时缩短至2小时,并移除了每日一次的限制,允许更及时的压力状态提醒。 BREAKING CHANGE: HealthKit数据类型API现在可能返回"TYPE_NOT_AVAILABLE"错误,调用方需要处理此新错误类型
167 lines
5.1 KiB
TypeScript
167 lines
5.1 KiB
TypeScript
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();
|