Files
digital-pilates/services/hrvMonitor.ts
richarjiang 5e11da34ee feat(app): 新增HRV压力提醒设置与锻炼记录分享功能
- 通知设置页面新增 HRV 压力提醒开关,支持自定义开启或关闭压力监测推送
- 锻炼详情页集成分享功能,支持将运动数据生成精美长图并分享
- 优化 HRV 监测服务逻辑,在发送通知前检查用户偏好设置
- 更新多语言配置文件,添加相关文案翻译
- 将应用版本号更新至 1.1.5
2025-12-16 11:27:11 +08:00

178 lines
5.5 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 { getHRVReminderEnabled, getNotificationEnabled } from '@/utils/userPreferences';
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 [notificationsEnabled, hrvReminderEnabled] = await Promise.all([
getNotificationEnabled(),
getHRVReminderEnabled(),
]);
if (!notificationsEnabled || !hrvReminderEnabled) {
logger.info('[HRVMonitor] Notification preference disabled, skip HRV push');
return;
}
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();