import dayjs from 'dayjs'; import { FastingPlan } from '@/constants/Fasting'; import { FastingCycle, FastingCycleSession } from '@/store/fastingSlice'; import { getNotificationEnabled } from '@/utils/userPreferences'; import { notificationService, NotificationTypes } from './notifications'; const REMINDER_OFFSET_MINUTES = 30; // 提前30分钟提醒 // 通知同步锁,防止并发操作 let notificationSyncLock = false; export class FastingCycleNotificationManager { /** * 为周期性断食计划安排通知 * @param cycle 周期性断食计划 * @param session 当前断食会话 */ static async scheduleCycleNotifications( cycle: FastingCycle, session: FastingCycleSession | null ): Promise { // 获取锁,防止并发操作 if (notificationSyncLock) { console.log('通知同步正在进行中,跳过本次操作'); return; } try { notificationSyncLock = true; // 检查通知权限 const notificationsEnabled = await getNotificationEnabled(); console.log('🔔 周期性断食通知安排 - 权限检查:', { notificationsEnabled, cycleEnabled: cycle.enabled, planId: cycle.planId }); if (!notificationsEnabled) { console.log('⚠️ 用户已关闭通知权限,跳过通知安排'); return; } if (!cycle.enabled) { console.log('⚠️ 周期性断食已暂停,跳过通知安排'); return; } // 如果没有当前会话,不安排通知 if (!session) { console.log('⚠️ 没有当前断食会话,跳过通知安排'); return; } const plan = await this.getPlanById(cycle.planId); if (!plan) { console.warn('❌ 未找到断食计划:', cycle.planId); return; } const now = dayjs(); const start = dayjs(session.startISO); const end = dayjs(session.endISO); console.log('📅 断食会话信息:', { planTitle: plan.title, cycleDate: session.cycleDate, start: start.format('YYYY-MM-DD HH:mm'), end: end.format('YYYY-MM-DD HH:mm'), now: now.format('YYYY-MM-DD HH:mm'), }); // 如果断食期已结束,不安排通知 if (end.isBefore(now)) { console.log('⚠️ 断食期已结束,跳过通知安排'); return; } // 取消之前的周期性通知 console.log('🧹 取消之前的周期性通知...'); await this.cancelAllCycleNotifications(); // 1. 安排开始前30分钟通知 if (start.isAfter(now)) { const preStart = start.subtract(REMINDER_OFFSET_MINUTES, 'minute'); if (preStart.isAfter(now)) { try { const notificationId = await notificationService.scheduleNotificationAtDate( { title: `${plan.title} 即将开始`, body: `还有 ${REMINDER_OFFSET_MINUTES} 分钟就要进入断食窗口,喝一杯温水,准备迎接更轻盈的自己!`, data: { type: NotificationTypes.FASTING_START, planId: plan.id, subtype: 'cycle_pre_start', cycleDate: session.cycleDate, }, sound: true, priority: 'high', }, preStart.toDate() ); console.log(`✅ 已安排周期性断食开始前30分钟通知 [${notificationId}]: ${preStart.format('YYYY-MM-DD HH:mm')}`); } catch (error) { console.error('❌ 安排周期性断食开始前30分钟通知失败', error); } } else { console.log(`⏭️ 跳过开始前30分钟通知(时间已过): ${preStart.format('YYYY-MM-DD HH:mm')}`); } // 2. 安排开始时通知 try { const notificationId = await notificationService.scheduleNotificationAtDate( { title: `${plan.title} 开始了`, body: `现在开始 ${plan.title},放下零食,给身体一次真正的休息时间。`, data: { type: NotificationTypes.FASTING_START, planId: plan.id, subtype: 'cycle_start', cycleDate: session.cycleDate, }, sound: true, priority: 'high', }, start.toDate() ); console.log(`✅ 已安排周期性断食开始时通知 [${notificationId}]: ${start.format('YYYY-MM-DD HH:mm')}`); } catch (error) { console.error('❌ 安排周期性断食开始时通知失败', error); } } // 3. 安排结束前30分钟通知 if (end.isAfter(now)) { const preEnd = end.subtract(REMINDER_OFFSET_MINUTES, 'minute'); if (preEnd.isAfter(now)) { try { const notificationId = await notificationService.scheduleNotificationAtDate( { title: '即将结束断食', body: `还有 ${REMINDER_OFFSET_MINUTES} 分钟就要结束 ${plan.title},准备高蛋白 + 低GI 的餐食奖励自己!`, data: { type: NotificationTypes.FASTING_END, planId: plan.id, subtype: 'cycle_pre_end', cycleDate: session.cycleDate, }, sound: true, priority: 'high', }, preEnd.toDate() ); console.log(`✅ 已安排周期性断食结束前30分钟通知 [${notificationId}]: ${preEnd.format('YYYY-MM-DD HH:mm')}`); } catch (error) { console.error('❌ 安排周期性断食结束前30分钟通知失败', error); } } else { console.log(`⏭️ 跳过结束前30分钟通知(时间已过): ${preEnd.format('YYYY-MM-DD HH:mm')}`); } // 4. 安排结束时通知 try { const notificationId = await notificationService.scheduleNotificationAtDate( { title: '补能时刻到啦', body: `${plan.title} 已完成!用一顿高蛋白 + 低GI 的餐食奖励自己,让代谢持续高效运转。`, data: { type: NotificationTypes.FASTING_END, planId: plan.id, subtype: 'cycle_end', cycleDate: session.cycleDate, }, sound: true, priority: 'high', }, end.toDate() ); console.log(`✅ 已安排周期性断食结束时通知 [${notificationId}]: ${end.format('YYYY-MM-DD HH:mm')}`); } catch (error) { console.error('❌ 安排周期性断食结束时通知失败', error); } } console.log('🎉 周期性断食通知安排完成'); } catch (error) { console.error('❌ 安排周期性断食通知失败', error); throw error; // 抛出错误以便调用方处理 } finally { notificationSyncLock = false; } } /** * 取消所有周期性断食通知 * 只取消带有 cycle_ 前缀的通知,避免影响单次断食通知 */ static async cancelAllCycleNotifications(): Promise { try { const scheduledNotifications = await notificationService.getAllScheduledNotifications(); // 只过滤出周期性断食相关的通知(带有 cycle_ 前缀) const cycleNotifications = scheduledNotifications.filter(notification => { const data = notification.content.data as any; return data && data.subtype?.startsWith('cycle_'); }); // 取消所有周期性通知 for (const notification of cycleNotifications) { try { await notificationService.cancelNotification(notification.identifier); } catch (error) { console.warn('取消周期性断食通知失败', error); } } console.log(`已取消 ${cycleNotifications.length} 个周期性断食通知`); } catch (error) { console.error('取消周期性断食通知失败', error); } } /** * 更新每日通知(在断食结束后安排下一天的通知) * @param cycle 周期性断食计划 * @param nextSession 下一个断食会话 */ static async updateDailyNotifications( cycle: FastingCycle, nextSession: FastingCycleSession | null ): Promise { try { // 取消当前的通知 await this.cancelAllCycleNotifications(); // 安排下一天的通知 if (cycle.enabled && nextSession) { await this.scheduleCycleNotifications(cycle, nextSession); } } catch (error) { console.error('更新每日断食通知失败', error); } } /** * 验证周期性通知是否正确设置 * @param cycle 周期性断食计划 * @param session 当前断食会话 */ static async verifyCycleNotifications( cycle: FastingCycle, session: FastingCycleSession | null ): Promise { // 如果正在同步,直接返回 true 避免重复操作 if (notificationSyncLock) { console.log('通知同步正在进行中,跳过验证'); return true; } try { if (!cycle.enabled || !session) { // 如果没有启用周期性计划或没有会话,应该没有通知 const scheduledNotifications = await notificationService.getAllScheduledNotifications(); const cycleNotifications = scheduledNotifications.filter(notification => { const data = notification.content.data as any; return data && data.subtype?.startsWith('cycle_'); }); if (cycleNotifications.length > 0) { // 有不应该存在的通知,需要清理 await this.cancelAllCycleNotifications(); return false; } return true; } const now = dayjs(); const start = dayjs(session.startISO); const end = dayjs(session.endISO); // 如果断食期已结束,应该没有通知 if (end.isBefore(now)) { await this.cancelAllCycleNotifications(); return false; } const scheduledNotifications = await notificationService.getAllScheduledNotifications(); const cycleNotifications = scheduledNotifications.filter(notification => { const data = notification.content.data as any; return data && data.subtype?.startsWith('cycle_'); }); // 检查是否有足够的通知 const expectedNotifications = []; // 开始前30分钟通知 if (start.isAfter(now)) { const preStart = start.subtract(REMINDER_OFFSET_MINUTES, 'minute'); if (preStart.isAfter(now)) { expectedNotifications.push('cycle_pre_start'); } expectedNotifications.push('cycle_start'); } // 结束前30分钟通知 if (end.isAfter(now)) { const preEnd = end.subtract(REMINDER_OFFSET_MINUTES, 'minute'); if (preEnd.isAfter(now)) { expectedNotifications.push('cycle_pre_end'); } expectedNotifications.push('cycle_end'); } // 如果通知数量不匹配,标记为需要重新安排,但不在这里直接调用 if (cycleNotifications.length !== expectedNotifications.length) { console.log('周期性通知数量不匹配,需要重新安排'); return false; } return true; } catch (error) { console.error('验证周期性断食通知失败', error); return false; } } /** * 获取断食计划信息 */ private static async getPlanById(planId: string): Promise { try { // 这里需要导入 FASTING_PLANS,但为了避免循环依赖,我们使用动态导入 const { FASTING_PLANS } = await import('@/constants/Fasting'); return FASTING_PLANS.find(plan => plan.id === planId); } catch (error) { console.error('获取断食计划失败', error); return undefined; } } }