diff --git a/app/_layout.tsx b/app/_layout.tsx index c61d799..a31eaf3 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,10 +1,10 @@ +import '@/i18n'; import { DefaultTheme, ThemeProvider } from '@react-navigation/native'; import { useFonts } from 'expo-font'; import { Stack } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import 'react-native-reanimated'; -import '@/i18n'; import PrivacyConsentModal from '@/components/PrivacyConsentModal'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; @@ -12,6 +12,7 @@ import { useQuickActions } from '@/hooks/useQuickActions'; import { clearAiCoachSessionCache } from '@/services/aiCoachSession'; import { notificationService } from '@/services/notifications'; import { setupQuickActions } from '@/services/quickActions'; +import { sleepMonitorService } from '@/services/sleepMonitor'; import { initializeWaterRecordBridge } from '@/services/waterRecordBridge'; import { WaterRecordSource } from '@/services/waterRecords'; import { workoutMonitorService } from '@/services/workoutMonitor'; @@ -94,181 +95,276 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { }, [isLoggedIn]); React.useEffect(() => { - const loadUserData = async () => { - // 数据已经在启动界面预加载,这里只需要快速同步到 Redux 状态 - await dispatch(fetchMyProfile()); + // ==================== 第一优先级:立即执行(关键路径,0-500ms)==================== + const initializeCriticalServices = async () => { + try { + logger.info('🚀 开始初始化关键服务...'); + + // 1. 加载用户数据(首屏展示需要) + await dispatch(fetchMyProfile()); + logger.info('✅ 用户数据加载完成'); + + // 2. 初始化 HealthKit 权限系统(不请求权限,仅初始化) + initializeHealthPermissions(); + logger.info('✅ HealthKit 权限系统初始化完成'); + + // 3. 初始化通知服务基础功能 + await notificationService.initialize(); + logger.info('✅ 通知服务初始化完成'); + + // 4. 初始化快捷动作(用户可能立即使用) + await setupQuickActions(); + logger.info('✅ 快捷动作初始化完成'); + + // 5. 清空 AI 教练会话缓存(轻量操作) + clearAiCoachSessionCache(); + logger.info('✅ AI 教练缓存清理完成'); + + logger.info('🎉 关键服务初始化完成'); + } catch (error) { + logger.error('❌ 关键服务初始化失败:', error); + } }; - const initHealthPermissions = async () => { - // 初始化 HealthKit 权限管理系统 - try { - logger.info('初始化 HealthKit 权限管理系统...'); - initializeHealthPermissions(); + // ==================== 第二优先级:短延迟(1-2秒后执行)==================== + const initializeSecondaryServices = () => { + setTimeout(async () => { + try { + logger.info('⏰ 开始初始化次要服务...'); - // 延迟请求权限,避免应用启动时弹窗 + // 1. 请求 HealthKit 权限(延迟到 3 秒,避免打断用户浏览) + setTimeout(async () => { + try { + await ensureHealthPermissions(); + logger.info('✅ HealthKit 权限请求完成'); + } catch (error) { + logger.warn('⚠️ HealthKit 权限请求失败,可能在模拟器上运行:', error); + } + }, 3000); + + // 2. 初始化喝水记录 Bridge + initializeWaterRecordBridge(); + logger.info('✅ 喝水记录 Bridge 初始化完成'); + + // 3. 异步同步 Widget 数据(不阻塞主流程) + syncWidgetDataInBackground(); + + logger.info('🎉 次要服务初始化完成'); + } catch (error) { + logger.error('❌ 次要服务初始化失败:', error); + } + }, 2000); + }; + + // ==================== 第三优先级:中延迟(3-5秒后或交互完成后执行)==================== + const initializeBackgroundServices = () => { + // 使用 InteractionManager 确保在所有交互和动画完成后再执行 + const { InteractionManager } = require('react-native'); + + InteractionManager.runAfterInteractions(() => { setTimeout(async () => { try { - await ensureHealthPermissions(); - logger.info('HealthKit 权限请求完成'); + logger.info('📅 开始初始化后台服务...'); + + // 1. 批量注册所有通知提醒 + await registerAllNotifications(); + + // 2. 初始化后台任务管理器 + await initializeBackgroundTaskManager(); + + // 3. 初始化健康监听服务 + await initializeHealthMonitoring(); + + logger.info('🎉 后台服务初始化完成'); } catch (error) { - logger.warn('HealthKit 权限请求失败,可能在模拟器上运行:', error); + logger.error('❌ 后台服务初始化失败:', error); } - }, 2000); + }, 3000); + }); + }; - logger.info('HealthKit 权限管理初始化完成'); - } catch (error) { - logger.warn('HealthKit 权限管理初始化失败:', error); - } - } - - const initializeNotifications = async () => { - try { + // ==================== 第四优先级:空闲时执行(不影响用户体验)==================== + const initializeIdleServices = () => { + setTimeout(async () => { try { - logger.info('初始化后台任务管理器...'); + logger.info('🔄 开始初始化空闲服务...'); - await BackgroundTaskManager.getInstance().initialize(); + // 1. 后台任务详细状态检查 + await checkBackgroundTaskStatus(); - logger.info('后台任务管理器初始化成功'); - - // 检查后台任务状态并恢复 - await checkAndRestoreBackgroundTasks(); - - // 在开发环境中初始化调试工具 - if (__DEV__) { + // 2. 开发环境调试工具 + if (__DEV__ && BackgroundTaskDebugger) { BackgroundTaskDebugger.getInstance().initialize(); - logger.info('后台任务调试工具已初始化(开发环境)'); + logger.info('✅ 后台任务调试工具已初始化(开发环境)'); } - } catch (backgroundError) { - logger.error('后台任务管理器初始化失败,将跳过后台任务:', backgroundError); - } - // 初始化通知服务 - await notificationService.initialize(); - logger.info('通知服务初始化成功'); - - // 注册午餐提醒(12:00) - await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || ''); - logger.info('午餐提醒已注册'); - - // 注册晚餐提醒(18:00) - await NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || ''); - logger.info('晚餐提醒已注册'); - - // 注册心情提醒(21:00) - await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || ''); - logger.info('心情提醒已注册'); - - // 注册默认喝水提醒(9:00-21:00,每2小时一次) - await WaterNotificationHelpers.scheduleRegularWaterReminders(profile.name || '用户'); - logger.info('默认喝水提醒已注册'); - - // 安排断食通知(如果存在活跃的断食计划) - try { - const fastingSchedule = store.getState().fasting.activeSchedule; - if (fastingSchedule) { - const fastingPlan = store.getState().fasting.activeSchedule ? null : null; - // 断食通知将通过 useFastingNotifications hook 在页面加载时自动安排 - logger.info('检测到活跃的断食计划,将通过页面 hook 自动安排通知'); - } + logger.info('🎉 空闲服务初始化完成'); } catch (error) { - logger.warn('安排断食通知失败:', error); + logger.error('❌ 空闲服务初始化失败:', error); } + }, 8000); // 8秒后执行,确保不影响用户体验 + }; - // 初始化快捷动作 - await setupQuickActions(); - logger.info('快捷动作初始化成功'); + // ==================== 辅助函数 ==================== - // 初始化喝水记录 bridge - initializeWaterRecordBridge(); - logger.info('喝水记录 Bridge 初始化成功'); - - // 初始化锻炼监听服务 - const initializeWorkoutMonitoring = async () => { - try { - await workoutMonitorService.initialize(); - logger.info('锻炼监听服务初始化成功'); - } catch (error) { - logger.warn('锻炼监听服务初始化失败:', error); - } - }; - - initializeWorkoutMonitoring(); - - // 检查并同步Widget数据更改 + // 异步同步 Widget 数据(不阻塞主流程) + const syncWidgetDataInBackground = async () => { + try { const widgetSync = await syncPendingWidgetChanges(); if (widgetSync.hasPendingChanges && widgetSync.pendingRecords) { - logger.info(`检测到 ${widgetSync.pendingRecords.length} 条待同步的水记录`); + logger.info(`🔄 检测到 ${widgetSync.pendingRecords.length} 条待同步的水记录`); - // 将待同步的记录添加到 Redux store + // 异步处理每条记录 for (const record of widgetSync.pendingRecords) { try { await store.dispatch(createWaterRecordAction({ amount: record.amount, recordedAt: record.recordedAt, - source: WaterRecordSource.Auto, // 标记为自动添加(来自Widget) + source: WaterRecordSource.Auto, })).unwrap(); - logger.info(`成功同步水记录: ${record.amount}ml at ${record.recordedAt}`); + logger.info(`✅ 成功同步水记录: ${record.amount}ml at ${record.recordedAt}`); } catch (error) { - logger.error('同步水记录失败:', error); + logger.error('❌ 同步水记录失败:', error); } } // 清除已同步的记录 await clearPendingWaterRecords(); - logger.info('所有待同步的水记录已处理完成'); + logger.info('✅ 所有待同步的水记录已处理完成'); } } catch (error) { - logger.error('通知服务、后台任务管理器或快捷动作初始化失败:', error); + logger.error('❌ Widget 数据同步失败:', error); } }; - // 检查并恢复后台任务 - const checkAndRestoreBackgroundTasks = async () => { + // 批量注册所有通知提醒 + const registerAllNotifications = async () => { try { + logger.info('📢 开始批量注册通知提醒...'); + + // 并行注册所有通知,提高效率 + await Promise.all([ + // 营养提醒 + NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '').then(() => + logger.info('✅ 午餐提醒已注册') + ), + NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '').then(() => + logger.info('✅ 晚餐提醒已注册') + ), + + // 心情提醒 + MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '').then(() => + logger.info('✅ 心情提醒已注册') + ), + + // 喝水提醒 + WaterNotificationHelpers.scheduleRegularWaterReminders(profile.name || '用户').then(() => + logger.info('✅ 喝水提醒已注册') + ), + ]); + + // 检查断食通知(如果有活跃计划) + const fastingSchedule = store.getState().fasting.activeSchedule; + if (fastingSchedule) { + logger.info('✅ 检测到活跃的断食计划,将通过页面 hook 自动安排通知'); + } + + logger.info('🎉 所有通知提醒注册完成'); + } catch (error) { + logger.error('❌ 通知提醒注册失败:', error); + } + }; + + // 初始化后台任务管理器 + const initializeBackgroundTaskManager = async () => { + try { + logger.info('⚙️ 初始化后台任务管理器...'); + + await BackgroundTaskManager.getInstance().initialize(); + logger.info('✅ 后台任务管理器初始化成功'); + + // 简单的任务调度检查 + const taskManager = BackgroundTaskManager.getInstance(); + const status = await taskManager.getStatus(); + + if (status === 'available') { + const pendingRequests = await taskManager.getPendingRequests(); + if (pendingRequests.length === 0) { + await taskManager.scheduleNextTask(); + logger.info('✅ 已调度新的后台任务'); + } + } + } catch (error) { + logger.error('❌ 后台任务管理器初始化失败:', error); + } + }; + + // 初始化健康监听服务(锻炼 + 睡眠) + const initializeHealthMonitoring = async () => { + try { + logger.info('💪 初始化健康监听服务...'); + + // 并行初始化锻炼和睡眠监听 + await Promise.allSettled([ + workoutMonitorService.initialize().then(() => + logger.info('✅ 锻炼监听服务初始化成功') + ), + sleepMonitorService.initialize().then(() => + logger.info('✅ 睡眠监听服务初始化成功') + ), + ]); + + logger.info('🎉 健康监听服务初始化完成'); + } catch (error) { + logger.error('❌ 健康监听服务初始化失败:', error); + } + }; + + // 后台任务详细状态检查(空闲时执行) + const checkBackgroundTaskStatus = async () => { + try { + logger.info('🔍 检查后台任务详细状态...'); + const taskManager = BackgroundTaskManager.getInstance(); const status = await taskManager.getStatus(); const statusText = await taskManager.checkStatus(); - logger.info(`后台任务状态检查: ${status} (${statusText})`); + logger.info(`📊 后台任务状态: ${status} (${statusText})`); - // 检查是否有待处理的任务请求 - const pendingRequests = await taskManager.getPendingRequests(); - logger.info(`当前待处理的任务请求数量: ${pendingRequests.length}`); - - // 如果没有待处理的任务请求,且状态可用,则调度一个新任务 - if (pendingRequests.length === 0 && status === 'available') { - logger.info('没有待处理的任务请求,调度新的后台任务'); - await taskManager.scheduleNextTask(); - } - - // 检查上次后台任务执行时间 + // 检查上次执行时间 const lastCheckTime = await taskManager.getLastBackgroundCheckTime(); if (lastCheckTime) { const timeSinceLastCheck = Date.now() - lastCheckTime; const hoursSinceLastCheck = timeSinceLastCheck / (1000 * 60 * 60); - logger.info(`上次后台任务执行时间: ${new Date(lastCheckTime).toLocaleString()} (${hoursSinceLastCheck.toFixed(1)}小时前)`); + logger.info(`⏱️ 上次执行: ${new Date(lastCheckTime).toLocaleString()} (${hoursSinceLastCheck.toFixed(1)}小时前)`); - // 如果超过24小时没有执行后台任务,可能需要手动触发一次 if (hoursSinceLastCheck > 24) { - logger.warn('超过24小时未执行后台任务,可能需要检查系统设置'); + logger.warn('⚠️ 超过24小时未执行后台任务,请检查系统设置'); } } + logger.info('✅ 后台任务状态检查完成'); } catch (error) { - logger.error('检查后台任务状态失败:', error); + logger.error('❌ 后台任务状态检查失败:', error); } }; - loadUserData(); - initHealthPermissions(); - initializeNotifications(); + // ==================== 执行初始化流程 ==================== + + // 立即执行关键服务 + initializeCriticalServices(); + + // 延迟执行次要服务 + initializeSecondaryServices(); + + // 交互完成后执行后台服务 + initializeBackgroundServices(); + + // 空闲时执行非关键服务 + initializeIdleServices(); - - // 冷启动时清空 AI 教练会话缓存 - clearAiCoachSessionCache(); - - }, [dispatch]); + }, [dispatch, profile.name]); React.useEffect(() => { diff --git a/app/medications/add-medication.tsx b/app/medications/add-medication.tsx index c3c17b7..6ff72c2 100644 --- a/app/medications/add-medication.tsx +++ b/app/medications/add-medication.tsx @@ -163,6 +163,10 @@ export default function AddMedicationScreen() { const [note, setNote] = useState(''); const [dictationActive, setDictationActive] = useState(false); const [dictationLoading, setDictationLoading] = useState(false); + // 临时存储当前语音识别的结果,用于实时预览 + const [currentDictationText, setCurrentDictationText] = useState(''); + // 记录语音识别开始前的文本,用于取消时恢复 + const [noteBeforeDictation, setNoteBeforeDictation] = useState(''); const isDictationSupported = Platform.OS === 'ios'; useEffect(() => { @@ -181,20 +185,44 @@ export default function AddMedicationScreen() { }); }, [timesPerDay]); - const appendDictationResult = useCallback( + // 实时更新语音识别结果(替换式,不是追加) + const updateDictationResult = useCallback( (text: string) => { const clean = text.trim(); if (!clean) return; + + // 实时更新:用识别的新文本替换当前识别文本 + setCurrentDictationText(clean); + + // 同步更新到 note 中,以便用户能看到实时效果 setNote((prev) => { - if (!prev) { + // 移除之前的语音识别文本,添加新的识别文本 + const baseText = noteBeforeDictation; + if (!baseText) { return clean; } - return `${prev}${prev.endsWith('\n') ? '' : '\n'}${clean}`; + // 在原文本后追加,确保格式正确 + return `${baseText}${baseText.endsWith('\n') ? '' : '\n'}${clean}`; }); }, - [setNote] + [noteBeforeDictation] ); + // 确认语音识别结果 + const confirmDictationResult = useCallback(() => { + // 语音识别结束,确认当前文本 + setCurrentDictationText(''); + setNoteBeforeDictation(''); + }, []); + + // 取消语音识别 + const cancelDictationResult = useCallback(() => { + // 恢复到语音识别前的文本 + setNote(noteBeforeDictation); + setCurrentDictationText(''); + setNoteBeforeDictation(''); + }, [noteBeforeDictation]); + const stepTitle = STEP_TITLES[currentStep] ?? STEP_TITLES[0]; const stepDescription = STEP_DESCRIPTIONS[currentStep] ?? ''; @@ -224,19 +252,25 @@ export default function AddMedicationScreen() { }; Voice.onSpeechEnd = () => { + // 语音识别结束,确认识别结果 + confirmDictationResult(); setDictationActive(false); setDictationLoading(false); }; Voice.onSpeechResults = (event: any) => { + // 获取最新的识别结果(这是累积的结果,包含之前说过的内容) const recognized = event?.value?.[0]; if (recognized) { - appendDictationResult(recognized); + // 实时更新识别结果,替换式而非追加式 + updateDictationResult(recognized); } }; Voice.onSpeechError = (error: any) => { console.log('[MEDICATION] voice error', error); + // 发生错误时取消识别 + cancelDictationResult(); setDictationActive(false); setDictationLoading(false); Alert.alert('语音识别不可用', '无法使用语音输入,请检查权限设置后重试'); @@ -249,7 +283,7 @@ export default function AddMedicationScreen() { }) .catch(() => {}); }; - }, [appendDictationResult, isDictationSupported]); + }, [updateDictationResult, confirmDictationResult, cancelDictationResult, isDictationSupported]); const handleNext = useCallback(async () => { if (!canProceed) return; @@ -359,25 +393,34 @@ export default function AddMedicationScreen() { try { if (dictationActive) { + // 停止录音 setDictationLoading(true); await Voice.stop(); + // Voice.onSpeechEnd 会自动确认结果 setDictationLoading(false); return; } + // 开始录音前,保存当前的文本内容 + setNoteBeforeDictation(note); + setCurrentDictationText(''); + setDictationLoading(true); try { + // 确保之前的录音已停止 await Voice.stop(); } catch { - // no-op: safe to ignore if not already recording + // 忽略错误:如果之前没有录音,stop 会抛出异常 } + + // 开始语音识别 await Voice.start('zh-CN'); } catch (error) { console.log('[MEDICATION] unable to start dictation', error); setDictationLoading(false); Alert.alert('无法启动语音输入', '请检查麦克风与语音识别权限后重试'); } - }, [dictationActive, dictationLoading, isDictationSupported]); + }, [dictationActive, dictationLoading, isDictationSupported, note]); // 处理图片选择(拍照或相册) const handleSelectPhoto = useCallback(() => { diff --git a/constants/Routes.ts b/constants/Routes.ts index a328439..741e22b 100644 --- a/constants/Routes.ts +++ b/constants/Routes.ts @@ -79,6 +79,7 @@ export const ROUTES = { // 药品相关路由 MEDICATION_EDIT_FREQUENCY: '/medications/edit-frequency', + MEDICATION_MANAGE: '/medications/manage-medications', } as const; // 路由参数常量 diff --git a/ios/OutLive/HealthKitManager.m b/ios/OutLive/HealthKitManager.m index c293ee9..246372d 100644 --- a/ios/OutLive/HealthKitManager.m +++ b/ios/OutLive/HealthKitManager.m @@ -89,4 +89,11 @@ RCT_EXTERN_METHOD(startWorkoutObserver:(RCTPromiseResolveBlock)resolver RCT_EXTERN_METHOD(stopWorkoutObserver:(RCTPromiseResolveBlock)resolver rejecter:(RCTPromiseRejectBlock)rejecter) +// Sleep Observer Methods +RCT_EXTERN_METHOD(startSleepObserver:(RCTPromiseResolveBlock)resolver + rejecter:(RCTPromiseRejectBlock)rejecter) + +RCT_EXTERN_METHOD(stopSleepObserver:(RCTPromiseResolveBlock)resolver + rejecter:(RCTPromiseRejectBlock)rejecter) + @end \ No newline at end of file diff --git a/ios/OutLive/HealthKitManager.swift b/ios/OutLive/HealthKitManager.swift index ffbf86a..bc51684 100644 --- a/ios/OutLive/HealthKitManager.swift +++ b/ios/OutLive/HealthKitManager.swift @@ -1700,9 +1700,10 @@ class HealthKitManager: RCTEventEmitter { return description.lowercased() } -// MARK: - Workout Observer Methods +// MARK: - Observer Methods private var workoutObserverQuery: HKObserverQuery? +private var sleepObserverQuery: HKObserverQuery? @objc func startWorkoutObserver( @@ -1781,10 +1782,89 @@ private func sendWorkoutUpdateEvent() { ]) } +// MARK: - Sleep Observer Methods + +@objc +func startSleepObserver( + _ resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock +) { + guard HKHealthStore.isHealthDataAvailable() else { + rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil) + return + } + + // 如果已经有观察者在运行,先停止它 + if let existingQuery = sleepObserverQuery { + healthStore.stop(existingQuery) + sleepObserverQuery = nil + } + + // 创建睡眠数据观察者 + let sleepType = ReadTypes.sleep + + sleepObserverQuery = HKObserverQuery(sampleType: sleepType, predicate: nil) { [weak self] (query, completionHandler, error) in + if let error = error { + print("Sleep observer error: \(error.localizedDescription)") + completionHandler() + return + } + + print("Sleep data updated, sending event to React Native") + // 发送简单的更新事件到 React Native,让 RN 层处理数据分析 + self?.sendSleepUpdateEvent() + completionHandler() + } + + // 启用后台传递 + healthStore.enableBackgroundDelivery(for: sleepType, frequency: .immediate) { (success, error) in + if let error = error { + print("Failed to enable background delivery for sleep: \(error.localizedDescription)") + } else { + print("Background delivery for sleep enabled successfully") + } + } + + // 执行查询 + healthStore.execute(sleepObserverQuery!) + + resolver(["success": true]) +} + +@objc +func stopSleepObserver( + _ resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock +) { + if let query = sleepObserverQuery { + healthStore.stop(query) + sleepObserverQuery = nil + + // 禁用后台传递 + healthStore.disableBackgroundDelivery(for: ReadTypes.sleep) { (success, error) in + if let error = error { + print("Failed to disable background delivery for sleep: \(error.localizedDescription)") + } + } + + resolver(["success": true]) + } else { + resolver(["success": true]) // 即使没有查询在运行也返回成功 + } +} + +/// 发送睡眠更新事件到 React Native(简单通知,不包含分析数据) +private func sendSleepUpdateEvent() { + sendEvent(withName: "sleepUpdate", body: [ + "timestamp": Date().timeIntervalSince1970, + "type": "sleep_data_updated" + ]) +} + // MARK: - RCTEventEmitter Overrides override func supportedEvents() -> [String]! { - return ["workoutUpdate"] + return ["workoutUpdate", "sleepUpdate"] } override static func requiresMainQueueSetup() -> Bool { diff --git a/ios/medicine/medicine.swift b/ios/medicine/medicine.swift index 9a7e25f..33c1251 100644 --- a/ios/medicine/medicine.swift +++ b/ios/medicine/medicine.swift @@ -105,6 +105,7 @@ struct medicineEntryView: View { .foregroundColor(Color.secondary) } .padding() + .widgetURL(URL(string: "digitalpilates://medications")) } } @@ -174,10 +175,11 @@ struct medicine: Widget { var body: some WidgetConfiguration { AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in medicineEntryView(entry: entry) - .containerBackground(.fill.tertiary, for: .widget) + .containerBackground(.white, for: .widget) } .configurationDisplayName("用药计划") .description("显示今日用药计划和服药状态") + .supportedFamilies([.systemSmall, .systemMedium]) } } diff --git a/services/sleepMonitor.ts b/services/sleepMonitor.ts new file mode 100644 index 0000000..a6344c2 --- /dev/null +++ b/services/sleepMonitor.ts @@ -0,0 +1,513 @@ +/** + * 睡眠监控服务 + * + * 负责监听睡眠数据更新事件,分析睡眠质量并提供反馈 + */ + +import { logger } from '@/utils/logger'; +import { NativeEventEmitter, NativeModules } from 'react-native'; +import { analyzeSleepAndSendNotification } from './sleepNotificationService'; + +const { HealthKitManager } = NativeModules; + + +// 睡眠阶段类型 +export type SleepStage = 'inBed' | 'asleep' | 'awake' | 'core' | 'deep' | 'rem' | 'unknown'; + +// 睡眠样本数据 +export interface SleepSample { + id: string; + startDate: string; + endDate: string; + value: number; + categoryType: SleepStage; + duration: number; + source: { + name: string; + bundleIdentifier: string; + }; + metadata: Record; +} + +// 睡眠分析结果 +export interface SleepAnalysisData { + sessionStart: string; + sessionEnd: string; + totalSleepDuration: number; // 秒 + totalSleepHours: number; // 小时 + deepSleepDuration: number; // 秒 + deepSleepHours: number; // 小时 + deepSleepPercentage: number; // 百分比 + remSleepDuration: number; // 秒 + remSleepHours: number; // 小时 + remSleepPercentage: number; // 百分比 + coreSleepDuration: number; // 秒 + coreSleepHours: number; // 小时 + coreSleepPercentage: number; // 百分比 + awakeDuration: number; // 秒 + awakeMinutes: number; // 分钟 + sleepEfficiency: number; // 百分比 + sleepScore: number; // 0-100分 + quality: 'excellent' | 'good' | 'fair' | 'poor' | 'very_poor'; +} + +// 睡眠事件数据 +export interface SleepEventData { + timestamp: number; + type: 'sleep_data_updated'; +} + +/** + * 睡眠监控服务类 + */ +class SleepMonitorService { + private eventEmitter: NativeEventEmitter | null = null; + private eventSubscription: any = null; + private isInitialized = false; + private lastProcessedTime = 0; + private debounceDelay = 3000; // 3秒防抖延迟 + + /** + * 初始化睡眠监控服务 + */ + async initialize(): Promise { + if (this.isInitialized) { + console.log('[SleepMonitor] Already initialized'); + return true; + } + + try { + // 创建事件发射器 + this.eventEmitter = new NativeEventEmitter(HealthKitManager); + + // 订阅睡眠更新事件 + this.eventSubscription = this.eventEmitter.addListener( + 'sleepUpdate', + this.handleSleepUpdate.bind(this) + ); + + // 启动睡眠观察者 + await HealthKitManager.startSleepObserver(); + + this.isInitialized = true; + console.log('[SleepMonitor] Initialized successfully'); + return true; + } catch (error) { + console.error('[SleepMonitor] Initialization failed:', error); + return false; + } + } + + /** + * 停止睡眠监控服务 + */ + async stop(): Promise { + if (!this.isInitialized) { + return; + } + + try { + // 取消事件订阅 + if (this.eventSubscription) { + this.eventSubscription.remove(); + this.eventSubscription = null; + } + + // 停止睡眠观察者 + await HealthKitManager.stopSleepObserver(); + + this.isInitialized = false; + console.log('[SleepMonitor] Stopped successfully'); + } catch (error) { + console.error('[SleepMonitor] Stop failed:', error); + } + } + + /** + * 处理睡眠更新事件 + */ + private async handleSleepUpdate(event: SleepEventData): Promise { + console.log('[SleepMonitor] Sleep data updated:', event); + + // 防抖处理:避免短时间内重复处理 + const now = Date.now(); + if (now - this.lastProcessedTime < this.debounceDelay) { + console.log('[SleepMonitor] Debouncing, skipping this update'); + return; + } + this.lastProcessedTime = now; + + try { + // 分析最近的睡眠数据 + const analysis = await this.analyzeSleepData(); + + if (analysis) { + logger.info('[SleepMonitor] Sleep analysis completed:', { + score: analysis.sleepScore, + quality: analysis.quality, + duration: `${analysis.totalSleepHours.toFixed(1)}h`, + }); + + // 发送睡眠分析通知 + await this.notifySleepAnalysis(analysis); + } + } catch (error) { + console.error('[SleepMonitor] Failed to analyze sleep data:', error); + } + } + + /** + * 分析睡眠数据 + */ + async analyzeSleepData(): Promise { + try { + // 获取最近24小时的睡眠数据 + const endDate = new Date(); + const startDate = new Date(endDate.getTime() - 24 * 60 * 60 * 1000); + + const result = await HealthKitManager.getSleepData({ + startDate: startDate.toISOString(), + endDate: endDate.toISOString(), + }); + + if (!result.data || result.data.length === 0) { + console.log('[SleepMonitor] No sleep data found'); + return null; + } + + const sleepSamples: SleepSample[] = result.data; + + // 找到最近一次完整的睡眠会话 + const session = this.findLatestCompleteSleepSession(sleepSamples); + if (!session) { + console.log('[SleepMonitor] No complete sleep session found'); + return null; + } + + // 分析睡眠样本 + const analysis = this.analyzeSleepSamples(session.samples, session.startDate, session.endDate); + + return analysis; + } catch (error) { + console.error('[SleepMonitor] Failed to analyze sleep data:', error); + return null; + } + } + + /** + * 找到最近一次完整的睡眠会话 + */ + private findLatestCompleteSleepSession(samples: SleepSample[]): { + samples: SleepSample[]; + startDate: Date; + endDate: Date; + } | null { + if (samples.length === 0) return null; + + // 按开始时间排序 + const sortedSamples = [...samples].sort((a, b) => + new Date(a.startDate).getTime() - new Date(b.startDate).getTime() + ); + + // 从最新的样本开始往前查找 + const reversedSamples = [...sortedSamples].reverse(); + const sessionSamples: SleepSample[] = []; + let sessionEnd: Date | null = null; + + for (const sample of reversedSamples) { + // 如果是第一个样本,记录结束时间 + if (!sessionEnd) { + sessionEnd = new Date(sample.endDate); + } + + // 收集连续的睡眠样本(时间间隔不超过30分钟) + if (sessionSamples.length > 0) { + const lastSample = sessionSamples[sessionSamples.length - 1]; + const gap = new Date(lastSample.startDate).getTime() - new Date(sample.endDate).getTime(); + if (gap > 30 * 60 * 1000) { // 间隔超过30分钟 + break; + } + } + + sessionSamples.push(sample); + } + + // 反转回正常顺序 + sessionSamples.reverse(); + + if (sessionSamples.length === 0) return null; + + const sessionStart = new Date(sessionSamples[0].startDate); + const sessionEndDate = sessionEnd || new Date(sessionSamples[sessionSamples.length - 1].endDate); + + // 确保会话至少有2小时的数据 + const duration = sessionEndDate.getTime() - sessionStart.getTime(); + if (duration < 2 * 60 * 60 * 1000) { // 至少2小时 + return null; + } + + return { + samples: sessionSamples, + startDate: sessionStart, + endDate: sessionEndDate, + }; + } + + /** + * 分析睡眠样本数据 + */ + private analyzeSleepSamples( + samples: SleepSample[], + sessionStart: Date, + sessionEnd: Date + ): SleepAnalysisData { + // 计算各阶段睡眠时长 + let totalSleepDuration = 0; + let deepSleepDuration = 0; + let remSleepDuration = 0; + let coreSleepDuration = 0; + let awakeDuration = 0; + + for (const sample of samples) { + const duration = sample.duration; + + switch (sample.categoryType) { + case 'deep': + deepSleepDuration += duration; + totalSleepDuration += duration; + break; + case 'rem': + remSleepDuration += duration; + totalSleepDuration += duration; + break; + case 'core': + coreSleepDuration += duration; + totalSleepDuration += duration; + break; + case 'asleep': + totalSleepDuration += duration; + break; + case 'awake': + awakeDuration += duration; + break; + } + } + + // 计算睡眠效率 + const totalInBedDuration = (sessionEnd.getTime() - sessionStart.getTime()) / 1000; + const sleepEfficiency = totalInBedDuration > 0 + ? (totalSleepDuration / totalInBedDuration) * 100 + : 0; + + // 计算各阶段睡眠占比 + const deepSleepPercentage = totalSleepDuration > 0 + ? (deepSleepDuration / totalSleepDuration) * 100 + : 0; + const remSleepPercentage = totalSleepDuration > 0 + ? (remSleepDuration / totalSleepDuration) * 100 + : 0; + const coreSleepPercentage = totalSleepDuration > 0 + ? (coreSleepDuration / totalSleepDuration) * 100 + : 0; + + // 计算睡眠评分 + const sleepScore = this.calculateSleepScore( + totalSleepDuration, + deepSleepDuration, + remSleepDuration, + sleepEfficiency, + awakeDuration + ); + + return { + sessionStart: sessionStart.toISOString(), + sessionEnd: sessionEnd.toISOString(), + totalSleepDuration, + totalSleepHours: totalSleepDuration / 3600, + deepSleepDuration, + deepSleepHours: deepSleepDuration / 3600, + deepSleepPercentage, + remSleepDuration, + remSleepHours: remSleepDuration / 3600, + remSleepPercentage, + coreSleepDuration, + coreSleepHours: coreSleepDuration / 3600, + coreSleepPercentage, + awakeDuration, + awakeMinutes: awakeDuration / 60, + sleepEfficiency, + sleepScore, + quality: this.getSleepQualityLabel(sleepScore), + }; + } + + /** + * 计算睡眠评分(0-100分) + */ + private calculateSleepScore( + totalSleepDuration: number, + deepSleepDuration: number, + remSleepDuration: number, + sleepEfficiency: number, + awakeDuration: number + ): number { + let score = 0; + + // 1. 总睡眠时长评分(40分) + const sleepHours = totalSleepDuration / 3600; + if (sleepHours >= 7 && sleepHours <= 9) { + score += 40; // 理想睡眠时长 + } else if (sleepHours >= 6 && sleepHours < 7) { + score += 35; // 稍短 + } else if (sleepHours >= 9 && sleepHours < 10) { + score += 35; // 稍长 + } else if (sleepHours >= 5 && sleepHours < 6) { + score += 25; // 较短 + } else if (sleepHours >= 10 && sleepHours < 11) { + score += 30; // 较长 + } else if (sleepHours >= 4 && sleepHours < 5) { + score += 15; // 很短 + } else if (sleepHours >= 11) { + score += 20; // 过长 + } else { + score += 10; // 极短 + } + + // 2. 深度睡眠评分(25分) + const deepSleepPercentage = totalSleepDuration > 0 + ? (deepSleepDuration / totalSleepDuration) * 100 + : 0; + + if (deepSleepPercentage >= 13 && deepSleepPercentage <= 23) { + score += 25; // 理想的深度睡眠占比(13-23%) + } else if (deepSleepPercentage >= 10 && deepSleepPercentage < 13) { + score += 20; + } else if (deepSleepPercentage >= 23 && deepSleepPercentage < 30) { + score += 20; + } else if (deepSleepPercentage >= 7 && deepSleepPercentage < 10) { + score += 15; + } else if (deepSleepPercentage >= 30) { + score += 15; + } else { + score += 10; + } + + // 3. REM睡眠评分(20分) + const remSleepPercentage = totalSleepDuration > 0 + ? (remSleepDuration / totalSleepDuration) * 100 + : 0; + + if (remSleepPercentage >= 20 && remSleepPercentage <= 25) { + score += 20; // 理想的REM睡眠占比(20-25%) + } else if (remSleepPercentage >= 15 && remSleepPercentage < 20) { + score += 18; + } else if (remSleepPercentage >= 25 && remSleepPercentage < 30) { + score += 18; + } else if (remSleepPercentage >= 10 && remSleepPercentage < 15) { + score += 12; + } else if (remSleepPercentage >= 30) { + score += 15; + } else { + score += 8; + } + + // 4. 睡眠效率评分(15分) + if (sleepEfficiency >= 85) { + score += 15; // 优秀的睡眠效率 + } else if (sleepEfficiency >= 75) { + score += 12; + } else if (sleepEfficiency >= 65) { + score += 8; + } else { + score += 5; + } + + return Math.min(Math.round(score), 100); + } + + /** + * 根据评分获取睡眠质量标签 + */ + private getSleepQualityLabel(score: number): 'excellent' | 'good' | 'fair' | 'poor' | 'very_poor' { + if (score >= 90) return 'excellent'; + if (score >= 75) return 'good'; + if (score >= 60) return 'fair'; + if (score >= 40) return 'poor'; + return 'very_poor'; + } + + /** + * 通知睡眠分析结果 + */ + private async notifySleepAnalysis(analysis: SleepAnalysisData): Promise { + try { + // 发送睡眠分析通知 + await analyzeSleepAndSendNotification(analysis); + + logger.info('[SleepMonitor] Sleep analysis notification sent:', { + score: analysis.sleepScore, + quality: analysis.quality, + duration: `${analysis.totalSleepHours.toFixed(1)}h`, + efficiency: `${analysis.sleepEfficiency.toFixed(0)}%`, + }); + + // 可以在这里更新 Redux store + // store.dispatch(updateSleepAnalysis(analysis)); + } catch (error) { + logger.error('[SleepMonitor] Failed to send sleep notification:', error); + } + } + + /** + * 获取睡眠质量描述 + */ + getQualityDescription(quality: string): string { + const descriptions = { + excellent: '优秀 - 您的睡眠质量非常好!', + good: '良好 - 您的睡眠质量不错', + fair: '一般 - 您的睡眠质量还可以改善', + poor: '较差 - 建议改善睡眠习惯', + very_poor: '很差 - 强烈建议关注睡眠健康', + }; + return descriptions[quality as keyof typeof descriptions] || '未知'; + } + + /** + * 获取睡眠建议 + */ + getSleepRecommendations(analysis: SleepAnalysisData): string[] { + const recommendations: string[] = []; + + // 基于总睡眠时长的建议 + if (analysis.totalSleepHours < 7) { + recommendations.push('建议增加睡眠时间,成年人每晚需要7-9小时睡眠'); + } else if (analysis.totalSleepHours > 9) { + recommendations.push('睡眠时间偏长,可能影响睡眠质量'); + } + + // 基于深度睡眠的建议 + if (analysis.deepSleepPercentage < 13) { + recommendations.push('深度睡眠不足,建议睡前避免使用电子设备'); + } + + // 基于REM睡眠的建议 + if (analysis.remSleepPercentage < 20) { + recommendations.push('REM睡眠不足,建议保持规律的作息时间'); + } + + // 基于睡眠效率的建议 + if (analysis.sleepEfficiency < 85) { + recommendations.push('睡眠效率较低,建议改善睡眠环境'); + } + + // 基于清醒时间的建议 + if (analysis.awakeMinutes > 30) { + recommendations.push('夜间醒来时间较长,建议睡前放松身心'); + } + + return recommendations; + } +} + +// 导出单例 +export const sleepMonitorService = new SleepMonitorService(); \ No newline at end of file diff --git a/services/sleepNotificationService.ts b/services/sleepNotificationService.ts new file mode 100644 index 0000000..ce86169 --- /dev/null +++ b/services/sleepNotificationService.ts @@ -0,0 +1,191 @@ +/** + * 睡眠通知服务 + * + * 负责在睡眠分析完成后发送通知,提供睡眠质量评估和建议 + */ + +import { logger } from '@/utils/logger'; +import * as Notifications from 'expo-notifications'; +import { SleepAnalysisData } from './sleepMonitor'; + +/** + * 分析睡眠数据并发送通知 + */ +export async function analyzeSleepAndSendNotification( + analysis: SleepAnalysisData +): Promise { + try { + logger.info('开始分析睡眠并发送通知:', { + score: analysis.sleepScore, + quality: analysis.quality, + duration: analysis.totalSleepHours, + }); + + // 构建通知内容 + const notification = buildSleepNotification(analysis); + + // 发送通知 + await Notifications.scheduleNotificationAsync({ + content: notification, + trigger: null, // 立即发送 + }); + + logger.info('睡眠分析通知已发送'); + } catch (error) { + logger.error('发送睡眠分析通知失败:', error); + throw error; + } +} + +/** + * 构建睡眠通知内容 + */ +function buildSleepNotification(analysis: SleepAnalysisData): Notifications.NotificationContentInput { + const { sleepScore, quality, totalSleepHours, sleepEfficiency } = analysis; + + // 根据质量等级选择emoji和标题 + const qualityConfig = getQualityConfig(quality); + + // 构建通知标题 + const title = `${qualityConfig.emoji} ${qualityConfig.title}`; + + // 构建通知正文 + const sleepDuration = formatSleepDuration(totalSleepHours); + const efficiencyText = `睡眠效率 ${sleepEfficiency.toFixed(0)}%`; + const body = `您昨晚睡了 ${sleepDuration},${efficiencyText}。评分:${sleepScore}分`; + + // 获取建议 + const suggestion = getSleepSuggestion(analysis); + + return { + title, + body: `${body}\n${suggestion}`, + data: { + type: 'sleep_analysis', + score: sleepScore, + quality, + analysis: JSON.stringify(analysis), + url: '/sleep-detail', // 点击通知跳转到睡眠详情页 + }, + sound: 'default', + badge: 1, + }; +} + +/** + * 获取质量配置 + */ +function getQualityConfig(quality: string): { + emoji: string; + title: string; +} { + const configs = { + excellent: { + emoji: '😴', + title: '睡眠质量优秀', + }, + good: { + emoji: '😊', + title: '睡眠质量良好', + }, + fair: { + emoji: '😐', + title: '睡眠质量一般', + }, + poor: { + emoji: '😟', + title: '睡眠质量较差', + }, + very_poor: { + emoji: '😰', + title: '睡眠质量很差', + }, + }; + + return configs[quality as keyof typeof configs] || { + emoji: '💤', + title: '睡眠分析完成', + }; +} + +/** + * 格式化睡眠时长 + */ +function formatSleepDuration(hours: number): string { + const h = Math.floor(hours); + const m = Math.round((hours - h) * 60); + + if (m === 0) { + return `${h}小时`; + } + return `${h}小时${m}分钟`; +} + +/** + * 获取睡眠建议 + */ +function getSleepSuggestion(analysis: SleepAnalysisData): string { + const { quality, totalSleepHours, deepSleepPercentage, remSleepPercentage, sleepEfficiency } = analysis; + + // 优秀或良好的睡眠 + if (quality === 'excellent' || quality === 'good') { + const tips = [ + '继续保持良好的睡眠习惯!', + '坚持规律作息,身体会感谢你!', + '优质睡眠让你精力充沛!', + ]; + return tips[Math.floor(Math.random() * tips.length)]; + } + + // 根据具体问题给出建议 + const suggestions: string[] = []; + + if (totalSleepHours < 7) { + suggestions.push('建议增加睡眠时间至7-9小时'); + } else if (totalSleepHours > 9) { + suggestions.push('睡眠时间偏长,注意睡眠质量'); + } + + if (deepSleepPercentage < 13) { + suggestions.push('深度睡眠不足,睡前避免使用电子设备'); + } + + if (remSleepPercentage < 20) { + suggestions.push('REM睡眠不足,保持规律的作息时间'); + } + + if (sleepEfficiency < 85) { + suggestions.push('睡眠效率较低,改善睡眠环境'); + } + + // 如果有具体建议,返回第一条;否则返回通用建议 + if (suggestions.length > 0) { + return `💡 ${suggestions[0]}`; + } + + return '建议关注睡眠质量,保持良好作息'; +} + +/** + * 发送简单的睡眠提醒(用于测试) + */ +export async function sendSimpleSleepReminder(userName: string = '朋友'): Promise { + try { + await Notifications.scheduleNotificationAsync({ + content: { + title: '😴 睡眠质量分析', + body: `${userName},您的睡眠数据已更新,点击查看详细分析`, + data: { + type: 'sleep_reminder', + url: '/sleep-detail', + }, + sound: 'default', + }, + trigger: null, + }); + + logger.info('简单睡眠提醒已发送'); + } catch (error) { + logger.error('发送简单睡眠提醒失败:', error); + } +} \ No newline at end of file