import dayjs from 'dayjs'; import * as Notifications from 'expo-notifications'; import { FastingPlan } from '@/constants/Fasting'; import { FastingSchedule } from '@/store/fastingSlice'; import { clearFastingNotificationIds, FastingNotificationIds, getFastingNotificationsRegistered, loadStoredFastingNotificationIds, saveFastingNotificationIds, setFastingNotificationsRegistered, } from '@/utils/fasting'; import { getNotificationEnabled } from '@/utils/userPreferences'; import { notificationService, NotificationTypes } from './notifications'; const REMINDER_OFFSET_MINUTES = 30; // 改为30分钟提醒 const cancelNotificationIds = async (ids?: FastingNotificationIds) => { if (!ids) return; const { preStartId, startId, preEndId, endId } = ids; // 取消开始前30分钟通知 try { if (preStartId) { await notificationService.cancelNotification(preStartId); } } catch (error) { console.warn('取消断食开始前30分钟提醒失败', error); } // 取消开始时通知 try { if (startId) { await notificationService.cancelNotification(startId); } } catch (error) { console.warn('取消断食开始时提醒失败', error); } // 取消结束前30分钟通知 try { if (preEndId) { await notificationService.cancelNotification(preEndId); } } catch (error) { console.warn('取消断食结束前30分钟提醒失败', error); } // 取消结束时通知 try { if (endId) { await notificationService.cancelNotification(endId); } } catch (error) { console.warn('取消断食结束时提醒失败', error); } }; export const ensureFastingNotificationsReady = async (): Promise => { try { const notificationsEnabled = await getNotificationEnabled(); if (!notificationsEnabled) { return false; } const registered = await getFastingNotificationsRegistered(); if (registered) { return true; } const status = await notificationService.getPermissionStatus(); if (status !== 'granted') { const requestStatus = await notificationService.requestPermission(); if (requestStatus !== 'granted') { return false; } } await setFastingNotificationsRegistered(true); return true; } catch (error) { console.warn('初始化断食通知失败', error); return false; } }; type ResyncOptions = { schedule: FastingSchedule | null; plan?: FastingPlan; previousIds?: FastingNotificationIds; enabled: boolean; }; export const resyncFastingNotifications = async ({ schedule, plan, previousIds, enabled, }: ResyncOptions): Promise => { const storedIds = previousIds ?? (await loadStoredFastingNotificationIds()); await cancelNotificationIds(storedIds); if (!enabled) { await setFastingNotificationsRegistered(false); await clearFastingNotificationIds(); return {}; } const preferenceEnabled = await getNotificationEnabled(); if (!preferenceEnabled) { await setFastingNotificationsRegistered(false); await clearFastingNotificationIds(); return {}; } if (!schedule || !plan) { await clearFastingNotificationIds(); return {}; } const now = dayjs(); const start = dayjs(schedule.startISO); const end = dayjs(schedule.endISO); if (end.isBefore(now)) { await clearFastingNotificationIds(); return {}; } const notificationIds: FastingNotificationIds = {}; // 1. 安排开始前30分钟通知 if (start.isAfter(now)) { const preStart = start.subtract(REMINDER_OFFSET_MINUTES, 'minute'); // 只有当开始前30分钟还在未来时才安排这个通知 if (preStart.isAfter(now)) { try { const preStartId = await notificationService.scheduleNotificationAtDate( { title: `${plan.title} 即将开始`, body: `还有 ${REMINDER_OFFSET_MINUTES} 分钟就要进入断食窗口,喝一杯温水,准备迎接更轻盈的自己!`, data: { type: NotificationTypes.FASTING_START, planId: plan.id, subtype: 'pre_start', }, sound: true, priority: 'high', }, preStart.toDate() ); notificationIds.preStartId = preStartId; } catch (error) { console.error('安排断食开始前30分钟通知失败', error); } } // 2. 安排开始时通知 try { const startId = await notificationService.scheduleNotificationAtDate( { title: `${plan.title} 开始了`, body: `现在开始 ${plan.title},放下零食,给身体一次真正的休息时间。`, data: { type: NotificationTypes.FASTING_START, planId: plan.id, subtype: 'start', }, sound: true, priority: 'high', }, start.toDate() ); notificationIds.startId = startId; } catch (error) { console.error('安排断食开始时通知失败', error); } } // 3. 安排结束前30分钟通知 if (end.isAfter(now)) { const preEnd = end.subtract(REMINDER_OFFSET_MINUTES, 'minute'); // 只有当结束前30分钟还在未来时才安排这个通知 if (preEnd.isAfter(now)) { try { const preEndId = await notificationService.scheduleNotificationAtDate( { title: '即将结束断食', body: `还有 ${REMINDER_OFFSET_MINUTES} 分钟就要结束 ${plan.title},准备高蛋白 + 低GI 的餐食奖励自己!`, data: { type: NotificationTypes.FASTING_END, planId: plan.id, subtype: 'pre_end', }, sound: true, priority: 'high', }, preEnd.toDate() ); notificationIds.preEndId = preEndId; } catch (error) { console.error('安排断食结束前30分钟通知失败', error); } } // 4. 安排结束时通知 try { const endId = await notificationService.scheduleNotificationAtDate( { title: '补能时刻到啦', body: `${plan.title} 已完成!用一顿高蛋白 + 低GI 的餐食奖励自己,让代谢持续高效运转。`, data: { type: NotificationTypes.FASTING_END, planId: plan.id, subtype: 'end', }, sound: true, priority: 'high', }, end.toDate() ); notificationIds.endId = endId; } catch (error) { console.error('安排断食结束时通知失败', error); } } await saveFastingNotificationIds(notificationIds); return notificationIds; }; /** * 选择性同步通知,只更新需要更新的通知 * @param schedule 断食计划 * @param plan 断食方案 * @param storedIds 存储的通知ID * @param validIds 有效的通知ID * @param enabled 是否启用通知 * @returns 更新后的通知ID */ const selectiveSyncNotifications = async ({ schedule, plan, storedIds, validIds, enabled, }: { schedule: FastingSchedule | null; plan?: FastingPlan; storedIds: FastingNotificationIds; validIds: FastingNotificationIds; enabled: boolean; }): Promise => { if (!enabled || !schedule || !plan) { await clearFastingNotificationIds(); return {}; } const now = dayjs(); const start = dayjs(schedule.startISO); const end = dayjs(schedule.endISO); if (end.isBefore(now)) { await clearFastingNotificationIds(); return {}; } const updatedIds: FastingNotificationIds = { ...validIds }; // 1. 检查开始前30分钟通知 const preStart = start.subtract(REMINDER_OFFSET_MINUTES, 'minute'); if (preStart.isAfter(now) && !validIds.preStartId) { try { const preStartId = await notificationService.scheduleNotificationAtDate( { title: `${plan.title} 即将开始`, body: `还有 ${REMINDER_OFFSET_MINUTES} 分钟就要进入断食窗口,喝一杯温水,准备迎接更轻盈的自己!`, data: { type: NotificationTypes.FASTING_START, planId: plan.id, subtype: 'pre_start', }, sound: true, priority: 'high', }, preStart.toDate() ); updatedIds.preStartId = preStartId; } catch (error) { console.error('安排断食开始前30分钟通知失败', error); } } // 2. 检查开始时通知 if (start.isAfter(now) && !validIds.startId) { try { const startId = await notificationService.scheduleNotificationAtDate( { title: `${plan.title} 开始了`, body: `现在开始 ${plan.title},放下零食,给身体一次真正的休息时间。`, data: { type: NotificationTypes.FASTING_START, planId: plan.id, subtype: 'start', }, sound: true, priority: 'high', }, start.toDate() ); updatedIds.startId = startId; } catch (error) { console.error('安排断食开始时通知失败', error); } } // 3. 检查结束前30分钟通知 const preEnd = end.subtract(REMINDER_OFFSET_MINUTES, 'minute'); if (preEnd.isAfter(now) && !validIds.preEndId) { try { const preEndId = await notificationService.scheduleNotificationAtDate( { title: '即将结束断食', body: `还有 ${REMINDER_OFFSET_MINUTES} 分钟就要结束 ${plan.title},准备高蛋白 + 低GI 的餐食奖励自己!`, data: { type: NotificationTypes.FASTING_END, planId: plan.id, subtype: 'pre_end', }, sound: true, priority: 'high', }, preEnd.toDate() ); updatedIds.preEndId = preEndId; } catch (error) { console.error('安排断食结束前30分钟通知失败', error); } } // 4. 检查结束时通知 if (end.isAfter(now) && !validIds.endId) { try { const endId = await notificationService.scheduleNotificationAtDate( { title: '补能时刻到啦', body: `${plan.title} 已完成!用一顿高蛋白 + 低GI 的餐食奖励自己,让代谢持续高效运转。`, data: { type: NotificationTypes.FASTING_END, planId: plan.id, subtype: 'end', }, sound: true, priority: 'high', }, end.toDate() ); updatedIds.endId = endId; } catch (error) { console.error('安排断食结束时通知失败', error); } } await saveFastingNotificationIds(updatedIds); return updatedIds; }; /** * 验证现有通知是否正确设置 * @param schedule 当前断食计划 * @param plan 当前断食方案 * @param storedIds 存储的通知ID * @returns 验证结果和需要更新的通知ID */ export const verifyFastingNotifications = async ({ schedule, plan, storedIds, }: { schedule: FastingSchedule | null; plan?: FastingPlan; storedIds: FastingNotificationIds; }): Promise<{ isValid: boolean; updatedIds: FastingNotificationIds }> => { if (!schedule || !plan) { // 如果没有计划或方案,应该清空所有通知 if (Object.values(storedIds).some(id => id)) { await cancelNotificationIds(storedIds); await clearFastingNotificationIds(); return { isValid: false, updatedIds: {} }; } return { isValid: true, updatedIds: storedIds }; } const now = dayjs(); const start = dayjs(schedule.startISO); const end = dayjs(schedule.endISO); // 如果断食期已结束,应该清空所有通知 if (end.isBefore(now)) { if (Object.values(storedIds).some(id => id)) { await cancelNotificationIds(storedIds); await clearFastingNotificationIds(); return { isValid: false, updatedIds: {} }; } return { isValid: true, updatedIds: {} }; } // 获取当前已安排的通知 let scheduledNotifications: Notifications.NotificationRequest[] = []; try { scheduledNotifications = await notificationService.getAllScheduledNotifications(); } catch (error) { console.warn('获取已安排通知失败', error); // 如果获取通知失败,不要立即重新同步,而是返回当前存储的ID // 这样可以避免可能的重复通知问题 return { isValid: false, updatedIds: storedIds }; } // 检查每个通知是否存在且正确 const expectedNotifications = [ { id: storedIds.preStartId, type: 'pre_start', time: start.subtract(REMINDER_OFFSET_MINUTES, 'minute') }, { id: storedIds.startId, type: 'start', time: start }, { id: storedIds.preEndId, type: 'pre_end', time: end.subtract(REMINDER_OFFSET_MINUTES, 'minute') }, { id: storedIds.endId, type: 'end', time: end }, ]; let needsResync = false; const validIds: FastingNotificationIds = {}; for (const expected of expectedNotifications) { // 跳过已过期的通知 if (expected.time.isBefore(now)) { if (expected.id) { needsResync = true; } continue; } if (!expected.id) { needsResync = true; continue; } // 检查通知是否还存在 const exists = scheduledNotifications.some(n => n.identifier === expected.id); if (!exists) { needsResync = true; continue; } // 通知存在且有效 switch (expected.type) { case 'pre_start': validIds.preStartId = expected.id; break; case 'start': validIds.startId = expected.id; break; case 'pre_end': validIds.preEndId = expected.id; break; case 'end': validIds.endId = expected.id; break; } } if (needsResync) { // 只同步需要更新的通知,而不是全部重新同步 const updatedIds = await selectiveSyncNotifications({ schedule, plan, storedIds, validIds, enabled: true }); return { isValid: false, updatedIds }; } return { isValid: true, updatedIds: validIds }; }; export type { FastingNotificationIds };