Files
digital-pilates/utils/notificationHelpers.ts
richarjiang a34ca556e8 feat(notifications): 新增晚餐和心情提醒功能,支持HRV压力检测和后台处理
- 新增晚餐提醒(18:00)和心情提醒(21:00)的定时通知
- 实现基于HRV数据的压力检测和智能鼓励通知
- 添加后台任务处理支持,修改iOS后台模式为processing
- 优化营养记录页面使用Redux状态管理,支持实时数据更新
- 重构卡路里计算公式,移除目标卡路里概念,改为基代+运动-饮食
- 新增营养目标动态计算功能,基于用户身体数据智能推荐
- 完善通知点击跳转逻辑,支持多种提醒类型的路由处理
2025-09-01 10:29:13 +08:00

811 lines
24 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 * as Notifications from 'expo-notifications';
import { NotificationData, notificationService } from '../services/notifications';
/**
* 构建 coach 页面的深度链接
*/
export function buildCoachDeepLink(params: {
action?: 'diet' | 'weight' | 'mood' | 'workout';
subAction?: 'record' | 'photo' | 'text' | 'card';
meal?: 'breakfast' | 'lunch' | 'dinner' | 'snack';
message?: string;
}): string {
const baseUrl = '/coach';
const searchParams = new URLSearchParams();
if (params.action) searchParams.set('action', params.action);
if (params.subAction) searchParams.set('subAction', params.subAction);
if (params.meal) searchParams.set('meal', params.meal);
if (params.message) searchParams.set('message', encodeURIComponent(params.message));
const queryString = searchParams.toString();
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
}
/**
* 运动相关的通知辅助函数
*/
export class WorkoutNotificationHelpers {
/**
* 发送运动开始提醒
*/
static async sendWorkoutStartReminder(userName: string) {
return notificationService.sendImmediateNotification({
title: '运动时间到',
body: `${userName},该开始今天的普拉提训练了!`,
data: { type: 'workout_start' },
sound: true,
priority: 'high',
});
}
/**
* 发送运动完成通知
*/
static async sendWorkoutCompleteNotification(userName: string, duration: number) {
return notificationService.sendImmediateNotification({
title: '运动完成',
body: `${userName},恭喜您完成了${duration}分钟的训练!`,
data: { type: 'workout_complete' },
sound: true,
priority: 'normal',
});
}
/**
* 安排每日运动提醒
*/
static async scheduleDailyWorkoutReminder(userName: string, hour: number = 9, minute: number = 0) {
const reminderTime = new Date();
reminderTime.setHours(hour, minute, 0, 0);
// 如果今天的时间已经过了,设置为明天
if (reminderTime.getTime() <= Date.now()) {
reminderTime.setDate(reminderTime.getDate() + 1);
}
return notificationService.scheduleRepeatingNotification(
{
title: '每日运动提醒',
body: `${userName},该开始今天的普拉提训练了!`,
data: { type: 'daily_workout_reminder' },
sound: true,
priority: 'high',
},
{ days: 1 }
);
}
}
/**
* 目标相关的通知辅助函数
*/
export class GoalNotificationHelpers {
/**
* 发送目标达成通知
*/
static async sendGoalAchievementNotification(userName: string, goalName: string) {
return notificationService.sendImmediateNotification({
title: '目标达成',
body: `${userName},恭喜您达成了目标:${goalName}`,
data: { type: 'goal_achievement', goalName },
sound: true,
priority: 'high',
});
}
/**
* 发送目标进度更新通知
*/
static async sendGoalProgressNotification(userName: string, goalName: string, progress: number) {
return notificationService.sendImmediateNotification({
title: '目标进度',
body: `${userName},您的目标"${goalName}"已完成${progress}%`,
data: { type: 'goal_progress', goalName, progress },
sound: true,
priority: 'normal',
});
}
/**
* 安排目标提醒
*/
static async scheduleGoalReminder(userName: string, goalName: string, deadline: Date) {
// 在截止日期前一天发送提醒
const reminderDate = new Date(deadline);
reminderDate.setDate(reminderDate.getDate() - 1);
return notificationService.scheduleNotificationAtDate(
{
title: '目标截止提醒',
body: `${userName},您的目标"${goalName}"明天就要截止了,加油!`,
data: { type: 'goal_deadline_reminder', goalName },
sound: true,
priority: 'high',
},
reminderDate
);
}
/**
* 根据目标设置创建定时推送
* @param goalData 目标数据
* @param userName 用户名
* @returns 通知ID数组
*/
static async scheduleGoalNotifications(
goalData: {
title: string;
repeatType: 'daily' | 'weekly' | 'monthly';
frequency: number;
hasReminder: boolean;
reminderTime?: string;
customRepeatRule?: {
weekdays?: number[];
dayOfMonth?: number[];
};
startTime?: number;
},
userName: string
): Promise<string[]> {
const notificationIds: string[] = [];
// 如果没有开启提醒,直接返回
if (!goalData.hasReminder || !goalData.reminderTime) {
console.log('目标未开启提醒或未设置提醒时间');
return notificationIds;
}
try {
// 解析提醒时间
const [hours, minutes] = goalData.reminderTime.split(':').map(Number);
// 创建通知内容
const notification: NotificationData = {
title: '目标提醒',
body: `${userName},该完成您的目标"${goalData.title}"了!`,
data: {
type: 'goal_reminder',
goalTitle: goalData.title,
repeatType: goalData.repeatType,
frequency: goalData.frequency
},
sound: true,
priority: 'high',
};
// 根据重复类型创建不同的通知
switch (goalData.repeatType) {
case 'daily':
// 每日重复 - 使用日历重复通知
const dailyId = await notificationService.scheduleCalendarRepeatingNotification(
notification,
{
type: Notifications.SchedulableTriggerInputTypes.DAILY,
hour: hours,
minute: minutes,
}
);
notificationIds.push(dailyId);
console.log(`已安排每日目标提醒通知ID${dailyId}`);
break;
case 'weekly':
// 每周重复 - 为每个选中的星期几创建单独的通知
if (goalData.customRepeatRule?.weekdays && goalData.customRepeatRule.weekdays.length > 0) {
for (const weekday of goalData.customRepeatRule.weekdays) {
const weeklyId = await notificationService.scheduleCalendarRepeatingNotification(
notification,
{
type: Notifications.SchedulableTriggerInputTypes.WEEKLY,
hour: hours,
minute: minutes,
weekdays: [weekday],
}
);
notificationIds.push(weeklyId);
console.log(`已安排每周目标提醒,星期${weekday}通知ID${weeklyId}`);
}
} else {
// 默认每周重复
const weeklyId = await notificationService.scheduleCalendarRepeatingNotification(
notification,
{
type: Notifications.SchedulableTriggerInputTypes.WEEKLY,
hour: hours,
minute: minutes,
}
);
notificationIds.push(weeklyId);
console.log(`已安排每周目标提醒通知ID${weeklyId}`);
}
break;
case 'monthly':
// 每月重复 - 为每个选中的日期创建单独的通知
if (goalData.customRepeatRule?.dayOfMonth && goalData.customRepeatRule.dayOfMonth.length > 0) {
for (const dayOfMonth of goalData.customRepeatRule.dayOfMonth) {
const monthlyId = await notificationService.scheduleCalendarRepeatingNotification(
notification,
{
type: Notifications.SchedulableTriggerInputTypes.MONTHLY,
hour: hours,
minute: minutes,
dayOfMonth: dayOfMonth,
}
);
notificationIds.push(monthlyId);
console.log(`已安排每月目标提醒,${dayOfMonth}通知ID${monthlyId}`);
}
} else {
// 默认每月重复
const monthlyId = await notificationService.scheduleCalendarRepeatingNotification(
notification,
{
type: Notifications.SchedulableTriggerInputTypes.MONTHLY,
hour: hours,
minute: minutes,
dayOfMonth: 1,
}
);
notificationIds.push(monthlyId);
console.log(`已安排每月目标提醒通知ID${monthlyId}`);
}
break;
}
console.log(`目标"${goalData.title}"的定时推送已创建完成,共${notificationIds.length}个通知`);
return notificationIds;
} catch (error) {
console.error('创建目标定时推送失败:', error);
throw error;
}
}
/**
* 取消特定目标的所有通知
*/
static async cancelGoalNotifications(goalTitle: string): Promise<void> {
try {
const notifications = await notificationService.getAllScheduledNotifications();
for (const notification of notifications) {
if (notification.content.data?.type === 'goal_reminder' &&
notification.content.data?.goalTitle === goalTitle) {
await notificationService.cancelNotification(notification.identifier);
console.log(`已取消目标"${goalTitle}"的通知:${notification.identifier}`);
}
}
} catch (error) {
console.error('取消目标通知失败:', error);
throw error;
}
}
}
/**
* 营养相关的通知辅助函数
*/
export class NutritionNotificationHelpers {
/**
* 发送营养记录提醒
*/
static async sendNutritionRecordReminder(userName: string) {
return notificationService.sendImmediateNotification({
title: '营养记录',
body: `${userName},记得记录今天的饮食情况`,
data: { type: 'nutrition_record_reminder' },
sound: true,
priority: 'normal',
});
}
/**
* 安排每日午餐提醒
* @param userName 用户名
* @param hour 小时 (默认12点)
* @param minute 分钟 (默认0分)
* @returns 通知ID
*/
static async scheduleDailyLunchReminder(
userName: string,
hour: number = 12,
minute: number = 0
): Promise<string | null> {
try {
// 检查是否已经存在午餐提醒
const existingNotifications = await notificationService.getAllScheduledNotifications();
console.log('existingNotifications', existingNotifications);
const existingLunchReminder = existingNotifications.find(
notification =>
notification.content.data?.type === 'lunch_reminder' &&
notification.content.data?.isDailyReminder === true
);
if (existingLunchReminder) {
console.log('午餐提醒已存在,跳过重复注册:', existingLunchReminder.identifier);
return existingLunchReminder.identifier;
}
// 构建跳转到 coach 页面的深度链接
const coachUrl = buildCoachDeepLink({
action: 'diet',
subAction: 'card',
meal: 'lunch'
});
// 创建午餐提醒通知
const notificationId = await notificationService.scheduleCalendarRepeatingNotification(
{
title: '午餐记录提醒',
body: `${userName},记得记录今天的午餐情况哦!`,
data: {
type: 'lunch_reminder',
isDailyReminder: true,
meal: '午餐',
url: coachUrl // 添加深度链接
},
sound: true,
priority: 'normal',
},
{
type: Notifications.SchedulableTriggerInputTypes.DAILY,
hour: hour,
minute: minute,
}
);
console.log('每日午餐提醒已安排ID:', notificationId);
return notificationId;
} catch (error) {
console.error('安排每日午餐提醒失败:', error);
throw error;
}
}
/**
* 发送午餐记录提醒
*/
static async sendLunchReminder(userName: string) {
const coachUrl = buildCoachDeepLink({
action: 'diet',
subAction: 'card',
meal: 'lunch'
});
return notificationService.sendImmediateNotification({
title: '午餐记录提醒',
body: `${userName},记得记录今天的午餐情况哦!`,
data: {
type: 'lunch_reminder',
meal: '午餐',
url: coachUrl
},
sound: true,
priority: 'normal',
});
}
/**
* 取消午餐提醒
*/
static async cancelLunchReminder(): Promise<void> {
try {
const notifications = await notificationService.getAllScheduledNotifications();
for (const notification of notifications) {
if (notification.content.data?.type === 'lunch_reminder' &&
notification.content.data?.isDailyReminder === true) {
await notificationService.cancelNotification(notification.identifier);
console.log('已取消午餐提醒:', notification.identifier);
}
}
} catch (error) {
console.error('取消午餐提醒失败:', error);
throw error;
}
}
/**
* 安排每日晚餐提醒
* @param userName 用户名
* @param hour 小时 (默认18点)
* @param minute 分钟 (默认0分)
* @returns 通知ID
*/
static async scheduleDailyDinnerReminder(
userName: string,
hour: number = 18,
minute: number = 0
): Promise<string | null> {
try {
// 检查是否已经存在晚餐提醒
const existingNotifications = await notificationService.getAllScheduledNotifications();
const existingDinnerReminder = existingNotifications.find(
notification =>
notification.content.data?.type === 'dinner_reminder' &&
notification.content.data?.isDailyReminder === true
);
if (existingDinnerReminder) {
console.log('晚餐提醒已存在,跳过重复注册:', existingDinnerReminder.identifier);
return existingDinnerReminder.identifier;
}
// 创建晚餐提醒通知
const notificationId = await notificationService.scheduleCalendarRepeatingNotification(
{
title: '🍽️ 晚餐时光到啦!',
body: `${userName},美好的晚餐时光开始了~记得记录今天的晚餐哦!营养均衡很重要呢 💪`,
data: {
type: 'dinner_reminder',
isDailyReminder: true,
meal: '晚餐',
url: '/nutrition/records' // 直接跳转到营养记录页面
},
sound: true,
priority: 'normal',
},
{
type: Notifications.SchedulableTriggerInputTypes.DAILY,
hour: hour,
minute: minute,
}
);
console.log('每日晚餐提醒已安排ID:', notificationId);
return notificationId;
} catch (error) {
console.error('安排每日晚餐提醒失败:', error);
throw error;
}
}
/**
* 取消晚餐提醒
*/
static async cancelDinnerReminder(): Promise<void> {
try {
const notifications = await notificationService.getAllScheduledNotifications();
for (const notification of notifications) {
if (notification.content.data?.type === 'dinner_reminder' &&
notification.content.data?.isDailyReminder === true) {
await notificationService.cancelNotification(notification.identifier);
console.log('已取消晚餐提醒:', notification.identifier);
}
}
} catch (error) {
console.error('取消晚餐提醒失败:', error);
throw error;
}
}
/**
* 安排营养记录提醒
*/
static async scheduleNutritionReminders(userName: string) {
// 安排三餐提醒
const mealTimes = [
{ hour: 8, minute: 0, meal: '早餐' },
{ hour: 12, minute: 0, meal: '午餐' },
{ hour: 18, minute: 0, meal: '晚餐' },
];
const notifications = [];
for (const mealTime of mealTimes) {
const reminderTime = new Date();
reminderTime.setHours(mealTime.hour, mealTime.minute, 0, 0);
// 如果今天的时间已经过了,设置为明天
if (reminderTime.getTime() <= Date.now()) {
reminderTime.setDate(reminderTime.getDate() + 1);
}
// 构建深度链接
const mealTypeMap: Record<string, string> = {
'早餐': 'breakfast',
'午餐': 'lunch',
'晚餐': 'dinner'
};
const coachUrl = buildCoachDeepLink({
action: 'diet',
subAction: 'card',
meal: mealTypeMap[mealTime.meal] as 'breakfast' | 'lunch' | 'dinner'
});
const notificationId = await notificationService.scheduleRepeatingNotification(
{
title: `${mealTime.meal}提醒`,
body: `${userName},记得记录您的${mealTime.meal}情况`,
data: {
type: 'meal_reminder',
meal: mealTime.meal,
url: coachUrl
},
sound: true,
priority: 'normal',
},
{ days: 1 }
);
notifications.push(notificationId);
}
return notifications;
}
}
/**
* 心情相关的通知辅助函数
*/
export class MoodNotificationHelpers {
/**
* 安排每日心情提醒
* @param userName 用户名
* @param hour 小时 (默认21点)
* @param minute 分钟 (默认0分)
* @returns 通知ID
*/
static async scheduleDailyMoodReminder(
userName: string,
hour: number = 21,
minute: number = 0
): Promise<string | null> {
try {
// 检查是否已经存在心情提醒
const existingNotifications = await notificationService.getAllScheduledNotifications();
const existingMoodReminder = existingNotifications.find(
notification =>
notification.content.data?.type === 'mood_reminder' &&
notification.content.data?.isDailyReminder === true
);
if (existingMoodReminder) {
console.log('心情提醒已存在,跳过重复注册:', existingMoodReminder.identifier);
return existingMoodReminder.identifier;
}
// 创建心情提醒通知
const notificationId = await notificationService.scheduleCalendarRepeatingNotification(
{
title: '🌙 今天过得怎么样呀?',
body: `${userName},夜深了~来记录一下今天的心情吧!每一份情感都值得被珍藏 ✨💕`,
data: {
type: 'mood_reminder',
isDailyReminder: true,
url: '/mood-statistics' // 跳转到心情统计页面
},
sound: true,
priority: 'normal',
},
{
type: Notifications.SchedulableTriggerInputTypes.DAILY,
hour: hour,
minute: minute,
}
);
console.log('每日心情提醒已安排ID:', notificationId);
return notificationId;
} catch (error) {
console.error('安排每日心情提醒失败:', error);
throw error;
}
}
/**
* 发送心情记录提醒
*/
static async sendMoodReminder(userName: string) {
return notificationService.sendImmediateNotification({
title: '🌙 今天过得怎么样呀?',
body: `${userName},夜深了~来记录一下今天的心情吧!每一份情感都值得被珍藏 ✨💕`,
data: {
type: 'mood_reminder',
url: '/mood-statistics'
},
sound: true,
priority: 'normal',
});
}
/**
* 取消心情提醒
*/
static async cancelMoodReminder(): Promise<void> {
try {
const notifications = await notificationService.getAllScheduledNotifications();
for (const notification of notifications) {
if (notification.content.data?.type === 'mood_reminder' &&
notification.content.data?.isDailyReminder === true) {
await notificationService.cancelNotification(notification.identifier);
console.log('已取消心情提醒:', notification.identifier);
}
}
} catch (error) {
console.error('取消心情提醒失败:', error);
throw error;
}
}
}
/**
* 通用通知辅助函数
*/
export class GeneralNotificationHelpers {
/**
* 发送欢迎通知
*/
static async sendWelcomeNotification(userName: string) {
return notificationService.sendImmediateNotification({
title: '欢迎使用',
body: `${userName},欢迎来到普拉提世界!开始您的健康之旅吧`,
data: { type: 'welcome' },
sound: true,
priority: 'normal',
});
}
/**
* 发送成就通知
*/
static async sendAchievementNotification(userName: string, achievement: string) {
return notificationService.sendImmediateNotification({
title: '新成就',
body: `${userName},恭喜您获得了新成就:${achievement}`,
data: { type: 'achievement', achievement },
sound: true,
priority: 'high',
});
}
/**
* 发送系统维护通知
*/
static async sendMaintenanceNotification(message: string) {
return notificationService.sendImmediateNotification({
title: '系统通知',
body: message,
data: { type: 'maintenance' },
sound: true,
priority: 'high',
});
}
/**
* 取消特定类型的通知
*/
static async cancelNotificationsByType(type: string) {
const notifications = await notificationService.getAllScheduledNotifications();
for (const notification of notifications) {
if (notification.content.data?.type === type) {
await notificationService.cancelNotification(notification.identifier);
}
}
}
/**
* 批量发送通知
*/
static async sendBatchNotifications(notifications: NotificationData[]) {
const results = [];
for (const notification of notifications) {
try {
const id = await notificationService.sendImmediateNotification(notification);
results.push({ success: true, id, notification });
} catch (error) {
results.push({ success: false, error, notification });
}
}
return results;
}
}
/**
* 通知模板
*/
export const NotificationTemplates = {
workout: {
start: (userName: string) => ({
title: '运动时间到',
body: `${userName},该开始今天的普拉提训练了!`,
data: { type: 'workout_start' },
sound: true,
priority: 'high' as const,
}),
complete: (userName: string, duration: number) => ({
title: '运动完成',
body: `${userName},恭喜您完成了${duration}分钟的训练!`,
data: { type: 'workout_complete' },
sound: true,
priority: 'normal' as const,
}),
},
goal: {
achievement: (userName: string, goalName: string) => ({
title: '目标达成',
body: `${userName},恭喜您达成了目标:${goalName}`,
data: { type: 'goal_achievement', goalName },
sound: true,
priority: 'high' as const,
}),
progress: (userName: string, goalName: string, progress: number) => ({
title: '目标进度',
body: `${userName},您的目标"${goalName}"已完成${progress}%`,
data: { type: 'goal_progress', goalName, progress },
sound: true,
priority: 'normal' as const,
}),
},
mood: {
reminder: (userName: string) => ({
title: '心情打卡',
body: `${userName},记得记录今天的心情状态哦`,
data: { type: 'mood_checkin_reminder' },
sound: true,
priority: 'normal' as const,
}),
},
nutrition: {
reminder: (userName: string, meal: string) => {
const mealTypeMap: Record<string, string> = {
'早餐': 'breakfast',
'午餐': 'lunch',
'晚餐': 'dinner',
'加餐': 'snack'
};
const coachUrl = buildCoachDeepLink({
action: 'diet',
subAction: 'card',
meal: mealTypeMap[meal] as 'breakfast' | 'lunch' | 'dinner' | 'snack'
});
return {
title: `${meal}提醒`,
body: `${userName},记得记录您的${meal}情况`,
data: {
type: 'meal_reminder',
meal,
url: coachUrl
},
sound: true,
priority: 'normal' as const,
};
},
lunch: (userName: string) => {
const coachUrl = buildCoachDeepLink({
action: 'diet',
subAction: 'card',
meal: 'lunch'
});
return {
title: '午餐记录提醒',
body: `${userName},记得记录今天的午餐情况哦!`,
data: {
type: 'lunch_reminder',
meal: '午餐',
url: coachUrl
},
sound: true,
priority: 'normal' as const,
};
},
},
};