feat(initialize): 优化权限和健康监听服务的初始化流程,增加延迟和错误处理
fix(version): 更新应用版本号至1.0.26 feat(sleep): 增加睡眠阶段统计的准确性,优化日志输出
This commit is contained in:
@@ -139,6 +139,8 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
permissionInitializedRef.current = true;
|
permissionInitializedRef.current = true;
|
||||||
|
|
||||||
|
const delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
const initializePermissionServices = async () => {
|
const initializePermissionServices = async () => {
|
||||||
try {
|
try {
|
||||||
logger.info('🔐 开始初始化需要权限的服务(onboarding 已完成)...');
|
logger.info('🔐 开始初始化需要权限的服务(onboarding 已完成)...');
|
||||||
@@ -148,14 +150,17 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
logger.info('✅ 通知服务初始化完成');
|
logger.info('✅ 通知服务初始化完成');
|
||||||
|
|
||||||
// 2. 延迟请求 HealthKit 权限(避免立即弹窗打断用户)
|
// 2. 延迟请求 HealthKit 权限(避免立即弹窗打断用户)
|
||||||
setTimeout(async () => {
|
await delay(2000);
|
||||||
try {
|
try {
|
||||||
await ensureHealthPermissions();
|
const granted = await ensureHealthPermissions();
|
||||||
|
if (granted) {
|
||||||
logger.info('✅ HealthKit 权限请求完成');
|
logger.info('✅ HealthKit 权限请求完成');
|
||||||
|
} else {
|
||||||
|
logger.warn('⚠️ 用户未授予 HealthKit 权限,相关功能将受限');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('⚠️ HealthKit 权限请求失败,可能在模拟器上运行:', error);
|
logger.warn('⚠️ HealthKit 权限请求失败,可能在模拟器上运行:', error);
|
||||||
}
|
}
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
// 3. 异步同步 Widget 数据
|
// 3. 异步同步 Widget 数据
|
||||||
syncWidgetDataInBackground();
|
syncWidgetDataInBackground();
|
||||||
@@ -163,6 +168,7 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
logger.info('🎉 权限相关服务初始化完成');
|
logger.info('🎉 权限相关服务初始化完成');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ 权限相关服务初始化失败:', error);
|
logger.error('❌ 权限相关服务初始化失败:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -314,19 +320,35 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
logger.info('💪 初始化健康监听服务...');
|
logger.info('💪 初始化健康监听服务...');
|
||||||
|
|
||||||
// 并行初始化锻炼和睡眠监听
|
const [workoutResult, sleepResult] = await Promise.allSettled([
|
||||||
await Promise.allSettled([
|
workoutMonitorService.initialize(),
|
||||||
workoutMonitorService.initialize().then(() =>
|
sleepMonitorService.initialize(),
|
||||||
logger.info('✅ 锻炼监听服务初始化成功')
|
|
||||||
),
|
|
||||||
sleepMonitorService.initialize().then(() =>
|
|
||||||
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('🎉 健康监听服务初始化完成');
|
logger.info('🎉 健康监听服务初始化完成');
|
||||||
|
} else {
|
||||||
|
logger.warn('⚠️ 健康监听服务部分未能初始化成功,请检查上述错误日志');
|
||||||
|
}
|
||||||
|
|
||||||
|
return workoutReady && sleepReady;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('❌ 健康监听服务初始化失败:', error);
|
logger.error('❌ 健康监听服务初始化失败:', error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -359,14 +381,21 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 执行权限相关初始化
|
const runInitializationSequence = async () => {
|
||||||
initializePermissionServices();
|
try {
|
||||||
|
await initializePermissionServices();
|
||||||
|
} catch {
|
||||||
|
logger.warn('⚠️ 权限相关服务初始化失败,将继续启动后台和空闲服务以便后续重试');
|
||||||
|
}
|
||||||
|
|
||||||
// 交互完成后执行后台服务
|
// 交互完成后执行后台服务
|
||||||
initializeBackgroundServices();
|
initializeBackgroundServices();
|
||||||
|
|
||||||
// 空闲时执行非关键服务
|
// 空闲时执行非关键服务
|
||||||
initializeIdleServices();
|
initializeIdleServices();
|
||||||
|
};
|
||||||
|
|
||||||
|
runInitializationSequence();
|
||||||
|
|
||||||
}, [onboardingCompleted, profile.name]);
|
}, [onboardingCompleted, profile.name]);
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.25</string>
|
<string>1.0.26</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { analyzeWorkoutAndSendNotification } from './workoutNotificationService'
|
|||||||
const { HealthKitManager } = NativeModules;
|
const { HealthKitManager } = NativeModules;
|
||||||
const workoutEmitter = new NativeEventEmitter(HealthKitManager);
|
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 {
|
class WorkoutMonitorService {
|
||||||
private static instance: WorkoutMonitorService;
|
private static instance: WorkoutMonitorService;
|
||||||
private isInitialized = false;
|
private isInitialized = false;
|
||||||
@@ -111,20 +114,46 @@ class WorkoutMonitorService {
|
|||||||
try {
|
try {
|
||||||
console.log('检查新的锻炼记录...');
|
console.log('检查新的锻炼记录...');
|
||||||
|
|
||||||
// 获取最近1小时的锻炼记录
|
const lookbackWindowMs = this.lastProcessedWorkoutId
|
||||||
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
|
? 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({
|
const recentWorkouts = await fetchRecentWorkouts({
|
||||||
startDate: oneHourAgo.toISOString(),
|
startDate: startDate.toISOString(),
|
||||||
endDate: new Date().toISOString(),
|
endDate: endDate.toISOString(),
|
||||||
limit: 10
|
limit: 10
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`找到 ${recentWorkouts.length} 条最近的锻炼记录`);
|
console.log(`找到 ${recentWorkouts.length} 条最近的锻炼记录`);
|
||||||
|
|
||||||
// 检查是否有新的锻炼记录
|
if (this.lastProcessedWorkoutId && !recentWorkouts.some(workout => workout.id === this.lastProcessedWorkoutId)) {
|
||||||
|
console.warn('上次处理的锻炼记录不在当前查询窗口内,可能存在漏报风险');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWorkouts: WorkoutData[] = [];
|
||||||
for (const workout of recentWorkouts) {
|
for (const workout of recentWorkouts) {
|
||||||
if (workout.id !== this.lastProcessedWorkoutId) {
|
if (workout.id === this.lastProcessedWorkoutId) {
|
||||||
console.log('检测到新锻炼:', {
|
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,
|
id: workout.id,
|
||||||
type: workout.workoutActivityTypeString,
|
type: workout.workoutActivityTypeString,
|
||||||
duration: workout.duration,
|
duration: workout.duration,
|
||||||
@@ -132,11 +161,10 @@ class WorkoutMonitorService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await this.processNewWorkout(workout);
|
await this.processNewWorkout(workout);
|
||||||
await this.saveLastProcessedWorkoutId(workout.id);
|
|
||||||
} else {
|
|
||||||
console.log('锻炼已处理过,跳过:', workout.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.saveLastProcessedWorkoutId(newWorkouts[0].id);
|
||||||
|
console.log('锻炼处理完成,最新处理的锻炼ID:', newWorkouts[0].id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查新锻炼失败:', error);
|
console.error('检查新锻炼失败:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ export enum SleepStage {
|
|||||||
REM = 'REM'
|
REM = 'REM'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 阶段中被视为真正睡着的类型
|
||||||
|
const ACTIVE_SLEEP_STAGE_SET = new Set<SleepStage>([
|
||||||
|
SleepStage.Asleep,
|
||||||
|
SleepStage.Core,
|
||||||
|
SleepStage.Deep,
|
||||||
|
SleepStage.REM
|
||||||
|
]);
|
||||||
|
|
||||||
// 睡眠质量评级
|
// 睡眠质量评级
|
||||||
export enum SleepQuality {
|
export enum SleepQuality {
|
||||||
Poor = 'poor',
|
Poor = 'poor',
|
||||||
@@ -219,12 +227,13 @@ export const calculateSleepStageStats = (samples: SleepSample[]): SleepStageStat
|
|||||||
samples.forEach(sample => {
|
samples.forEach(sample => {
|
||||||
const startTime = dayjs(sample.startDate);
|
const startTime = dayjs(sample.startDate);
|
||||||
const endTime = dayjs(sample.endDate);
|
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;
|
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()));
|
console.log('[Sleep] 阶段时间统计:', Array.from(stageMap.entries()));
|
||||||
@@ -234,7 +243,7 @@ export const calculateSleepStageStats = (samples: SleepSample[]): SleepStageStat
|
|||||||
.filter(([stage]) => stage !== SleepStage.InBed)
|
.filter(([stage]) => stage !== SleepStage.InBed)
|
||||||
.reduce((total, [, duration]) => total + duration, 0);
|
.reduce((total, [, duration]) => total + duration, 0);
|
||||||
|
|
||||||
console.log('[Sleep] 实际睡眠时间(包含醒来):', actualSleepTime, '分钟');
|
console.log('[Sleep] 实际睡眠时间(包含醒来):', Math.round(actualSleepTime), '分钟');
|
||||||
|
|
||||||
const stats: SleepStageStats[] = [];
|
const stats: SleepStageStats[] = [];
|
||||||
|
|
||||||
@@ -243,6 +252,7 @@ export const calculateSleepStageStats = (samples: SleepSample[]): SleepStageStat
|
|||||||
if (stage === SleepStage.InBed) return;
|
if (stage === SleepStage.InBed) return;
|
||||||
|
|
||||||
const percentage = actualSleepTime > 0 ? (duration / actualSleepTime) * 100 : 0;
|
const percentage = actualSleepTime > 0 ? (duration / actualSleepTime) * 100 : 0;
|
||||||
|
const roundedDuration = Math.round(duration);
|
||||||
let quality: SleepQuality;
|
let quality: SleepQuality;
|
||||||
|
|
||||||
// 根据不同阶段的标准百分比评估质量
|
// 根据不同阶段的标准百分比评估质量
|
||||||
@@ -278,12 +288,14 @@ export const calculateSleepStageStats = (samples: SleepSample[]): SleepStageStat
|
|||||||
|
|
||||||
stats.push({
|
stats.push({
|
||||||
stage,
|
stage,
|
||||||
duration,
|
duration: roundedDuration,
|
||||||
percentage: Math.round(percentage),
|
percentage: Math.round(percentage),
|
||||||
quality
|
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);
|
const sortedStats = stats.sort((a, b) => b.duration - a.duration);
|
||||||
@@ -410,10 +422,15 @@ export const fetchCompleteSleepData = async (date: Date): Promise<CompleteSleepD
|
|||||||
const sleepStages = calculateSleepStageStats(sleepSamples);
|
const sleepStages = calculateSleepStageStats(sleepSamples);
|
||||||
|
|
||||||
// 计算总睡眠时间(排除在床时间和醒来时间)
|
// 计算总睡眠时间(排除在床时间和醒来时间)
|
||||||
const actualSleepStages = sleepStages.filter(stage =>
|
const totalSleepMinutes = sleepSamples.reduce((total, sample) => {
|
||||||
stage.stage !== SleepStage.InBed
|
if (!ACTIVE_SLEEP_STAGE_SET.has(sample.value)) {
|
||||||
);
|
return total;
|
||||||
const totalSleepTime = actualSleepStages.reduce((total, stage) => total + stage.duration, 0);
|
}
|
||||||
|
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;
|
const sleepEfficiency = timeInBed > 0 ? Math.round((totalSleepTime / timeInBed) * 100) : 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user