Files
digital-pilates/services/notifications.ts

657 lines
20 KiB
TypeScript
Raw Permalink 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 { logger } from '@/utils/logger';
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 async function clearBadgeCount(): Promise<void> {
try {
await Notifications.setBadgeCountAsync(0);
console.log('✅ 应用角标已清除');
} catch (error) {
console.error('❌ 清除应用角标失败:', error);
}
}
/**
* 获取当前角标数量
*/
export async function getBadgeCount(): Promise<number> {
try {
return await Notifications.getBadgeCountAsync();
} catch (error) {
console.error('❌ 获取角标数量失败:', error);
return 0;
}
}
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;
}
// 清除应用角标(应用启动时)
await clearBadgeCount();
// 设置通知监听器
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);
// 用户点击通知后清除角标
clearBadgeCount();
// 根据通知类型处理不同的逻辑
if (data?.type === 'workout_reminder') {
// 处理运动提醒
console.log('用户点击了运动提醒通知');
} else if (data?.type === 'goal_achievement') {
// 处理目标达成通知
console.log('用户点击了目标达成通知');
} else if (data?.type === 'mood_checkin' || data?.type === 'mood_checkin_reminder') {
// 处理心情打卡提醒
console.log('用户点击了心情打卡提醒');
router.push({
pathname: '/mood/edit',
params: { date: new Date().toISOString().split('T')[0] }
} as any);
} 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' || data?.type === 'custom_water_reminder') {
// 处理喝水提醒通知
console.log('用户点击了喝水提醒通知', data);
// 跳转到统计页面查看喝水进度
if (data?.url) {
router.push(data.url as any);
}
} else if (data?.type === 'daily_summary' || data?.type === 'daily_summary_reminder') {
// 处理每日总结通知
console.log('用户点击了每日总结通知', data);
if (data?.url) {
router.push(data.url as any);
} else {
router.push('/(tabs)/statistics' 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) {
// 处理锻炼完成通知
logger.info('用户点击了锻炼完成通知', data);
const workoutId =
typeof data?.workoutId === 'string'
? data.workoutId
: data?.workoutId != null
? String(data.workoutId)
: null;
// 跳转到锻炼历史页面并在有锻炼ID时自动打开详情
if (workoutId) {
router.push({
pathname: '/workout/history',
params: { workoutId },
} as any);
} else {
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.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);
}
}
/**
* 检查用户是否允许推送通知(不包括系统权限检查,仅检查用户偏好)
*/
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',
HRV_STRESS_ALERT: 'hrv_stress_alert',
SLEEP_ANALYSIS: 'sleep_analysis',
SLEEP_REMINDER: 'sleep_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);
}
};
// 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);
}
};