feat: 增加睡眠分析通知功能,支持睡眠质量评估与建议
This commit is contained in:
@@ -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('每日健康数据无变化,跳过同步');
|
||||
|
||||
@@ -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;
|
||||
|
||||
// 便捷方法
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<{
|
||||
|
||||
Reference in New Issue
Block a user