diff --git a/app.json b/app.json index 0dd45d7..71386ef 100644 --- a/app.json +++ b/app.json @@ -18,8 +18,7 @@ "NSPhotoLibraryUsageDescription": "应用需要访问相册以选择您的体态照片用于AI测评。", "NSPhotoLibraryAddUsageDescription": "应用需要写入相册以保存拍摄的体态照片(可选)。", "UIBackgroundModes": [ - "background-fetch", - "background-processing" + "processing" ] } }, @@ -70,12 +69,7 @@ ] } ], - [ - "expo-background-fetch", - { - "minimumInterval": 15 - } - ] + "expo-background-task" ], "experiments": { "typedRoutes": true diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index 3114e8a..d39eeff 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -15,6 +15,7 @@ import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useBackgroundTasks } from '@/hooks/useBackgroundTasks'; import { notificationService } from '@/services/notifications'; +import { backgroundTaskManager } from '@/services/backgroundTaskManager'; import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice'; import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice'; import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice'; @@ -37,7 +38,8 @@ import { ScrollView, StyleSheet, Text, - View + View, + TouchableOpacity } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; @@ -618,6 +620,19 @@ export default function ExploreScreen() { 海豹健康 + + {/* 开发环境调试按钮 */} + {__DEV__ && ( + { + console.log('🔧 手动触发后台任务测试...'); + await backgroundTaskManager.debugExecuteBackgroundTask(); + }} + > + 🔧 + + )} @@ -813,6 +828,25 @@ const styles = StyleSheet.create({ fontWeight: '500', color: '#192126', }, + debugButton: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: '#FF6B6B', + alignItems: 'center', + justifyContent: 'center', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + debugButtonText: { + fontSize: 12, + }, sectionTitle: { diff --git a/hooks/useBackgroundTasks.ts b/hooks/useBackgroundTasks.ts index 2ea74fd..a713cb7 100644 --- a/hooks/useBackgroundTasks.ts +++ b/hooks/useBackgroundTasks.ts @@ -1,15 +1,15 @@ -import { BackgroundTaskType as BackgroundTask, backgroundTaskManager, TaskStatusType as TaskStatus } from '@/services/backgroundTaskManager'; -import * as BackgroundFetch from 'expo-background-fetch'; +import { BackgroundTaskConfig, backgroundTaskManager, TaskStatus } from '@/services/backgroundTaskManager'; +import * as BackgroundTask from 'expo-background-task'; import { useCallback, useEffect, useState } from 'react'; export interface UseBackgroundTasksReturn { // 状态 isInitialized: boolean; taskStatuses: TaskStatus[]; - registeredTasks: BackgroundTask[]; + registeredTasks: BackgroundTaskConfig[]; // 方法 - registerTask: (task: BackgroundTask) => Promise; + registerTask: (task: BackgroundTaskConfig) => Promise; unregisterTask: (taskId: string) => Promise; executeTask: (taskId: string, data?: any) => Promise; executeAllTasks: () => Promise<{ [taskId: string]: 'success' | 'failed' }>; @@ -17,15 +17,15 @@ export interface UseBackgroundTasksReturn { cleanupTaskStatuses: () => Promise; // 后台任务状态 - backgroundTaskStatus: BackgroundFetch.BackgroundFetchStatus | null; + backgroundTaskStatus: BackgroundTask.BackgroundTaskStatus | null; getBackgroundTaskStatus: () => Promise; } export const useBackgroundTasks = (): UseBackgroundTasksReturn => { const [isInitialized, setIsInitialized] = useState(false); const [taskStatuses, setTaskStatuses] = useState([]); - const [registeredTasks, setRegisteredTasks] = useState([]); - const [backgroundTaskStatus, setBackgroundTaskStatus] = useState(null); + const [registeredTasks, setRegisteredTasks] = useState([]); + const [backgroundTaskStatus, setBackgroundTaskStatus] = useState(null); // 初始化 useEffect(() => { @@ -49,7 +49,7 @@ export const useBackgroundTasks = (): UseBackgroundTasksReturn => { }, []); // 注册任务 - const registerTask = useCallback(async (task: BackgroundTask) => { + const registerTask = useCallback(async (task: BackgroundTaskConfig) => { await backgroundTaskManager.registerTask(task); refreshData(); }, [refreshData]); diff --git a/services/backgroundTaskManager.ts b/services/backgroundTaskManager.ts index 8eed91d..bb11fd3 100644 --- a/services/backgroundTaskManager.ts +++ b/services/backgroundTaskManager.ts @@ -1,16 +1,38 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; -import * as BackgroundFetch from 'expo-background-fetch'; +import * as BackgroundTask from 'expo-background-task'; import * as TaskManager from 'expo-task-manager'; +// 后台任务名称常量 +const BACKGROUND_TASK_NAME = 'health-background-task'; + +// 必须在全局作用域定义任务处理器 +TaskManager.defineTask(BACKGROUND_TASK_NAME, async () => { + console.log('======= 后台任务被系统调用 ======='); + const now = new Date(); + console.log(`后台任务执行时间: ${now.toISOString()}`); + + try { + // 获取后台任务管理器实例并执行所有注册的任务 + const manager = BackgroundTaskManager.getInstance(); + console.log('开始执行所有注册的任务...'); + const results = await manager.executeAllTasks(); + console.log('后台任务执行结果:', results); + + // 返回成功状态 + return BackgroundTask.BackgroundTaskResult.Success; + } catch (error) { + console.error('后台任务执行失败:', error); + return BackgroundTask.BackgroundTaskResult.Failed; + } +}); + // 任务类型定义 -export interface BackgroundTask { +export interface BackgroundTaskConfig { id: string; name: string; handler: (data?: any) => Promise; options?: { minimumInterval?: number; // 最小间隔时间(分钟) - stopOnTerminate?: boolean; // 应用终止时是否停止 - startOnBoot?: boolean; // 设备重启时是否启动 }; } @@ -27,9 +49,10 @@ export interface TaskStatus { // 后台任务管理器类 class BackgroundTaskManager { private static instance: BackgroundTaskManager; - private tasks: Map = new Map(); + private tasks: Map = new Map(); private taskStatuses: Map = new Map(); private isInitialized = false; + private systemTaskRegistered = false; // 单例模式 public static getInstance(): BackgroundTaskManager { @@ -49,11 +72,11 @@ class BackgroundTaskManager { this.isInitialized = true; // 注册后台任务 - await this.registerBackgroundTask(); - + await this.registerSystemBackgroundTask(); + // 加载已保存的任务状态 await this.loadTaskStatuses(); - + console.log('后台任务管理器初始化成功'); } catch (error) { console.error('后台任务管理器初始化失败:', error); @@ -61,53 +84,49 @@ class BackgroundTaskManager { } } - // 注册后台任务 - private async registerBackgroundTask(): Promise { - const BACKGROUND_FETCH_TASK = 'background-fetch-task'; + // 注册系统后台任务 + private async registerSystemBackgroundTask(): Promise { + console.log('开始注册系统后台任务...'); - console.log('注册后台获取任务'); - - // 定义后台获取任务 - TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { - console.log('后台获取任务被系统调用'); - const now = new Date(); - - try { - console.log(`开始执行后台任务 - ${now.toISOString()}`); - - // 执行所有注册的任务 - const results = await this.executeAllTasks(); - - console.log('后台任务执行完成:', results); - - // 返回成功状态 - return BackgroundFetch.BackgroundFetchResult.NewData; - } catch (error) { - console.error('后台任务执行失败:', error); - return BackgroundFetch.BackgroundFetchResult.Failed; - } - }); - - // 注册后台获取任务 try { - const status = await BackgroundFetch.getStatusAsync(); - if (status === BackgroundFetch.BackgroundFetchStatus.Available) { - await BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, { - minimumInterval: 15 * 60, // 15分钟(以秒为单位) - stopOnTerminate: false, - startOnBoot: true, - }); - console.log('后台获取任务注册成功'); + // 检查后台任务状态 + const status = await BackgroundTask.getStatusAsync(); + console.log('后台任务服务状态:', BackgroundTask.BackgroundTaskStatus[status]); + + if (status === BackgroundTask.BackgroundTaskStatus.Available) { + // 检查任务是否已经注册 + const isRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_TASK_NAME); + console.log('系统任务是否已注册:', isRegistered); + + if (!isRegistered) { + // 注册后台任务 + await BackgroundTask.registerTaskAsync(BACKGROUND_TASK_NAME); + console.log('✅ 系统后台任务注册成功'); + this.systemTaskRegistered = true; + } else { + console.log('✅ 系统后台任务已经注册,跳过重复注册'); + this.systemTaskRegistered = true; + } } else { - console.warn('后台获取不可用,状态:', status); + const statusText = Object.keys(BackgroundTask.BackgroundTaskStatus).find( + key => BackgroundTask.BackgroundTaskStatus[key as keyof typeof BackgroundTask.BackgroundTaskStatus] === status + ); + console.warn('❌ 后台任务服务不可用,状态:', statusText || status); + console.warn('可能的原因:'); + console.warn('- 设备省电模式开启'); + console.warn('- 后台应用刷新被禁用'); + console.warn('- 设备电量过低'); + this.systemTaskRegistered = false; } } catch (error) { - console.error('注册后台获取任务失败:', error); + console.error('❌ 注册系统后台任务失败:', error); + this.systemTaskRegistered = false; + throw error; } } // 注册自定义任务 - public async registerTask(task: BackgroundTask): Promise { + public async registerTask(task: BackgroundTaskConfig): Promise { try { // 检查任务是否已存在 if (this.tasks.has(task.id)) { @@ -165,10 +184,10 @@ class BackgroundTaskManager { } console.log(`开始执行任务: ${taskId}`); - + // 执行任务 await task.handler(data); - + // 更新任务状态 const status = this.taskStatuses.get(taskId); if (status) { @@ -181,14 +200,14 @@ class BackgroundTaskManager { console.log(`任务 ${taskId} 执行成功`); } catch (error) { console.error(`执行任务 ${taskId} 失败:`, error); - + // 更新错误状态 const status = this.taskStatuses.get(taskId); if (status) { status.lastError = error instanceof Error ? error.message : String(error); await this.saveTaskStatuses(); } - + throw error; } } @@ -196,8 +215,8 @@ class BackgroundTaskManager { // 执行所有任务 public async executeAllTasks(): Promise<{ [taskId: string]: 'success' | 'failed' }> { const results: { [taskId: string]: 'success' | 'failed' } = {}; - - for (const [taskId, task] of this.tasks) { + + for (const [taskId, task] of Array.from(this.tasks.entries())) { try { await this.executeTask(taskId); results[taskId] = 'success'; @@ -206,7 +225,7 @@ class BackgroundTaskManager { results[taskId] = 'failed'; } } - + return results; } @@ -221,13 +240,13 @@ class BackgroundTaskManager { } // 获取已注册的任务列表 - public getRegisteredTasks(): BackgroundTask[] { + public getRegisteredTasks(): BackgroundTaskConfig[] { return Array.from(this.tasks.values()); } // 检查后台任务状态 - public async getBackgroundTaskStatus(): Promise { - return await BackgroundFetch.getStatusAsync(); + public async getBackgroundTaskStatus(): Promise { + return await BackgroundTask.getStatusAsync(); } // 保存任务状态到本地存储 @@ -259,50 +278,92 @@ class BackgroundTaskManager { public async cleanupTaskStatuses(): Promise { const now = new Date(); const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); - + for (const [taskId, status] of this.taskStatuses) { if (status.lastExecution && status.lastExecution < thirtyDaysAgo && !status.isRegistered) { this.taskStatuses.delete(taskId); } } - + await this.saveTaskStatuses(); } - // 调试函数:强制触发后台任务执行 + // 手动触发后台任务(仅开发环境) + public async triggerTaskForTesting(): Promise { + if (!__DEV__) { + console.warn('⚠️ triggerTaskForTesting 仅在开发环境可用'); + return; + } + + try { + console.log('🧪 触发后台任务进行测试...'); + await BackgroundTask.triggerTaskWorkerForTestingAsync(); + console.log('✅ 后台任务测试触发成功'); + } catch (error) { + console.error('❌ 触发后台任务测试失败:', error); + } + } + + // 调试函数:显示后台任务状态 public async debugExecuteBackgroundTask(): Promise { - console.log('=== 调试:手动触发后台任务执行 ==='); - + console.log('==============================='); + console.log('🔧 调试:后台任务状态检查'); + console.log('==============================='); + try { // 获取后台任务状态 const status = await this.getBackgroundTaskStatus(); - console.log('后台获取状态:', status); - - // 执行所有注册的任务 - console.log('当前注册的任务数量:', this.tasks.size); + const statusText = Object.keys(BackgroundTask.BackgroundTaskStatus).find( + key => BackgroundTask.BackgroundTaskStatus[key as keyof typeof BackgroundTask.BackgroundTaskStatus] === status + ); + console.log('📊 后台任务服务状态:', statusText || status); + + // 检查系统任务是否已注册 + const isSystemTaskRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_TASK_NAME); + console.log('🔄 系统后台任务是否已注册:', isSystemTaskRegistered); + console.log('🔄 管理器中的系统任务状态:', this.systemTaskRegistered); + + // 显示自定义任务信息 + console.log('📝 当前注册的自定义任务数量:', this.tasks.size); this.tasks.forEach((task, id) => { - console.log(`- 任务ID: ${id}, 名称: ${task.name}`); + console.log(` - 任务ID: ${id}, 名称: ${task.name}`); }); - + + if (this.tasks.size === 0) { + console.warn('⚠️ 没有注册的自定义任务'); + } + + // 手动执行所有任务 + console.log('🚀 手动执行所有注册的任务...'); const results = await this.executeAllTasks(); - console.log('任务执行结果:', results); - - // 显示任务状态 + console.log('✅ 任务执行结果:', results); + + // 显示详细的任务状态 + console.log('📈 任务执行统计:'); const taskStatuses = this.getAllTaskStatuses(); - taskStatuses.forEach(status => { - console.log(`任务 ${status.id} 状态:`, { - isRegistered: status.isRegistered, - executionCount: status.executionCount, - lastExecution: status.lastExecution?.toISOString(), - lastError: status.lastError + taskStatuses.forEach(taskStatus => { + console.log(` 📊 任务 ${taskStatus.id}:`, { + 注册状态: taskStatus.isRegistered ? '✅ 已注册' : '❌ 未注册', + 执行次数: taskStatus.executionCount, + 最后执行: taskStatus.lastExecution?.toLocaleString('zh-CN') || '从未执行', + 最后错误: taskStatus.lastError || '无' }); }); - + + // 开发环境下触发测试 + if (__DEV__) { + console.log('🧪 开发环境:触发后台任务测试...'); + await this.triggerTaskForTesting(); + } + + console.log('==============================='); + console.log('✅ 调试检查完成'); + console.log('==============================='); + } catch (error) { - console.error('调试执行失败:', error); + console.error('❌ 调试执行失败:', error); + console.log('==============================='); } - - console.log('=== 调试执行完成 ==='); } } @@ -310,4 +371,4 @@ class BackgroundTaskManager { export const backgroundTaskManager = BackgroundTaskManager.getInstance(); // 导出类型 -export type { BackgroundTask as BackgroundTaskType, TaskStatus as TaskStatusType }; +export type { BackgroundTaskConfig as BackgroundTaskType, TaskStatus as TaskStatusType }; diff --git a/services/notifications.ts b/services/notifications.ts index f0e4f27..ebfbe61 100644 --- a/services/notifications.ts +++ b/services/notifications.ts @@ -182,7 +182,7 @@ export class NotificationService { } /** - * 检查用户是否允许推送通知 + * 检查用户是否允许推送通知(不包括系统权限检查,仅检查用户偏好) */ private async isNotificationAllowed(): Promise { try { @@ -193,16 +193,35 @@ export class NotificationService { return false; } + return true; + } catch (error) { + console.error('检查推送权限失败:', error); + // 如果检查失败,默认允许(避免阻塞重要的健康提醒) + return true; + } + } + + /** + * 完整的权限检查(包括系统权限和用户偏好) + */ + private async hasFullNotificationPermission(): Promise { + try { // 检查系统权限 const permissionStatus = await this.getPermissionStatus(); if (permissionStatus !== 'granted') { - console.log('系统推送权限未授予'); + console.log('系统推送权限未授予:', permissionStatus); + return false; + } + + // 检查用户偏好 + const userPreferenceEnabled = await this.isNotificationAllowed(); + if (!userPreferenceEnabled) { return false; } return true; } catch (error) { - console.error('检查推送权限失败:', error); + console.error('完整权限检查失败:', error); return false; } } @@ -215,11 +234,11 @@ export class NotificationService { trigger?: Notifications.NotificationTriggerInput ): Promise { try { - // 检查用户是否允许推送通知 - const isAllowed = await this.isNotificationAllowed(); - if (!isAllowed) { - console.log('推送通知被用户偏好设置或系统权限阻止,跳过发送'); - return 'blocked_by_user_preference'; + // 检查完整权限(系统权限 + 用户偏好) + const hasPermission = await this.hasFullNotificationPermission(); + if (!hasPermission) { + console.log('推送通知被系统权限或用户偏好设置阻止,跳过发送'); + return 'blocked_by_permission_or_preference'; } const notificationId = await Notifications.scheduleNotificationAsync({ @@ -234,10 +253,10 @@ export class NotificationService { trigger: trigger || null, // null表示立即发送 }); - console.log('本地通知已安排,ID:', notificationId); + console.log('✅ 本地通知已安排,ID:', notificationId); return notificationId; } catch (error) { - console.error('安排本地通知失败:', error); + console.error('❌ 安排本地通知失败:', error); throw error; } } @@ -246,6 +265,7 @@ export class NotificationService { * 发送立即通知 */ async sendImmediateNotification(notification: NotificationData): Promise { + console.log('📱 准备发送立即通知:', notification.title); return this.scheduleLocalNotification(notification); }