feat(hrv): 添加心率变异性监控和压力评估功能
- 新增 HRV 监听服务,实时监控心率变异性数据 - 实现 HRV 到压力指数的转换算法和压力等级评估 - 添加智能通知服务,在压力偏高时推送健康建议 - 优化日志系统,修复日志丢失问题并增强刷新机制 - 改进个人页面下拉刷新,支持并行数据加载 - 优化勋章数据缓存策略,减少不必要的网络请求 - 重构应用初始化流程,优化权限服务和健康监听服务的启动顺序 - 移除冗余日志输出,提升应用性能
This commit is contained in:
173
services/hrvMonitor.ts
Normal file
173
services/hrvMonitor.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { logger } from '@/utils/logger';
|
||||
import AsyncStorage from '@/utils/kvStore';
|
||||
import dayjs from 'dayjs';
|
||||
import { NativeEventEmitter, NativeModules } from 'react-native';
|
||||
import { analyzeHRVData, fetchHRVWithStatus } from '@/utils/health';
|
||||
import { convertHrvToStressIndex, getStressLevelInfo, StressLevel } from '@/utils/stress';
|
||||
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 = 4;
|
||||
|
||||
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;
|
||||
|
||||
if (elapsed < minIntervalMs) {
|
||||
const hoursLeft = ((minIntervalMs - elapsed) / (1000 * 60 * 60)).toFixed(1);
|
||||
logger.info(`[HRVMonitor] Cooldown active, ${hoursLeft}h remaining`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const lastSentDay = dayjs(state.lastSentAt).format('YYYY-MM-DD');
|
||||
const today = dayjs().format('YYYY-MM-DD');
|
||||
if (lastSentDay === today && state.lastStressLevel !== 'high') {
|
||||
logger.info('[HRVMonitor] Already sent HRV notification today');
|
||||
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();
|
||||
Reference in New Issue
Block a user