Files
digital-pilates/services/notifications.ts
richarjiang 21e57634e0 feat(hrv): 添加心率变异性监控和压力评估功能
- 新增 HRV 监听服务,实时监控心率变异性数据
- 实现 HRV 到压力指数的转换算法和压力等级评估
- 添加智能通知服务,在压力偏高时推送健康建议
- 优化日志系统,修复日志丢失问题并增强刷新机制
- 改进个人页面下拉刷新,支持并行数据加载
- 优化勋章数据缓存策略,减少不必要的网络请求
- 重构应用初始化流程,优化权限服务和健康监听服务的启动顺序
- 移除冗余日志输出,提升应用性能
2025-11-18 14:08:20 +08:00

614 lines
18 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 { ROUTES } from '@/constants/Routes';
import { getNotificationEnabled } from '@/utils/userPreferences';
import * as Notifications from 'expo-notifications';
import { router } from 'expo-router';
import { pushNotificationManager } from './pushNotificationManager';
// 配置通知处理方式
Notifications.setNotificationHandler({
handleNotification: async () => ({
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 isIniting = false
private constructor() { }
public static getInstance(): NotificationService {
if (!NotificationService.instance) {
NotificationService.instance = new NotificationService();
}
return NotificationService.instance;
}
/**
* 初始化推送通知服务
*/
async initialize(): Promise<void> {
if (this.isInitialized || this.isIniting) return;
try {
this.isIniting = true
// 初始化推送通知管理器(包含设备令牌管理)
const pushManagerInitialized = await pushNotificationManager.initialize({
onTokenReceived: (token) => {
console.log('设备令牌已接收:', token.substring(0, 20) + '...');
},
onTokenRefresh: (token) => {
console.log('设备令牌已刷新:', token.substring(0, 20) + '...');
},
onError: (error) => {
console.error('推送通知管理器错误:', error);
}
});
if (!pushManagerInitialized) {
console.warn('推送通知管理器初始化失败,但本地通知功能仍可用');
}
// 请求通知权限
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;
}
// 设置通知监听器
this.setupNotificationListeners();
// 检查已存在的通知
await this.getAllScheduledNotifications();
this.isInitialized = true;
console.log('推送通知服务初始化成功');
} catch (error) {
console.error('推送通知服务初始化失败:', error);
} finally {
this.isIniting = false
}
}
/**
* 设置通知监听器
*/
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 === NotificationTypes.CHALLENGE_ENCOURAGEMENT) {
console.log('用户点击了挑战提醒通知', data);
const targetUrl = (data?.url as string) || '/(tabs)/challenges';
router.push(targetUrl as any);
} else if (data?.type === 'mood_reminder') {
// 处理心情提醒通知
console.log('用户点击了心情提醒通知', data);
// 跳转到心情页面
if (data?.url) {
router.push(data.url as any);
}
} else if (data?.type === 'water_reminder' || data?.type === 'regular_water_reminder') {
// 处理喝水提醒通知
console.log('用户点击了喝水提醒通知', data);
// 跳转到统计页面查看喝水进度
if (data?.url) {
router.push(data.url as any);
}
} else if (data?.type === NotificationTypes.FASTING_START || data?.type === NotificationTypes.FASTING_END) {
router.push(ROUTES.TAB_FASTING as any);
} else if (data?.type === NotificationTypes.WORKOUT_COMPLETION) {
// 处理锻炼完成通知
console.log('用户点击了锻炼完成通知', data);
// 跳转到锻炼历史页面
router.push('/workout/history' as any);
} else if (data?.type === NotificationTypes.HRV_STRESS_ALERT) {
console.log('用户点击了 HRV 压力通知', data);
const targetUrl = (data?.url as string) || '/(tabs)/statistics';
router.push(targetUrl as any);
} else if (data?.type === NotificationTypes.MEDICATION_REMINDER) {
// 处理药品提醒通知
console.log('用户点击了药品提醒通知', data);
// 跳转到药品页面
router.push('/(tabs)/medications' as any);
}
}
/**
* 检查用户是否允许推送通知(不包括系统权限检查,仅检查用户偏好)
*/
private async isNotificationAllowed(): Promise<boolean> {
try {
// 检查用户偏好设置中的推送开关
const userPreferenceEnabled = await getNotificationEnabled();
if (!userPreferenceEnabled) {
console.log('用户已在偏好设置中关闭推送通知');
return false;
}
return true;
} catch (error) {
console.error('检查推送权限失败:', error);
// 如果检查失败,默认允许(避免阻塞重要的健康提醒)
return true;
}
}
/**
* 完整的权限检查(包括系统权限和用户偏好)
*/
private async hasFullNotificationPermission(): Promise<boolean> {
try {
// 检查系统权限
const permissionStatus = await this.getPermissionStatus();
if (permissionStatus !== 'granted') {
console.log('系统推送权限未授予:', permissionStatus);
return false;
}
// 检查用户偏好
const userPreferenceEnabled = await this.isNotificationAllowed();
if (!userPreferenceEnabled) {
return false;
}
return true;
} catch (error) {
console.error('完整权限检查失败:', error);
return false;
}
}
/**
* 发送本地推送通知
*/
async scheduleLocalNotification(
notification: NotificationData,
trigger?: Notifications.NotificationTriggerInput
): Promise<string> {
try {
// 检查完整权限(系统权限 + 用户偏好)
const hasPermission = await this.hasFullNotificationPermission();
if (!hasPermission) {
console.log('推送通知被系统权限或用户偏好设置阻止,跳过发送');
return 'blocked_by_permission_or_preference';
}
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> {
console.log('📱 准备发送立即通知:', notification.title);
return this.scheduleLocalNotification(notification);
}
/**
* 安排定时通知
*/
async scheduleNotificationAtDate(
notification: NotificationData,
date: Date
): Promise<string> {
try {
// 检查完整权限(系统权限 + 用户偏好)
const hasPermission = await this.hasFullNotificationPermission();
if (!hasPermission) {
console.log('⚠️ 定时通知被系统权限或用户偏好设置阻止,跳过安排');
return 'blocked_by_permission_or_preference';
}
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, '时间:', date.toLocaleString('zh-CN'));
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',
MOOD_CHECKIN: 'mood_checkin',
NUTRITION_REMINDER: 'nutrition_reminder',
PROGRESS_UPDATE: 'progress_update',
LUNCH_REMINDER: 'lunch_reminder',
DINNER_REMINDER: 'dinner_reminder',
MOOD_REMINDER: 'mood_reminder',
WATER_REMINDER: 'water_reminder',
REGULAR_WATER_REMINDER: 'regular_water_reminder',
CHALLENGE_ENCOURAGEMENT: 'challenge_encouragement',
WORKOUT_COMPLETION: 'workout_completion',
FASTING_START: 'fasting_start',
FASTING_END: 'fasting_end',
MEDICATION_REMINDER: 'medication_reminder',
HRV_STRESS_ALERT: 'hrv_stress_alert',
} 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);
}
};
// sendGoalAchievement 函数已删除,因为目标功能已移除
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);
}
};
export const sendMedicationReminder = (title: string, body: string, medicationId?: string, date?: Date) => {
const notification: NotificationData = {
title,
body,
data: {
type: NotificationTypes.MEDICATION_REMINDER,
medicationId: medicationId || ''
},
sound: true,
priority: 'high',
};
if (date) {
return notificationService.scheduleNotificationAtDate(notification, date);
} else {
return notificationService.sendImmediateNotification(notification);
}
};