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

496 lines
14 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 { router } from 'expo-router';
// 配置通知处理方式
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
shouldShowBanner: true,
shouldShowList: true,
}),
});
export interface NotificationData {
title: string;
body: string;
data?: Record<string, unknown>;
sound?: boolean;
priority?: 'default' | 'normal' | 'high';
vibrate?: number[];
}
interface DateTrigger {
type: Notifications.SchedulableTriggerInputTypes.DATE;
date: number;
}
interface RepeatingTrigger {
type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL;
seconds: number;
repeats?: boolean;
}
interface DailyTrigger {
type: Notifications.SchedulableTriggerInputTypes.DAILY;
hour: number;
minute: number;
}
interface WeeklyTrigger {
type: Notifications.SchedulableTriggerInputTypes.WEEKLY;
hour: number;
minute: number;
weekday: number;
}
interface MonthlyTrigger {
type: Notifications.SchedulableTriggerInputTypes.MONTHLY;
hour: number;
minute: number;
day: number;
}
type CalendarTrigger = DailyTrigger | WeeklyTrigger | MonthlyTrigger;
export class NotificationService {
private static instance: NotificationService;
private isInitialized = false;
private constructor() { }
public static getInstance(): NotificationService {
if (!NotificationService.instance) {
NotificationService.instance = new NotificationService();
}
return NotificationService.instance;
}
/**
* 初始化推送通知服务
*/
async initialize(): Promise<void> {
if (this.isInitialized) return;
try {
// 请求通知权限
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.warn('推送通知权限未授予');
return;
}
// 获取推送令牌(用于远程推送,本地推送不需要)
// if (Platform.OS !== 'web') {
// const token = await Notifications.getExpoPushTokenAsync({
// projectId: 'your-project-id', // 需要替换为实际的Expo项目ID
// });
// console.log('推送令牌:', token.data);
// }
// 设置通知监听器
this.setupNotificationListeners();
// 检查已存在的通知
const existingNotifications = await this.getAllScheduledNotifications();
console.log('已存在的通知数量:', existingNotifications.length);
this.isInitialized = true;
console.log('推送通知服务初始化成功');
} catch (error) {
console.error('推送通知服务初始化失败:', error);
}
}
/**
* 设置通知监听器
*/
private setupNotificationListeners(): void {
// 监听通知接收
Notifications.addNotificationReceivedListener((notification) => {
console.log('收到通知:', notification);
});
// 监听通知点击
Notifications.addNotificationResponseReceivedListener((response) => {
console.log('用户点击了通知:', response);
// 这里可以处理通知点击后的逻辑
this.handleNotificationResponse(response);
});
}
/**
* 处理通知响应
*/
private handleNotificationResponse(response: Notifications.NotificationResponse): void {
const { notification } = response;
const data = notification.request.content.data;
console.log('处理通知点击:', data);
// 根据通知类型处理不同的逻辑
if (data?.type === 'workout_reminder') {
// 处理运动提醒
console.log('用户点击了运动提醒通知');
} else if (data?.type === 'goal_achievement') {
// 处理目标达成通知
console.log('用户点击了目标达成通知');
} else if (data?.type === 'mood_checkin') {
// 处理心情打卡提醒
console.log('用户点击了心情打卡提醒');
} else if (data?.type === 'goal_reminder') {
// 处理目标提醒通知
console.log('用户点击了目标提醒通知', data);
// 这里可以添加导航到目标页面的逻辑
} else if (data?.type === 'lunch_reminder') {
// 处理午餐提醒通知
console.log('用户点击了午餐提醒通知', data);
// 跳转到营养记录页面
if (data?.url) {
router.push(data.url as any);
}
} else if (data?.type === 'dinner_reminder') {
// 处理晚餐提醒通知
console.log('用户点击了晚餐提醒通知', data);
// 跳转到营养记录页面
if (data?.url) {
router.push(data.url as any);
}
} else if (data?.type === 'mood_reminder') {
// 处理心情提醒通知
console.log('用户点击了心情提醒通知', data);
// 跳转到心情页面
if (data?.url) {
router.push(data.url as any);
}
}
}
/**
* 发送本地推送通知
*/
async scheduleLocalNotification(
notification: NotificationData,
trigger?: Notifications.NotificationTriggerInput
): Promise<string> {
try {
const notificationId = await Notifications.scheduleNotificationAsync({
content: {
title: notification.title,
body: notification.body,
data: notification.data || {},
sound: notification.sound ? 'default' : undefined,
priority: notification.priority || 'default',
vibrate: notification.vibrate,
},
trigger: trigger || null, // null表示立即发送
});
console.log('本地通知已安排ID:', notificationId);
return notificationId;
} catch (error) {
console.error('安排本地通知失败:', error);
throw error;
}
}
/**
* 发送立即通知
*/
async sendImmediateNotification(notification: NotificationData): Promise<string> {
return this.scheduleLocalNotification(notification);
}
/**
* 安排定时通知
*/
async scheduleNotificationAtDate(
notification: NotificationData,
date: Date
): Promise<string> {
try {
const notificationId = await Notifications.scheduleNotificationAsync({
content: {
title: notification.title,
body: notification.body,
data: notification.data || {},
sound: notification.sound ? 'default' : undefined,
priority: notification.priority || 'default',
vibrate: notification.vibrate,
},
trigger: {
type: Notifications.SchedulableTriggerInputTypes.DATE,
date: date.getTime(),
} as DateTrigger,
});
console.log('定时通知已安排ID:', notificationId);
return notificationId;
} catch (error) {
console.error('安排定时通知失败:', error);
throw error;
}
}
/**
* 安排重复通知(仅支持秒级别)
*/
async scheduleRepeatingNotification(
notification: NotificationData,
interval: {
seconds?: number;
minutes?: number;
hours?: number;
days?: number;
weeks?: number;
months?: number;
years?: number;
}
): Promise<string> {
try {
// 计算总秒数
const totalSeconds =
(interval.seconds || 0) +
(interval.minutes || 0) * 60 +
(interval.hours || 0) * 3600 +
(interval.days || 0) * 86400 +
(interval.weeks || 0) * 604800 +
(interval.months || 0) * 2592000 +
(interval.years || 0) * 31536000;
if (totalSeconds <= 0) {
throw new Error('重复间隔必须大于0');
}
const notificationId = await Notifications.scheduleNotificationAsync({
content: {
title: notification.title,
body: notification.body,
data: notification.data || {},
sound: notification.sound ? 'default' : undefined,
priority: notification.priority || 'default',
vibrate: notification.vibrate,
},
trigger: {
type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL,
seconds: totalSeconds,
repeats: true,
} as RepeatingTrigger,
});
console.log('重复通知已安排ID:', notificationId);
return notificationId;
} catch (error) {
console.error('安排重复通知失败:', error);
throw error;
}
}
/**
* 安排日历重复通知(支持每日、每周、每月)
*/
async scheduleCalendarRepeatingNotification(
notification: NotificationData,
options: {
type: Notifications.SchedulableTriggerInputTypes;
hour: number;
minute: number;
weekdays?: number[]; // 0-60为周日仅用于weekly类型
dayOfMonth?: number; // 1-31仅用于monthly类型
}
): Promise<string> {
try {
let trigger: CalendarTrigger;
switch (options.type) {
case Notifications.SchedulableTriggerInputTypes.DAILY:
trigger = {
type: Notifications.SchedulableTriggerInputTypes.DAILY,
hour: options.hour,
minute: options.minute,
};
break;
case Notifications.SchedulableTriggerInputTypes.WEEKLY:
if (options.weekdays && options.weekdays.length > 0) {
trigger = {
type: Notifications.SchedulableTriggerInputTypes.WEEKLY,
hour: options.hour,
minute: options.minute,
weekday: options.weekdays[0], // Expo只支持单个weekday
};
} else {
trigger = {
type: Notifications.SchedulableTriggerInputTypes.DAILY,
hour: options.hour,
minute: options.minute,
};
}
break;
case Notifications.SchedulableTriggerInputTypes.MONTHLY:
trigger = {
type: Notifications.SchedulableTriggerInputTypes.MONTHLY,
hour: options.hour,
minute: options.minute,
day: options.dayOfMonth || 1,
};
break;
default:
throw new Error('不支持的重复类型');
}
const notificationId = await Notifications.scheduleNotificationAsync({
content: {
title: notification.title,
body: notification.body,
data: notification.data || {},
sound: notification.sound ? 'default' : undefined,
priority: notification.priority || 'default',
vibrate: notification.vibrate,
},
trigger,
});
console.log(`${options.type}重复通知已安排ID:`, notificationId);
return notificationId;
} catch (error) {
console.error('安排日历重复通知失败:', error);
throw error;
}
}
/**
* 取消特定通知
*/
async cancelNotification(notificationId: string): Promise<void> {
try {
await Notifications.cancelScheduledNotificationAsync(notificationId);
console.log('通知已取消:', notificationId);
} catch (error) {
console.error('取消通知失败:', error);
throw error;
}
}
/**
* 取消所有通知
*/
async cancelAllNotifications(): Promise<void> {
try {
await Notifications.cancelAllScheduledNotificationsAsync();
console.log('所有通知已取消');
} catch (error) {
console.error('取消所有通知失败:', error);
throw error;
}
}
/**
* 获取所有已安排的通知
*/
async getAllScheduledNotifications(): Promise<Notifications.NotificationRequest[]> {
try {
const notifications = await Notifications.getAllScheduledNotificationsAsync();
return notifications;
} catch (error) {
console.error('获取已安排通知失败:', error);
throw error;
}
}
/**
* 获取通知权限状态
*/
async getPermissionStatus(): Promise<Notifications.PermissionStatus> {
try {
const { status } = await Notifications.getPermissionsAsync();
return status;
} catch (error) {
console.error('获取通知权限状态失败:', error);
throw error;
}
}
/**
* 请求通知权限
*/
async requestPermission(): Promise<Notifications.PermissionStatus> {
try {
const { status } = await Notifications.requestPermissionsAsync();
return status;
} catch (error) {
console.error('请求通知权限失败:', error);
throw error;
}
}
}
// 导出单例实例
export const notificationService = NotificationService.getInstance();
// 预定义的推送通知类型
export const NotificationTypes = {
WORKOUT_REMINDER: 'workout_reminder',
GOAL_ACHIEVEMENT: 'goal_achievement',
MOOD_CHECKIN: 'mood_checkin',
NUTRITION_REMINDER: 'nutrition_reminder',
PROGRESS_UPDATE: 'progress_update',
LUNCH_REMINDER: 'lunch_reminder',
DINNER_REMINDER: 'dinner_reminder',
MOOD_REMINDER: 'mood_reminder',
} as const;
// 便捷方法
export const sendWorkoutReminder = (title: string, body: string, date?: Date) => {
const notification: NotificationData = {
title,
body,
data: { type: NotificationTypes.WORKOUT_REMINDER },
sound: true,
priority: 'high',
};
if (date) {
return notificationService.scheduleNotificationAtDate(notification, date);
} else {
return notificationService.sendImmediateNotification(notification);
}
};
export const sendGoalAchievement = (title: string, body: string) => {
const notification: NotificationData = {
title,
body,
data: { type: NotificationTypes.GOAL_ACHIEVEMENT },
sound: true,
priority: 'high',
};
return notificationService.sendImmediateNotification(notification);
};
export const sendMoodCheckinReminder = (title: string, body: string, date?: Date) => {
const notification: NotificationData = {
title,
body,
data: { type: NotificationTypes.MOOD_CHECKIN },
sound: true,
priority: 'normal',
};
if (date) {
return notificationService.scheduleNotificationAtDate(notification, date);
} else {
return notificationService.sendImmediateNotification(notification);
}
};