- 新增 HRV 监听服务,实时监控心率变异性数据 - 实现 HRV 到压力指数的转换算法和压力等级评估 - 添加智能通知服务,在压力偏高时推送健康建议 - 优化日志系统,修复日志丢失问题并增强刷新机制 - 改进个人页面下拉刷新,支持并行数据加载 - 优化勋章数据缓存策略,减少不必要的网络请求 - 重构应用初始化流程,优化权限服务和健康监听服务的启动顺序 - 移除冗余日志输出,提升应用性能
174 lines
5.3 KiB
TypeScript
174 lines
5.3 KiB
TypeScript
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();
|