Files
digital-pilates/services/sleepNotificationService.ts

184 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 睡眠通知服务
*
* 负责在睡眠分析完成后发送通知,提供睡眠质量评估和建议
*/
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);
/**
* 分析睡眠数据并发送通知
*/
export async function analyzeSleepAndSendNotification(
analysis: SleepAnalysisData
): Promise<void> {
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 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}`,
data: {
type: 'sleep_analysis',
score: sleepScore,
quality,
date: sleepDate, // 添加日期参数,用于点击通知后跳转
analysis: JSON.stringify(analysis),
url: '/sleep-detail', // 点击通知跳转到睡眠详情页
},
sound: 'default',
badge: 1,
};
}
/**
* 获取质量配置
*/
function getQualityConfig(quality: string): {
emoji: string;
title: string;
} {
const configs: Record<string, { emoji: string; title: string }> = {
excellent: {
emoji: '🥳',
title: t('quality.excellent'),
},
good: {
emoji: '☀️',
title: t('quality.good'),
},
fair: {
emoji: '🌤️',
title: t('quality.fair'),
},
poor: {
emoji: '🌛',
title: t('quality.poor'),
},
very_poor: {
emoji: '🫂',
title: t('quality.veryPoor'),
},
};
return configs[quality] || {
emoji: '🛏️',
title: t('quality.default'),
};
}
/**
* 格式化睡眠时长
*/
function formatSleepDuration(hours: number): string {
const h = Math.floor(hours);
const m = Math.round((hours - h) * 60);
if (m === 0) {
return t('duration.hoursOnly', { hours: h });
}
return t('duration.hoursAndMinutes', { hours: h, minutes: m });
}
/**
* 获取睡眠建议
*/
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)]}`;
}
// 根据具体问题给出温暖的建议
const suggestions: string[] = [];
if (totalSleepHours < 7) {
suggestions.push(t('tips.suggestions.shortSleep'));
} else if (totalSleepHours > 9) {
suggestions.push(t('tips.suggestions.longSleep'));
}
if (deepSleepPercentage < 13) {
suggestions.push(t('tips.suggestions.lowDeepSleep'));
}
if (remSleepPercentage < 20) {
suggestions.push(t('tips.suggestions.lowRemSleep'));
}
if (sleepEfficiency < 85) {
suggestions.push(t('tips.suggestions.lowEfficiency'));
}
// 如果有具体建议,返回第一条;否则返回通用建议
if (suggestions.length > 0) {
return `💡 ${suggestions[0]}`;
}
return `💡 ${t('tips.general')}`;
}