feat: 增加睡眠分析通知功能,支持睡眠质量评估与建议

This commit is contained in:
richarjiang
2025-12-03 10:13:14 +08:00
parent 02b2de3ea3
commit e713ffbace
14 changed files with 190 additions and 415 deletions

View File

@@ -16,10 +16,12 @@ import {
fetchOxygenSaturation,
fetchPersonalHealthData,
fetchSmartHRVData,
fetchStepCount,
saveHeight,
saveWeight
} from '@/utils/health';
import AsyncStorage from '@/utils/kvStore';
import { logger } from '@/utils/logger';
import { convertHrvToStressIndex } from '@/utils/stress';
import dayjs from 'dayjs';
import { DailyHealthDataDto, updateDailyHealthData } from './users';
@@ -468,8 +470,6 @@ export async function getSyncStatusInfo(): Promise<SyncStatus | null> {
* @param waterIntake - 当日饮水量(从应用内部获取,因为 HealthKit 可能不包含应用内记录)
*/
export async function syncDailyHealthReport(waterIntake?: number): Promise<boolean> {
console.log('=== 开始同步每日健康报表 ===');
try {
const today = new Date();
const dateStr = dayjs(today).format('YYYY-MM-DD');
@@ -483,7 +483,8 @@ export async function syncDailyHealthReport(waterIntake?: number): Promise<boole
exerciseMinutesData,
standHoursData,
oxygenSaturation,
hrvData
hrvData,
stepCount
] = await Promise.all([
// 卡路里
fetchActiveEnergyBurned({
@@ -507,7 +508,9 @@ export async function syncDailyHealthReport(waterIntake?: number): Promise<boole
endDate: dayjs(today).endOf('day').toISOString()
}),
// HRV (用于计算压力)
fetchSmartHRVData(today)
fetchSmartHRVData(today),
// 步数
fetchStepCount(today)
]);
// 2. 数据处理与计算
@@ -555,10 +558,11 @@ export async function syncDailyHealthReport(waterIntake?: number): Promise<boole
...(basalEnergy > 0 && { basalMetabolism: Math.round(basalEnergy) }),
...(sleepMinutes > 0 && { sleepMinutes }),
...(oxygenSaturation !== null && oxygenSaturation > 0 && { bloodOxygen: oxygenSaturation }),
...(stressLevel > 0 && { stressLevel })
...(stressLevel > 0 && { stressLevel }),
...(stepCount > 0 && { steps: Math.round(stepCount) })
};
console.log('准备同步每日健康数据:', healthData);
logger.info('准备同步每日健康数据:', healthData);
// 4. 检查是否需要同步 (与上次同步的数据比较)
const lastSyncStatusStr = await AsyncStorage.getItem(DAILY_HEALTH_SYNC_KEY);
@@ -576,7 +580,8 @@ export async function syncDailyHealthReport(waterIntake?: number): Promise<boole
healthData.basalMetabolism !== lastData.basalMetabolism ||
healthData.sleepMinutes !== lastData.sleepMinutes ||
healthData.bloodOxygen !== lastData.bloodOxygen ||
healthData.stressLevel !== lastData.stressLevel;
healthData.stressLevel !== lastData.stressLevel ||
healthData.steps !== lastData.steps;
if (!isDifferent) {
console.log('每日健康数据无变化,跳过同步');

View File

@@ -265,6 +265,15 @@ export class NotificationService {
console.log('用户点击了 HRV 压力通知', data);
const targetUrl = (data?.url as string) || '/(tabs)/statistics';
router.push(targetUrl as any);
} else if (data?.type === NotificationTypes.SLEEP_ANALYSIS || data?.type === NotificationTypes.SLEEP_REMINDER) {
// 处理睡眠分析通知
console.log('用户点击了睡眠分析通知', data);
// 从通知数据中获取日期,如果没有则使用今天
const sleepDate = data?.date as string || new Date().toISOString().split('T')[0];
router.push({
pathname: ROUTES.SLEEP_DETAIL,
params: { date: sleepDate },
} as any);
}
}
@@ -607,6 +616,8 @@ export const NotificationTypes = {
FASTING_START: 'fasting_start',
FASTING_END: 'fasting_end',
HRV_STRESS_ALERT: 'hrv_stress_alert',
SLEEP_ANALYSIS: 'sleep_analysis',
SLEEP_REMINDER: 'sleep_reminder',
} as const;
// 便捷方法

View File

@@ -4,10 +4,14 @@
* 负责在睡眠分析完成后发送通知,提供睡眠质量评估和建议
*/
import i18n from '@/i18n';
import { logger } from '@/utils/logger';
import dayjs from 'dayjs';
import * as Notifications from 'expo-notifications';
import { SleepAnalysisData } from './sleepMonitor';
const t = (key: string, options?: Record<string, unknown>) => i18n.t(`sleepNotification.${key}`, options);
/**
* 分析睡眠数据并发送通知
*/
@@ -51,12 +55,22 @@ function buildSleepNotification(analysis: SleepAnalysisData): Notifications.Noti
// 构建通知正文
const sleepDuration = formatSleepDuration(totalSleepHours);
const efficiencyText = `睡眠效率 ${sleepEfficiency.toFixed(0)}%`;
const body = `您昨晚睡了 ${sleepDuration}${efficiencyText}。评分:${sleepScore}`;
const body = t('body', {
duration: sleepDuration,
efficiency: sleepEfficiency.toFixed(0),
score: sleepScore
});
// 获取建议
const suggestion = getSleepSuggestion(analysis);
// 计算睡眠日期
// 睡眠详情页面使用的日期逻辑是传入的日期会查询从前一天18:00到当天12:00的数据
// 所以我们应该传递醒来的日期sessionEnd这样用户点击通知后能看到正确的睡眠数据
const sleepDate = analysis.sessionEnd
? dayjs(analysis.sessionEnd).format('YYYY-MM-DD')
: dayjs().format('YYYY-MM-DD');
return {
title,
body: `${body}\n${suggestion}`,
@@ -64,6 +78,7 @@ function buildSleepNotification(analysis: SleepAnalysisData): Notifications.Noti
type: 'sleep_analysis',
score: sleepScore,
quality,
date: sleepDate, // 添加日期参数,用于点击通知后跳转
analysis: JSON.stringify(analysis),
url: '/sleep-detail', // 点击通知跳转到睡眠详情页
},
@@ -79,32 +94,32 @@ function getQualityConfig(quality: string): {
emoji: string;
title: string;
} {
const configs = {
const configs: Record<string, { emoji: string; title: string }> = {
excellent: {
emoji: '😴',
title: '睡眠质量优秀',
emoji: '🥳',
title: t('quality.excellent'),
},
good: {
emoji: '😊',
title: '睡眠质量良好',
emoji: '☀️',
title: t('quality.good'),
},
fair: {
emoji: '😐',
title: '睡眠质量一般',
emoji: '🌤️',
title: t('quality.fair'),
},
poor: {
emoji: '😟',
title: '睡眠质量较差',
emoji: '🌛',
title: t('quality.poor'),
},
very_poor: {
emoji: '😰',
title: '睡眠质量很差',
emoji: '🫂',
title: t('quality.veryPoor'),
},
};
return configs[quality as keyof typeof configs] || {
emoji: '💤',
title: '睡眠分析完成',
return configs[quality] || {
emoji: '🛏️',
title: t('quality.default'),
};
}
@@ -116,9 +131,9 @@ function formatSleepDuration(hours: number): string {
const m = Math.round((hours - h) * 60);
if (m === 0) {
return `${h}小时`;
return t('duration.hoursOnly', { hours: h });
}
return `${h}小时${m}分钟`;
return t('duration.hoursAndMinutes', { hours: h, minutes: m });
}
/**
@@ -127,35 +142,36 @@ function formatSleepDuration(hours: number): string {
function getSleepSuggestion(analysis: SleepAnalysisData): string {
const { quality, totalSleepHours, deepSleepPercentage, remSleepPercentage, sleepEfficiency } = analysis;
// 优秀或良好的睡眠
// 优秀或良好的睡眠 - 给予鼓励
if (quality === 'excellent' || quality === 'good') {
const tips = [
'继续保持良好的睡眠习惯!',
'坚持规律作息,身体会感谢你!',
'优质睡眠让你精力充沛!',
t('tips.excellent.keepItUp'),
t('tips.excellent.greatJob'),
t('tips.excellent.energized'),
t('tips.excellent.proud'),
];
return tips[Math.floor(Math.random() * tips.length)];
return `${tips[Math.floor(Math.random() * tips.length)]}`;
}
// 根据具体问题给出建议
// 根据具体问题给出温暖的建议
const suggestions: string[] = [];
if (totalSleepHours < 7) {
suggestions.push('建议增加睡眠时间至7-9小时');
suggestions.push(t('tips.suggestions.shortSleep'));
} else if (totalSleepHours > 9) {
suggestions.push('睡眠时间偏长,注意睡眠质量');
suggestions.push(t('tips.suggestions.longSleep'));
}
if (deepSleepPercentage < 13) {
suggestions.push('深度睡眠不足,睡前避免使用电子设备');
suggestions.push(t('tips.suggestions.lowDeepSleep'));
}
if (remSleepPercentage < 20) {
suggestions.push('REM睡眠不足保持规律的作息时间');
suggestions.push(t('tips.suggestions.lowRemSleep'));
}
if (sleepEfficiency < 85) {
suggestions.push('睡眠效率较低,改善睡眠环境');
suggestions.push(t('tips.suggestions.lowEfficiency'));
}
// 如果有具体建议,返回第一条;否则返回通用建议
@@ -163,29 +179,5 @@ function getSleepSuggestion(analysis: SleepAnalysisData): string {
return `💡 ${suggestions[0]}`;
}
return '建议关注睡眠质量,保持良好作息';
return `💡 ${t('tips.general')}`;
}
/**
* 发送简单的睡眠提醒(用于测试)
*/
export async function sendSimpleSleepReminder(userName: string = '朋友'): Promise<void> {
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);
}
}

View File

@@ -64,6 +64,7 @@ export type DailyHealthDataDto = {
sleepMinutes?: number; // minutes
bloodOxygen?: number; // % (0-100)
stressLevel?: number; // ms (based on HRV)
steps?: number; // 步数
};
export async function updateDailyHealthData(dto: DailyHealthDataDto): Promise<{