diff --git a/app/_layout.tsx b/app/_layout.tsx index ac25267..36320d3 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -139,6 +139,8 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { permissionInitializedRef.current = true; + const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + const initializePermissionServices = async () => { try { logger.info('🔐 开始初始化需要权限的服务(onboarding 已完成)...'); @@ -148,14 +150,17 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { logger.info('✅ 通知服务初始化完成'); // 2. 延迟请求 HealthKit 权限(避免立即弹窗打断用户) - setTimeout(async () => { - try { - await ensureHealthPermissions(); + await delay(2000); + try { + const granted = await ensureHealthPermissions(); + if (granted) { logger.info('✅ HealthKit 权限请求完成'); - } catch (error) { - logger.warn('⚠️ HealthKit 权限请求失败,可能在模拟器上运行:', error); + } else { + logger.warn('⚠️ 用户未授予 HealthKit 权限,相关功能将受限'); } - }, 2000); + } catch (error) { + logger.warn('⚠️ HealthKit 权限请求失败,可能在模拟器上运行:', error); + } // 3. 异步同步 Widget 数据 syncWidgetDataInBackground(); @@ -163,6 +168,7 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { logger.info('🎉 权限相关服务初始化完成'); } catch (error) { logger.error('❌ 权限相关服务初始化失败:', error); + throw error; } }; @@ -314,19 +320,35 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { try { logger.info('💪 初始化健康监听服务...'); - // 并行初始化锻炼和睡眠监听 - await Promise.allSettled([ - workoutMonitorService.initialize().then(() => - logger.info('✅ 锻炼监听服务初始化成功') - ), - sleepMonitorService.initialize().then(() => - logger.info('✅ 睡眠监听服务初始化成功') - ), + const [workoutResult, sleepResult] = await Promise.allSettled([ + workoutMonitorService.initialize(), + sleepMonitorService.initialize(), ]); - logger.info('🎉 健康监听服务初始化完成'); + const workoutReady = workoutResult.status === 'fulfilled'; + if (workoutReady) { + logger.info('✅ 锻炼监听服务初始化成功'); + } else { + logger.error('❌ 锻炼监听服务初始化失败:', workoutResult.reason); + } + + const sleepReady = sleepResult.status === 'fulfilled'; + if (sleepReady) { + logger.info('✅ 睡眠监听服务初始化成功'); + } else { + logger.error('❌ 睡眠监听服务初始化失败:', sleepResult.reason); + } + + if (workoutReady && sleepReady) { + logger.info('🎉 健康监听服务初始化完成'); + } else { + logger.warn('⚠️ 健康监听服务部分未能初始化成功,请检查上述错误日志'); + } + + return workoutReady && sleepReady; } catch (error) { logger.error('❌ 健康监听服务初始化失败:', error); + return false; } }; @@ -359,14 +381,21 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { } }; - // 执行权限相关初始化 - initializePermissionServices(); - - // 交互完成后执行后台服务 - initializeBackgroundServices(); - - // 空闲时执行非关键服务 - initializeIdleServices(); + const runInitializationSequence = async () => { + try { + await initializePermissionServices(); + } catch { + logger.warn('⚠️ 权限相关服务初始化失败,将继续启动后台和空闲服务以便后续重试'); + } + + // 交互完成后执行后台服务 + initializeBackgroundServices(); + + // 空闲时执行非关键服务 + initializeIdleServices(); + }; + + runInitializationSequence(); }, [onboardingCompleted, profile.name]); diff --git a/ios/OutLive/Info.plist b/ios/OutLive/Info.plist index 4b1db45..4e6122e 100644 --- a/ios/OutLive/Info.plist +++ b/ios/OutLive/Info.plist @@ -27,7 +27,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0.25 + 1.0.26 CFBundleSignature ???? CFBundleURLTypes diff --git a/services/workoutMonitor.ts b/services/workoutMonitor.ts index 696f984..59d7660 100644 --- a/services/workoutMonitor.ts +++ b/services/workoutMonitor.ts @@ -6,6 +6,9 @@ import { analyzeWorkoutAndSendNotification } from './workoutNotificationService' const { HealthKitManager } = NativeModules; const workoutEmitter = new NativeEventEmitter(HealthKitManager); +const INITIAL_LOOKBACK_WINDOW_MS = 24 * 60 * 60 * 1000; // 24小时 +const DEFAULT_LOOKBACK_WINDOW_MS = 12 * 60 * 60 * 1000; // 12小时 + class WorkoutMonitorService { private static instance: WorkoutMonitorService; private isInitialized = false; @@ -111,32 +114,57 @@ class WorkoutMonitorService { try { console.log('检查新的锻炼记录...'); - // 获取最近1小时的锻炼记录 - const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); + const lookbackWindowMs = this.lastProcessedWorkoutId + ? DEFAULT_LOOKBACK_WINDOW_MS + : INITIAL_LOOKBACK_WINDOW_MS; + const startDate = new Date(Date.now() - lookbackWindowMs); + const endDate = new Date(); + + console.log( + `锻炼查询窗口: ${Math.round(lookbackWindowMs / (1000 * 60 * 60))} 小时 (${startDate.toISOString()} - ${endDate.toISOString()})` + ); + const recentWorkouts = await fetchRecentWorkouts({ - startDate: oneHourAgo.toISOString(), - endDate: new Date().toISOString(), + startDate: startDate.toISOString(), + endDate: endDate.toISOString(), limit: 10 }); console.log(`找到 ${recentWorkouts.length} 条最近的锻炼记录`); - // 检查是否有新的锻炼记录 - for (const workout of recentWorkouts) { - if (workout.id !== this.lastProcessedWorkoutId) { - console.log('检测到新锻炼:', { - id: workout.id, - type: workout.workoutActivityTypeString, - duration: workout.duration, - startDate: workout.startDate - }); - - await this.processNewWorkout(workout); - await this.saveLastProcessedWorkoutId(workout.id); - } else { - console.log('锻炼已处理过,跳过:', workout.id); - } + if (this.lastProcessedWorkoutId && !recentWorkouts.some(workout => workout.id === this.lastProcessedWorkoutId)) { + console.warn('上次处理的锻炼记录不在当前查询窗口内,可能存在漏报风险'); } + + const newWorkouts: WorkoutData[] = []; + for (const workout of recentWorkouts) { + if (workout.id === this.lastProcessedWorkoutId) { + break; + } + newWorkouts.push(workout); + } + + if (newWorkouts.length === 0) { + console.log('没有检测到新的锻炼记录'); + return; + } + + console.log(`检测到 ${newWorkouts.length} 条新的锻炼记录,将按时间顺序处理`); + + // 先处理最旧的锻炼,确保通知顺序正确 + for (const workout of newWorkouts.reverse()) { + console.log('处理新锻炼:', { + id: workout.id, + type: workout.workoutActivityTypeString, + duration: workout.duration, + startDate: workout.startDate + }); + + await this.processNewWorkout(workout); + } + + await this.saveLastProcessedWorkoutId(newWorkouts[0].id); + console.log('锻炼处理完成,最新处理的锻炼ID:', newWorkouts[0].id); } catch (error) { console.error('检查新锻炼失败:', error); } @@ -170,4 +198,4 @@ class WorkoutMonitorService { } } -export const workoutMonitorService = WorkoutMonitorService.getInstance(); \ No newline at end of file +export const workoutMonitorService = WorkoutMonitorService.getInstance(); diff --git a/utils/sleepHealthKit.ts b/utils/sleepHealthKit.ts index 6f7ce03..a950cc2 100644 --- a/utils/sleepHealthKit.ts +++ b/utils/sleepHealthKit.ts @@ -11,6 +11,14 @@ export enum SleepStage { REM = 'REM' } +// 阶段中被视为真正睡着的类型 +const ACTIVE_SLEEP_STAGE_SET = new Set([ + SleepStage.Asleep, + SleepStage.Core, + SleepStage.Deep, + SleepStage.REM +]); + // 睡眠质量评级 export enum SleepQuality { Poor = 'poor', @@ -219,12 +227,13 @@ export const calculateSleepStageStats = (samples: SleepSample[]): SleepStageStat samples.forEach(sample => { const startTime = dayjs(sample.startDate); const endTime = dayjs(sample.endDate); - const duration = endTime.diff(startTime, 'minute'); + const durationMinutes = endTime.diff(startTime, 'minute', true); + const roundedDuration = Math.round(durationMinutes); - console.log(`[Sleep] 阶段: ${sample.value}, 持续时间: ${duration}分钟`); + console.log(`[Sleep] 阶段: ${sample.value}, 持续时间: ${roundedDuration}分钟`); const currentDuration = stageMap.get(sample.value) || 0; - stageMap.set(sample.value, currentDuration + duration); + stageMap.set(sample.value, currentDuration + durationMinutes); }); console.log('[Sleep] 阶段时间统计:', Array.from(stageMap.entries())); @@ -234,7 +243,7 @@ export const calculateSleepStageStats = (samples: SleepSample[]): SleepStageStat .filter(([stage]) => stage !== SleepStage.InBed) .reduce((total, [, duration]) => total + duration, 0); - console.log('[Sleep] 实际睡眠时间(包含醒来):', actualSleepTime, '分钟'); + console.log('[Sleep] 实际睡眠时间(包含醒来):', Math.round(actualSleepTime), '分钟'); const stats: SleepStageStats[] = []; @@ -243,6 +252,7 @@ export const calculateSleepStageStats = (samples: SleepSample[]): SleepStageStat if (stage === SleepStage.InBed) return; const percentage = actualSleepTime > 0 ? (duration / actualSleepTime) * 100 : 0; + const roundedDuration = Math.round(duration); let quality: SleepQuality; // 根据不同阶段的标准百分比评估质量 @@ -278,12 +288,14 @@ export const calculateSleepStageStats = (samples: SleepSample[]): SleepStageStat stats.push({ stage, - duration, + duration: roundedDuration, percentage: Math.round(percentage), quality }); - console.log(`[Sleep] 阶段统计: ${stage}, 时长: ${duration}分钟, 百分比: ${Math.round(percentage)}%, 质量: ${quality}`); + console.log( + `[Sleep] 阶段统计: ${stage}, 时长: ${roundedDuration}分钟, 百分比: ${Math.round(percentage)}%, 质量: ${quality}` + ); }); const sortedStats = stats.sort((a, b) => b.duration - a.duration); @@ -410,10 +422,15 @@ export const fetchCompleteSleepData = async (date: Date): Promise - stage.stage !== SleepStage.InBed - ); - const totalSleepTime = actualSleepStages.reduce((total, stage) => total + stage.duration, 0); + const totalSleepMinutes = sleepSamples.reduce((total, sample) => { + if (!ACTIVE_SLEEP_STAGE_SET.has(sample.value)) { + return total; + } + const start = dayjs(sample.startDate); + const end = dayjs(sample.endDate); + return total + end.diff(start, 'minute', true); + }, 0); + const totalSleepTime = Math.round(totalSleepMinutes); // 重新计算睡眠效率 const sleepEfficiency = timeInBed > 0 ? Math.round((totalSleepTime / timeInBed) * 100) : 0; @@ -501,4 +518,4 @@ export const getSleepStageColor = (stage: SleepStage): string => { default: return '#9CA3AF'; } -}; \ No newline at end of file +};