import { NativeEventEmitter, NativeModules, Platform, type EmitterSubscription } from 'react-native'; import { listChallenges } from '@/services/challengesApi'; import { resyncFastingNotifications } from '@/services/fastingNotifications'; import { store } from '@/store'; import { selectActiveFastingPlan, selectActiveFastingSchedule } from '@/store/fastingSlice'; import { getWaterIntakeFromHealthKit } from '@/utils/health'; import AsyncStorage from '@/utils/kvStore'; import { logger } from '@/utils/logger'; import { ChallengeNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers'; import { getWaterGoalFromStorage } from '@/utils/userPreferences'; import dayjs from 'dayjs'; export const BACKGROUND_TASK_IDENTIFIER = 'com.anonymous.digitalpilates.task'; const DEFAULT_RESCHEDULE_INTERVAL_SECONDS = 60 * 15; // 15 minutes const BACKGROUND_EVENT = 'BackgroundTaskBridge.execute'; const EXPIRATION_EVENT = 'BackgroundTaskBridge.expire'; const NativeBackgroundModule = NativeModules.BackgroundTaskBridge; const isIosBackgroundModuleAvailable = Platform.OS === 'ios' && NativeBackgroundModule; // 检查通知权限 async function checkNotificationPermissions(): Promise { try { const Notifications = await import('expo-notifications'); const { status } = await Notifications.getPermissionsAsync(); return status === 'granted'; } catch (error) { console.error('检查通知权限失败:', error); return false; } } // 执行喝水提醒后台任务 async function executeWaterReminderTask(): Promise { try { console.log('执行喝水提醒后台任务...'); let state; try { state = store.getState(); } catch (error) { console.log('无法获取 Redux state,使用本地存储:', error); const dailyGoal = await getWaterGoalFromStorage(); if (!dailyGoal || dailyGoal <= 0) { console.log('没有设置喝水目标,跳过喝水提醒'); return; } await sendSimpleWaterReminder(); return; } const waterStats = state.water?.todayStats; const userProfile = state.user?.profile; let dailyGoal = waterStats?.dailyGoal ?? 0; if (!dailyGoal || dailyGoal <= 0) { dailyGoal = await getWaterGoalFromStorage(); } if (!dailyGoal || dailyGoal <= 0) { console.log('没有设置喝水目标,跳过喝水提醒'); return; } const currentHour = new Date().getHours(); const userName = userProfile?.name || '朋友'; const todayRange = { startDate: dayjs().startOf('day').toDate().toISOString(), endDate: dayjs().endOf('day').toDate().toISOString() }; let totalAmount = waterStats?.totalAmount ?? 0; let completionRate = waterStats?.completionRate ?? (dailyGoal > 0 ? (totalAmount / dailyGoal) * 100 : 0); try { const healthKitRecords = await getWaterIntakeFromHealthKit(todayRange); if (Array.isArray(healthKitRecords) && healthKitRecords.length > 0) { totalAmount = healthKitRecords.reduce((sum: number, record: unknown) => { if (record && typeof record === 'object' && 'value' in record) { const { value } = record as { value?: number | string }; const numericValue = Number(value ?? 0); return Number.isFinite(numericValue) ? sum + numericValue : sum; } return sum; }, 0); completionRate = Math.min((totalAmount / dailyGoal) * 100, 100); } else { console.log('HealthKit 未返回今日饮水记录,使用应用内缓存数据'); } } catch (healthKitError) { console.error('从HealthKit获取饮水记录失败,使用应用内缓存数据:', healthKitError); } const todayWaterStats = { totalAmount, dailyGoal, completionRate: Number.isFinite(completionRate) ? completionRate : 0 }; const notificationSent = await WaterNotificationHelpers.checkWaterGoalAndNotify( userName, todayWaterStats, currentHour ); if (notificationSent) { console.log('后台喝水提醒通知已发送'); await AsyncStorage.setItem('@last_background_water_check', Date.now().toString()); } else { console.log('无需发送后台喝水提醒通知'); } } catch (error) { console.error('执行喝水提醒后台任务失败:', error); } } async function executeChallengeReminderTask(): Promise { try { console.log('执行挑战鼓励提醒后台任务...'); let userName = '朋友'; try { const state = store.getState(); const normalizedUserName = state.user?.profile?.name?.trim(); userName = normalizedUserName && normalizedUserName.length > 0 ? normalizedUserName : '朋友'; } catch (error) { console.log('无法获取用户名,使用默认值:', error); } const challenges = await listChallenges(); const joinedChallenges = challenges.filter((challenge) => challenge.isJoined && challenge.progress); if (!joinedChallenges.length) { console.log('没有加入的挑战或挑战没有进度,跳过挑战提醒'); return; } const todayKey = new Date().toISOString().slice(0, 10); const eligibleChallenges = []; for (const challenge of joinedChallenges) { const progress = challenge.progress; if (!progress || progress.checkedInToday) { continue; } const storageKey = `@challenge_encouragement_sent:${challenge.id}`; const lastSent = await AsyncStorage.getItem(storageKey); if (lastSent === todayKey) { continue; } eligibleChallenges.push(challenge); } if (eligibleChallenges.length > 0) { const randomIndex = Math.floor(Math.random() * eligibleChallenges.length); const selectedChallenge = eligibleChallenges[randomIndex]; try { await ChallengeNotificationHelpers.sendEncouragementNotification({ userName, challengeTitle: selectedChallenge.title, challengeId: selectedChallenge.id, }); const storageKey = `@challenge_encouragement_sent:${selectedChallenge.id}`; await AsyncStorage.setItem(storageKey, todayKey); console.log(`已随机选择并发送挑战鼓励通知: ${selectedChallenge.title}`); } catch (notificationError) { console.error('发送挑战鼓励通知失败:', notificationError); } } else { console.log('没有符合条件的挑战需要发送鼓励通知'); } console.log('挑战鼓励提醒后台任务完成'); } catch (error) { console.error('执行挑战鼓励提醒后台任务失败:', error); } } // 执行断食通知后台任务 async function executeFastingNotificationTask(): Promise { try { console.log('执行断食通知后台任务...'); let state; try { state = store.getState(); } catch (error) { console.log('无法获取 Redux state,跳过断食通知任务:', error); return; } const activeSchedule = selectActiveFastingSchedule(state); const activePlan = selectActiveFastingPlan(state); if (!activeSchedule || !activePlan) { console.log('没有激活的断食计划,跳过断食通知任务'); return; } const end = dayjs(activeSchedule.endISO); if (end.isBefore(dayjs())) { console.log('断食计划已结束,跳过断食通知任务'); return; } console.log('正在同步断食通知...', { planId: activePlan.id, start: activeSchedule.startISO, end: activeSchedule.endISO, }); await resyncFastingNotifications({ schedule: activeSchedule, plan: activePlan, enabled: true, }); console.log('断食通知后台同步完成'); } catch (error) { console.error('执行断食通知后台任务失败:', error); } } // 发送测试通知以验证后台任务执行 async function sendTestNotification(): Promise { try { console.log('发送后台任务测试通知...'); const Notifications = await import('expo-notifications'); await Notifications.scheduleNotificationAsync({ content: { title: '后台任务测试', body: `后台任务正在执行中... 时间: ${new Date().toLocaleTimeString()}`, data: { type: 'background_task_test', timestamp: Date.now() } }, trigger: null, }); console.log('后台任务测试通知发送成功'); await AsyncStorage.setItem('@last_background_test_notification', Date.now().toString()); } catch (error) { console.error('发送测试通知失败:', error); } } async function sendSimpleWaterReminder(): Promise { try { const userName = '朋友'; const Notifications = await import('expo-notifications'); const notificationId = await Notifications.scheduleNotificationAsync({ content: { title: '💧 该喝水啦!', body: `${userName},记得补充水分,保持身体健康~`, data: { type: 'water_reminder', url: '/statistics' }, sound: 'default', }, trigger: null, }); console.log('简单喝水提醒已发送,ID:', notificationId); } catch (error) { console.error('发送简单喝水提醒失败:', error); } } async function executeBackgroundTasks(): Promise { console.log('开始执行后台任务...'); try { const hasPermission = await checkNotificationPermissions(); if (!hasPermission) { console.log('没有通知权限,跳过后台任务'); return; } try { const state = store.getState(); if (!state) { console.log('Redux store 未初始化,跳过后台任务'); return; } } catch (error) { console.log('无法访问 Redux store,跳过后台任务:', error); return; } const testNotificationsEnabled = await AsyncStorage.getItem('@background_test_notifications_enabled') === 'true'; if (testNotificationsEnabled) { await sendTestNotification(); } await executeWaterReminderTask(); await executeChallengeReminderTask(); await executeFastingNotificationTask(); console.log('后台任务执行完成'); } catch (error) { console.error('执行后台任务失败:', error); throw error; } } export class BackgroundTaskManagerV2 { private static instance: BackgroundTaskManagerV2; private isInitialized = false; private executingPromise: Promise | null = null; private eventSubscription?: EmitterSubscription; private expirationSubscription?: EmitterSubscription; static getInstance(): BackgroundTaskManagerV2 { if (!BackgroundTaskManagerV2.instance) { BackgroundTaskManagerV2.instance = new BackgroundTaskManagerV2(); } return BackgroundTaskManagerV2.instance; } async initialize(): Promise { if (this.isInitialized) { logger.info('[BackgroundTaskManagerV2] 后台任务管理器已初始化,跳过重复初始化'); return; } if (!isIosBackgroundModuleAvailable) { logger.warn('[BackgroundTaskManagerV2] iOS 原生后台模块不可用,跳过初始化'); this.isInitialized = false; return; } const emitter = new NativeEventEmitter(NativeBackgroundModule); this.eventSubscription = emitter.addListener(BACKGROUND_EVENT, (payload) => { logger.info('[BackgroundTaskManagerV2] 收到后台任务事件', payload); this.handleBackgroundExecution(); }); this.expirationSubscription = emitter.addListener(EXPIRATION_EVENT, (payload) => { logger.warn('[BackgroundTaskManagerV2] 后台任务在完成前即将过期', payload); // 处理任务过期情况,确保重新调度 this.handleTaskExpiration(); }); try { // 检查后台刷新状态 const status = await this.getStatus(); logger.info('[BackgroundTaskManagerV2] 后台刷新状态:', status); if (status === 'denied' || status === 'restricted') { logger.warn('[BackgroundTaskManagerV2] 后台刷新被限制或拒绝,后台任务可能无法正常工作'); // 不抛出错误,但标记为未完全初始化 this.isInitialized = false; return; } await NativeBackgroundModule.configure({ identifier: BACKGROUND_TASK_IDENTIFIER, taskType: 'refresh', requiresNetworkConnectivity: false, requiresExternalPower: false, defaultDelay: DEFAULT_RESCHEDULE_INTERVAL_SECONDS, }); this.isInitialized = true; logger.info('[BackgroundTaskManagerV2] 已初始化并注册 iOS 后台任务'); // 立即调度一次后台任务 await this.scheduleNextTask(); // 检查待处理的任务请求 const pendingRequests = await this.getPendingRequests(); logger.info('[BackgroundTaskManagerV2] 当前待处理的任务请求数量:', pendingRequests.length); } catch (error: any) { // BGTaskSchedulerErrorDomain 错误码 1 表示后台任务功能不可用 // 这在模拟器上是正常的,因为模拟器不完全支持后台任务 const errorMessage = error?.message || String(error); const isBGTaskUnavailable = errorMessage.includes('BGTaskSchedulerErrorDomain') && (errorMessage.includes('错误1') || errorMessage.includes('code 1')); if (isBGTaskUnavailable) { logger.warn('[BackgroundTaskManagerV2] 后台任务功能在当前环境不可用(模拟器限制),将在真机上正常工作'); this.removeListeners(); this.isInitialized = false; // 不抛出错误,因为这是预期行为 return; } // 其他错误情况,尝试恢复 logger.error('[BackgroundTaskManagerV2] 初始化失败,尝试恢复', error); try { // 尝试重新初始化一次 await this.attemptRecovery(); } catch (recoveryError) { logger.error('[BackgroundTaskManagerV2] 恢复失败,放弃初始化', recoveryError); this.removeListeners(); throw error; } } } async scheduleNextTask(): Promise { if (!isIosBackgroundModuleAvailable) { return; } try { await NativeBackgroundModule.schedule({ delay: DEFAULT_RESCHEDULE_INTERVAL_SECONDS }); logger.info('[BackgroundTaskManagerV2] 已调度下一次后台任务'); } catch (error) { logger.error('[BackgroundTaskManagerV2] 调度后台任务失败', error); } } private async handleBackgroundExecution(): Promise { if (this.executingPromise) { logger.info('[BackgroundTaskManagerV2] 已有后台任务在执行,忽略重复触发'); return; } const startTime = Date.now(); logger.info('[BackgroundTaskManagerV2] 开始执行后台任务'); this.executingPromise = executeBackgroundTasks() .then(async () => { const executionTime = Date.now() - startTime; logger.info(`[BackgroundTaskManagerV2] 后台任务执行成功,耗时: ${executionTime}ms`); if (isIosBackgroundModuleAvailable) { try { await NativeBackgroundModule.complete(true, DEFAULT_RESCHEDULE_INTERVAL_SECONDS); logger.info('[BackgroundTaskManagerV2] 已标记后台任务成功完成并重新调度'); } catch (error) { logger.error('[BackgroundTaskManagerV2] 标记后台任务成功完成失败', error); // 即使标记失败,也尝试手动重新调度 await this.scheduleNextTask(); } } }) .catch(async (error) => { const executionTime = Date.now() - startTime; logger.error(`[BackgroundTaskManagerV2] 后台任务执行失败,耗时: ${executionTime}ms`, error); if (isIosBackgroundModuleAvailable) { try { await NativeBackgroundModule.complete(false, DEFAULT_RESCHEDULE_INTERVAL_SECONDS); logger.info('[BackgroundTaskManagerV2] 已标记后台任务失败并重新调度'); } catch (completionError) { logger.error('[BackgroundTaskManagerV2] 标记后台任务失败状态时出错', completionError); // 即使标记失败,也尝试手动重新调度 await this.scheduleNextTask(); } } }) .finally(() => { this.executingPromise = null; }); await this.executingPromise; } private async handleTaskExpiration(): Promise { logger.warn('[BackgroundTaskManagerV2] 处理后台任务过期'); // 任务过期时,确保重新调度下一次任务 try { await this.scheduleNextTask(); logger.info('[BackgroundTaskManagerV2] 已为过期的任务重新调度'); } catch (error) { logger.error('[BackgroundTaskManagerV2] 为过期任务重新调度失败', error); } } private async attemptRecovery(): Promise { logger.info('[BackgroundTaskManagerV2] 尝试恢复后台任务功能'); // 等待一段时间后重试 await new Promise(resolve => setTimeout(resolve, 2000)); // 取消所有现有任务 if (isIosBackgroundModuleAvailable) { try { await NativeBackgroundModule.cancelAll(); logger.info('[BackgroundTaskManagerV2] 已取消所有现有后台任务'); } catch (error) { logger.warn('[BackgroundTaskManagerV2] 取消现有任务失败', error); } } // 重新配置 await NativeBackgroundModule.configure({ identifier: BACKGROUND_TASK_IDENTIFIER, taskType: 'refresh', requiresNetworkConnectivity: false, requiresExternalPower: false, defaultDelay: DEFAULT_RESCHEDULE_INTERVAL_SECONDS, }); logger.info('[BackgroundTaskManagerV2] 后台任务功能恢复成功'); } async stop(): Promise { if (!isIosBackgroundModuleAvailable) { return; } try { await NativeBackgroundModule.cancelAll(); } catch (error) { logger.error('[BackgroundTaskManagerV2] 停止后台任务失败', error); } finally { this.removeListeners(); this.isInitialized = false; } } private removeListeners(): void { this.eventSubscription?.remove(); this.expirationSubscription?.remove(); this.eventSubscription = undefined; this.expirationSubscription = undefined; } async getStatus(): Promise { if (!isIosBackgroundModuleAvailable) { return Platform.OS; } try { const status = await NativeBackgroundModule.backgroundRefreshStatus(); return status; } catch (error) { logger.error('[BackgroundTaskManagerV2] 获取后台任务状态失败', error); return 'unknown'; } } async checkStatus(): Promise { const status = await this.getStatus(); switch (status) { case 'available': return '可用'; case 'restricted': return '受限制'; case 'denied': return '被拒绝'; default: return '未知'; } } async triggerTaskForTesting(): Promise { if (!isIosBackgroundModuleAvailable) { await executeBackgroundTasks(); return; } try { await NativeBackgroundModule.simulateLaunch(); } catch (error) { logger.error('[BackgroundTaskManagerV2] 模拟后台任务触发失败', error); throw error; } } async testBackgroundTask(): Promise { await this.triggerTaskForTesting(); } async getLastBackgroundCheckTime(): Promise { try { const lastCheck = await AsyncStorage.getItem('@last_background_water_check'); return lastCheck ? parseInt(lastCheck, 10) : null; } catch (error) { console.error('获取最后后台检查时间失败:', error); return null; } } async getPendingRequests(): Promise { if (!isIosBackgroundModuleAvailable) { return []; } try { const requests = await NativeBackgroundModule.getPendingRequests(); return Array.isArray(requests) ? requests : []; } catch (error) { logger.error('[BackgroundTaskManagerV2] 获取待处理的后台任务请求失败', error); return []; } } } export type BackgroundTaskEvent = { taskId: string; timestamp: number; success: boolean; error?: string; }; export const BackgroundTaskManager = BackgroundTaskManagerV2;