feat: 添加后台任务管理器,支持喝水和站立提醒功能
This commit is contained in:
@@ -527,6 +527,8 @@ export default function ExploreScreen() {
|
|||||||
}
|
}
|
||||||
}, [todayWaterStats, userProfile]);
|
}, [todayWaterStats, userProfile]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 日期点击时,加载对应日期数据
|
// 日期点击时,加载对应日期数据
|
||||||
const onSelectDate = React.useCallback((index: number, date: Date) => {
|
const onSelectDate = React.useCallback((index: number, date: Date) => {
|
||||||
setSelectedIndex(index);
|
setSelectedIndex(index);
|
||||||
@@ -1082,4 +1084,5 @@ const styles = StyleSheet.create({
|
|||||||
padding: 4,
|
padding: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { notificationService } from '@/services/notifications';
|
|||||||
import { store } from '@/store';
|
import { store } from '@/store';
|
||||||
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
|
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
|
||||||
import { MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers';
|
import { MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers';
|
||||||
|
import { backgroundTaskManager } from '@/services/backgroundTaskManager';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import RNExitApp from 'react-native-exit-app';
|
import RNExitApp from 'react-native-exit-app';
|
||||||
|
|
||||||
@@ -35,8 +36,12 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
// 初始化通知服务
|
// 初始化通知服务
|
||||||
await notificationService.initialize();
|
await notificationService.initialize();
|
||||||
console.log('通知服务初始化成功');
|
console.log('通知服务初始化成功');
|
||||||
|
|
||||||
|
// 初始化后台任务管理器
|
||||||
|
await backgroundTaskManager.initialize();
|
||||||
|
console.log('后台任务管理器初始化成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('通知服务初始化失败:', error);
|
console.error('通知服务或后台任务管理器初始化失败:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,6 +76,10 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
// 注册心情提醒(21:00)
|
// 注册心情提醒(21:00)
|
||||||
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name);
|
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name);
|
||||||
console.log('心情提醒已注册');
|
console.log('心情提醒已注册');
|
||||||
|
|
||||||
|
// 注册喝水提醒后台任务
|
||||||
|
await backgroundTaskManager.registerWaterReminderTask();
|
||||||
|
console.log('喝水提醒后台任务已注册');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('注册提醒失败:', error);
|
console.error('注册提醒失败:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1739,6 +1739,8 @@ PODS:
|
|||||||
- RevenueCat (5.34.0)
|
- RevenueCat (5.34.0)
|
||||||
- RNAppleHealthKit (1.7.0):
|
- RNAppleHealthKit (1.7.0):
|
||||||
- React
|
- React
|
||||||
|
- RNBackgroundFetch (4.2.8):
|
||||||
|
- React-Core
|
||||||
- RNCAsyncStorage (2.2.0):
|
- RNCAsyncStorage (2.2.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNCMaskedView (0.3.2):
|
- RNCMaskedView (0.3.2):
|
||||||
@@ -2086,6 +2088,7 @@ DEPENDENCIES:
|
|||||||
- ReactCodegen (from `build/generated/ios`)
|
- ReactCodegen (from `build/generated/ios`)
|
||||||
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
||||||
- RNAppleHealthKit (from `../node_modules/react-native-health`)
|
- RNAppleHealthKit (from `../node_modules/react-native-health`)
|
||||||
|
- RNBackgroundFetch (from `../node_modules/react-native-background-fetch`)
|
||||||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||||
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
|
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
|
||||||
- "RNCPicker (from `../node_modules/@react-native-picker/picker`)"
|
- "RNCPicker (from `../node_modules/@react-native-picker/picker`)"
|
||||||
@@ -2312,6 +2315,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native/ReactCommon"
|
:path: "../node_modules/react-native/ReactCommon"
|
||||||
RNAppleHealthKit:
|
RNAppleHealthKit:
|
||||||
:path: "../node_modules/react-native-health"
|
:path: "../node_modules/react-native-health"
|
||||||
|
RNBackgroundFetch:
|
||||||
|
:path: "../node_modules/react-native-background-fetch"
|
||||||
RNCAsyncStorage:
|
RNCAsyncStorage:
|
||||||
:path: "../node_modules/@react-native-async-storage/async-storage"
|
:path: "../node_modules/@react-native-async-storage/async-storage"
|
||||||
RNCMaskedView:
|
RNCMaskedView:
|
||||||
@@ -2445,6 +2450,7 @@ SPEC CHECKSUMS:
|
|||||||
ReactCommon: 7eb76fcd5133313d8c6a138a5c7dd89f80f189d5
|
ReactCommon: 7eb76fcd5133313d8c6a138a5c7dd89f80f189d5
|
||||||
RevenueCat: eb2aa042789d9c99ad5172bd96e28b96286d6ada
|
RevenueCat: eb2aa042789d9c99ad5172bd96e28b96286d6ada
|
||||||
RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9
|
RNAppleHealthKit: 86ef7ab70f762b802f5c5289372de360cca701f9
|
||||||
|
RNBackgroundFetch: e44c9e85d7fb3122c37d8a806278f62c7682d7ea
|
||||||
RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f
|
RNCAsyncStorage: b44e8a4e798c3e1f56bffccd0f591f674fb9198f
|
||||||
RNCMaskedView: d4644e239e65383f96d2f32c40c297f09705ac96
|
RNCMaskedView: d4644e239e65383f96d2f32c40c297f09705ac96
|
||||||
RNCPicker: da0f1c9411208c1ca52bc98383db54a06e0a3862
|
RNCPicker: da0f1c9411208c1ca52bc98383db54a06e0a3862
|
||||||
|
|||||||
@@ -273,6 +273,7 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/QCloudCOSXML/QCloudCOSXML.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/QCloudCOSXML/QCloudCOSXML.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNBackgroundFetch/TSBackgroundFetchPrivacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNDeviceInfo/RNDeviceInfoPrivacyInfo.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||||
@@ -297,6 +298,7 @@
|
|||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QCloudCOSXML.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QCloudCOSXML.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TSBackgroundFetchPrivacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNDeviceInfoPrivacyInfo.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
||||||
|
|||||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -44,6 +44,7 @@
|
|||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-native": "0.79.5",
|
"react-native": "0.79.5",
|
||||||
|
"react-native-background-fetch": "^4.2.8",
|
||||||
"react-native-cos-sdk": "^1.2.1",
|
"react-native-cos-sdk": "^1.2.1",
|
||||||
"react-native-device-info": "^14.0.4",
|
"react-native-device-info": "^14.0.4",
|
||||||
"react-native-exit-app": "^2.0.0",
|
"react-native-exit-app": "^2.0.0",
|
||||||
@@ -11397,6 +11398,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-background-fetch": {
|
||||||
|
"version": "4.2.8",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/react-native-background-fetch/-/react-native-background-fetch-4.2.8.tgz",
|
||||||
|
"integrity": "sha512-vKPumvhBuxr3oI1L7cNunYIsKV8jD4Xz2A9JT/FW5yvn7GAAct184FAZ9dFef75auBxixinaCjRBlip53xGWmQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react-native-cos-sdk": {
|
"node_modules/react-native-cos-sdk": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://mirrors.tencent.com/npm/react-native-cos-sdk/-/react-native-cos-sdk-1.2.1.tgz",
|
"resolved": "https://mirrors.tencent.com/npm/react-native-cos-sdk/-/react-native-cos-sdk-1.2.1.tgz",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-native": "0.79.5",
|
"react-native": "0.79.5",
|
||||||
|
"react-native-background-fetch": "^4.2.8",
|
||||||
"react-native-cos-sdk": "^1.2.1",
|
"react-native-cos-sdk": "^1.2.1",
|
||||||
"react-native-device-info": "^14.0.4",
|
"react-native-device-info": "^14.0.4",
|
||||||
"react-native-exit-app": "^2.0.0",
|
"react-native-exit-app": "^2.0.0",
|
||||||
@@ -77,4 +78,4 @@
|
|||||||
"typescript": "~5.8.3"
|
"typescript": "~5.8.3"
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
|||||||
421
services/backgroundTaskManager.ts
Normal file
421
services/backgroundTaskManager.ts
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
import { store } from '@/store';
|
||||||
|
import { StandReminderHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import BackgroundFetch from 'react-native-background-fetch';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台任务标识符
|
||||||
|
*/
|
||||||
|
export const BACKGROUND_TASK_IDS = {
|
||||||
|
WATER_REMINDER: 'water-reminder-task',
|
||||||
|
STAND_REMINDER: 'stand-reminder-task',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台任务管理器
|
||||||
|
* 负责配置和管理 iOS 应用的后台任务执行
|
||||||
|
*/
|
||||||
|
export class BackgroundTaskManager {
|
||||||
|
private static instance: BackgroundTaskManager;
|
||||||
|
private isInitialized = false;
|
||||||
|
|
||||||
|
static getInstance(): BackgroundTaskManager {
|
||||||
|
if (!BackgroundTaskManager.instance) {
|
||||||
|
BackgroundTaskManager.instance = new BackgroundTaskManager();
|
||||||
|
}
|
||||||
|
return BackgroundTaskManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化后台任务管理器
|
||||||
|
*/
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
if (this.isInitialized) {
|
||||||
|
console.log('后台任务管理器已初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 配置后台获取
|
||||||
|
const status = await BackgroundFetch.configure({
|
||||||
|
minimumFetchInterval: 15000, // 最小间隔15分钟(iOS 实际控制间隔)
|
||||||
|
}, async (taskId) => {
|
||||||
|
console.log('[BackgroundFetch] 后台任务执行:', taskId);
|
||||||
|
await this.executeBackgroundTasks();
|
||||||
|
// 完成任务
|
||||||
|
BackgroundFetch.finish(taskId);
|
||||||
|
}, (error) => {
|
||||||
|
console.error('[BackgroundFetch] 配置失败:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[BackgroundFetch] 配置状态:', status);
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
console.log('后台任务管理器初始化完成');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化后台任务管理器失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行后台任务
|
||||||
|
*/
|
||||||
|
private async executeBackgroundTasks(): Promise<void> {
|
||||||
|
console.log('开始执行后台任务...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查应用权限和用户设置
|
||||||
|
const hasPermission = await this.checkNotificationPermissions();
|
||||||
|
if (!hasPermission) {
|
||||||
|
console.log('没有通知权限,跳过后台任务');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行喝水提醒检查任务
|
||||||
|
await this.executeWaterReminderTask();
|
||||||
|
|
||||||
|
// 执行站立提醒检查任务
|
||||||
|
await this.executeStandReminderTask();
|
||||||
|
|
||||||
|
console.log('后台任务执行完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('执行后台任务失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行喝水提醒后台任务
|
||||||
|
*/
|
||||||
|
private async executeWaterReminderTask(): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('执行喝水提醒后台任务...');
|
||||||
|
|
||||||
|
// 获取当前状态
|
||||||
|
const state = store.getState();
|
||||||
|
const waterStats = state.water.todayStats;
|
||||||
|
const userProfile = state.user.profile;
|
||||||
|
|
||||||
|
// 检查是否有喝水目标设置
|
||||||
|
if (!waterStats || !waterStats.dailyGoal || waterStats.dailyGoal <= 0) {
|
||||||
|
console.log('没有设置喝水目标,跳过喝水提醒');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查时间限制(避免深夜打扰)
|
||||||
|
const currentHour = new Date().getHours();
|
||||||
|
if (currentHour < 9 || currentHour >= 21) {
|
||||||
|
console.log(`当前时间${currentHour}点,不在提醒时间范围内,跳过喝水提醒`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户名
|
||||||
|
const userName = userProfile?.name || '朋友';
|
||||||
|
|
||||||
|
// 构造今日统计数据
|
||||||
|
const todayWaterStats = {
|
||||||
|
totalAmount: waterStats.totalAmount || 0,
|
||||||
|
dailyGoal: waterStats.dailyGoal,
|
||||||
|
completionRate: waterStats.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行站立提醒后台任务
|
||||||
|
*/
|
||||||
|
private async executeStandReminderTask(): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log('执行站立提醒后台任务...');
|
||||||
|
|
||||||
|
// 获取当前状态
|
||||||
|
const state = store.getState();
|
||||||
|
const userProfile = state.user.profile;
|
||||||
|
|
||||||
|
// 检查时间限制(工作时间内提醒,避免深夜或清晨打扰)
|
||||||
|
const currentHour = new Date().getHours();
|
||||||
|
if (currentHour < 9 || currentHour >= 21) {
|
||||||
|
console.log(`当前时间${currentHour}点,不在站立提醒时间范围内,跳过站立提醒`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户名
|
||||||
|
const userName = userProfile?.name || '朋友';
|
||||||
|
|
||||||
|
// 调用站立提醒检查函数
|
||||||
|
const notificationSent = await StandReminderHelpers.checkStandStatusAndNotify(userName);
|
||||||
|
|
||||||
|
if (notificationSent) {
|
||||||
|
console.log('后台站立提醒通知已发送');
|
||||||
|
// 记录后台任务执行时间
|
||||||
|
await AsyncStorage.setItem('@last_background_stand_check', Date.now().toString());
|
||||||
|
} else {
|
||||||
|
console.log('无需发送后台站立提醒通知');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('执行站立提醒后台任务失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查通知权限
|
||||||
|
*/
|
||||||
|
private async checkNotificationPermissions(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const Notifications = await import('expo-notifications');
|
||||||
|
const { status } = await Notifications.getPermissionsAsync();
|
||||||
|
return status === 'granted';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查通知权限失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动后台任务
|
||||||
|
*/
|
||||||
|
async start(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await BackgroundFetch.start();
|
||||||
|
console.log('后台任务已启动');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('启动后台任务失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止后台任务
|
||||||
|
*/
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await BackgroundFetch.stop();
|
||||||
|
console.log('后台任务已停止');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('停止后台任务失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取后台任务状态
|
||||||
|
*/
|
||||||
|
async getStatus(): Promise<number> {
|
||||||
|
try {
|
||||||
|
return await BackgroundFetch.status();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取后台任务状态失败:', error);
|
||||||
|
return BackgroundFetch.STATUS_DENIED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查后台任务状态
|
||||||
|
*/
|
||||||
|
async checkStatus(): Promise<string> {
|
||||||
|
const status = await this.getStatus();
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case BackgroundFetch.STATUS_AVAILABLE:
|
||||||
|
return '可用';
|
||||||
|
case BackgroundFetch.STATUS_DENIED:
|
||||||
|
return '被拒绝';
|
||||||
|
case BackgroundFetch.STATUS_RESTRICTED:
|
||||||
|
return '受限制';
|
||||||
|
default:
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试后台任务
|
||||||
|
*/
|
||||||
|
async testBackgroundTask(): Promise<void> {
|
||||||
|
console.log('开始测试后台任务...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 手动触发后台任务执行
|
||||||
|
await this.executeBackgroundTasks();
|
||||||
|
console.log('后台任务测试完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('后台任务测试失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册喝水提醒后台任务
|
||||||
|
*/
|
||||||
|
async registerWaterReminderTask(): Promise<void> {
|
||||||
|
console.log('注册喝水提醒后台任务...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查是否已经初始化
|
||||||
|
if (!this.isInitialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动后台任务
|
||||||
|
await this.start();
|
||||||
|
|
||||||
|
console.log('喝水提醒后台任务注册成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('注册喝水提醒后台任务失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消喝水提醒后台任务
|
||||||
|
*/
|
||||||
|
async unregisterWaterReminderTask(): Promise<void> {
|
||||||
|
console.log('取消喝水提醒后台任务...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.stop();
|
||||||
|
console.log('喝水提醒后台任务已取消');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('取消喝水提醒后台任务失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最后一次后台检查时间
|
||||||
|
*/
|
||||||
|
async getLastBackgroundCheckTime(): Promise<number | null> {
|
||||||
|
try {
|
||||||
|
const lastCheck = await AsyncStorage.getItem('@last_background_water_check');
|
||||||
|
return lastCheck ? parseInt(lastCheck) : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取最后后台检查时间失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册站立提醒后台任务
|
||||||
|
*/
|
||||||
|
async registerStandReminderTask(): Promise<void> {
|
||||||
|
console.log('注册站立提醒后台任务...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查是否已经初始化
|
||||||
|
if (!this.isInitialized) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动后台任务
|
||||||
|
await this.start();
|
||||||
|
|
||||||
|
console.log('站立提醒后台任务注册成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('注册站立提醒后台任务失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消站立提醒后台任务
|
||||||
|
*/
|
||||||
|
async unregisterStandReminderTask(): Promise<void> {
|
||||||
|
console.log('取消站立提醒后台任务...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 取消所有相关通知
|
||||||
|
await StandReminderHelpers.cancelStandReminders();
|
||||||
|
console.log('站立提醒后台任务已取消');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('取消站立提醒后台任务失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最后一次站立检查时间
|
||||||
|
*/
|
||||||
|
async getLastStandCheckTime(): Promise<number | null> {
|
||||||
|
try {
|
||||||
|
const lastCheck = await AsyncStorage.getItem('@last_background_stand_check');
|
||||||
|
return lastCheck ? parseInt(lastCheck) : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取最后站立检查时间失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试站立提醒任务
|
||||||
|
*/
|
||||||
|
async testStandReminderTask(): Promise<void> {
|
||||||
|
console.log('开始测试站立提醒后台任务...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 手动触发站立提醒任务执行
|
||||||
|
await this.executeStandReminderTask();
|
||||||
|
console.log('站立提醒后台任务测试完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('站立提醒后台任务测试失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台任务管理器单例实例
|
||||||
|
*/
|
||||||
|
export const backgroundTaskManager = BackgroundTaskManager.getInstance();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台任务事件类型
|
||||||
|
*/
|
||||||
|
export interface BackgroundTaskEvent {
|
||||||
|
taskId: string;
|
||||||
|
timestamp: number;
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台任务配置选项
|
||||||
|
*/
|
||||||
|
export interface BackgroundTaskConfig {
|
||||||
|
minimumFetchInterval?: number;
|
||||||
|
stopOnTerminate?: boolean;
|
||||||
|
startOnBoot?: boolean;
|
||||||
|
enableHeadless?: boolean;
|
||||||
|
requiredNetworkType?: 'NONE' | 'ANY' | 'CELLULAR' | 'WIFI';
|
||||||
|
requiresCharging?: boolean;
|
||||||
|
requiresDeviceIdle?: boolean;
|
||||||
|
requiresBatteryNotLow?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认后台任务配置
|
||||||
|
*/
|
||||||
|
export const DEFAULT_BACKGROUND_TASK_CONFIG: BackgroundTaskConfig = {
|
||||||
|
minimumFetchInterval: 15000, // 15分钟
|
||||||
|
stopOnTerminate: false,
|
||||||
|
startOnBoot: true,
|
||||||
|
enableHeadless: true,
|
||||||
|
requiredNetworkType: 'ANY',
|
||||||
|
requiresCharging: false,
|
||||||
|
requiresDeviceIdle: false,
|
||||||
|
requiresBatteryNotLow: false,
|
||||||
|
};
|
||||||
@@ -460,13 +460,13 @@ export async function fetchTodayHRV(): Promise<number | null> {
|
|||||||
// 获取最近几小时内的实时HRV数据
|
// 获取最近几小时内的实时HRV数据
|
||||||
export async function fetchRecentHRV(hoursBack: number = 2): Promise<number | null> {
|
export async function fetchRecentHRV(hoursBack: number = 2): Promise<number | null> {
|
||||||
console.log(`开始获取最近${hoursBack}小时内的HRV数据...`);
|
console.log(`开始获取最近${hoursBack}小时内的HRV数据...`);
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const options = {
|
const options = {
|
||||||
startDate: dayjs(now).subtract(hoursBack, 'hour').toDate().toISOString(),
|
startDate: dayjs(now).subtract(hoursBack, 'hour').toDate().toISOString(),
|
||||||
endDate: now.toISOString()
|
endDate: now.toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
return fetchHeartRateVariability(options);
|
return fetchHeartRateVariability(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,18 +543,18 @@ export async function saveWaterIntakeToHealthKit(amount: number, recordedAt?: st
|
|||||||
endDate: recordedAt ? new Date(recordedAt).toISOString() : new Date().toISOString(),
|
endDate: recordedAt ? new Date(recordedAt).toISOString() : new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
AppleHealthKit.saveWater(waterOptions, (error: Object, result: boolean) => {
|
AppleHealthKit.saveWater(waterOptions, (error: Object, result) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('添加饮水记录到 HealthKit 失败:', error);
|
console.error('添加饮水记录到 HealthKit 失败:', error);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('成功添加饮水记录到 HealthKit:', {
|
console.log('成功添加饮水记录到 HealthKit:', {
|
||||||
originalAmount: amount,
|
originalAmount: amount,
|
||||||
convertedAmount: amount / 1000,
|
convertedAmount: amount / 1000,
|
||||||
recordedAt,
|
recordedAt,
|
||||||
result
|
result
|
||||||
});
|
});
|
||||||
resolve(true);
|
resolve(true);
|
||||||
});
|
});
|
||||||
@@ -570,7 +570,7 @@ export async function getWaterIntakeFromHealthKit(options: HealthDataOptions): P
|
|||||||
resolve([]);
|
resolve([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('从 HealthKit 获取饮水记录:', results);
|
console.log('从 HealthKit 获取饮水记录:', results);
|
||||||
resolve(results || []);
|
resolve(results || []);
|
||||||
});
|
});
|
||||||
@@ -584,7 +584,31 @@ export async function deleteWaterIntakeFromHealthKit(recordId: string, recordedA
|
|||||||
// 这是一个占位函数,实际实现可能需要更复杂的逻辑
|
// 这是一个占位函数,实际实现可能需要更复杂的逻辑
|
||||||
console.log('注意: HealthKit 通常不支持直接删除单条饮水记录');
|
console.log('注意: HealthKit 通常不支持直接删除单条饮水记录');
|
||||||
console.log('记录信息:', { recordId, recordedAt });
|
console.log('记录信息:', { recordId, recordedAt });
|
||||||
|
|
||||||
// 返回 true 表示"成功"(但实际上可能没有真正删除)
|
// 返回 true 表示"成功"(但实际上可能没有真正删除)
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取当前小时的站立状态
|
||||||
|
export async function getCurrentHourStandStatus(): Promise<{ hasStood: boolean; standHours: number; standHoursGoal: number }> {
|
||||||
|
try {
|
||||||
|
const currentHour = new Date().getHours();
|
||||||
|
console.log(`检查当前小时 ${currentHour} 的站立状态...`);
|
||||||
|
|
||||||
|
// 获取今日健康数据
|
||||||
|
const todayHealthData = await fetchTodayHealthData();
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasStood: todayHealthData.standHours > currentHour - 1, // 如果站立小时数大于当前小时-1,说明当前小时已站立
|
||||||
|
standHours: todayHealthData.standHours,
|
||||||
|
standHoursGoal: todayHealthData.standHoursGoal
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取当前小时站立状态失败:', error);
|
||||||
|
return {
|
||||||
|
hasStood: true, // 默认认为已站立,避免过度提醒
|
||||||
|
standHours: 0,
|
||||||
|
standHoursGoal: 12
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
import { NotificationData, notificationService } from '../services/notifications';
|
import { NotificationData, notificationService } from '../services/notifications';
|
||||||
|
import { getNotificationEnabled } from './userPreferences';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建 coach 页面的深度链接
|
* 构建 coach 页面的深度链接
|
||||||
@@ -917,6 +918,102 @@ export class GeneralNotificationHelpers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 站立提醒通知助手
|
||||||
|
*/
|
||||||
|
export class StandReminderHelpers {
|
||||||
|
/**
|
||||||
|
* 检查站立状态并发送提醒通知
|
||||||
|
*/
|
||||||
|
static async checkStandStatusAndNotify(userName: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
console.log('检查站立状态并发送提醒通知...');
|
||||||
|
|
||||||
|
// 动态导入健康工具,避免循环依赖
|
||||||
|
const { getCurrentHourStandStatus } = await import('@/utils/health');
|
||||||
|
|
||||||
|
// 获取当前小时站立状态
|
||||||
|
const standStatus = await getCurrentHourStandStatus();
|
||||||
|
|
||||||
|
console.log('当前站立状态:', standStatus);
|
||||||
|
|
||||||
|
// 如果已经站立过,不需要提醒
|
||||||
|
if (standStatus.hasStood) {
|
||||||
|
console.log('用户当前小时已经站立,无需提醒');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查时间范围(工作时间内提醒,避免深夜或清晨打扰)
|
||||||
|
const currentHour = new Date().getHours();
|
||||||
|
if (currentHour < 9 || currentHour >= 21) {
|
||||||
|
console.log(`当前时间${currentHour}点,不在站立提醒时间范围内`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否启用了通知
|
||||||
|
if (!(await getNotificationEnabled())) {
|
||||||
|
console.log('用户未启用通知功能,跳过站立提醒');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成提醒消息
|
||||||
|
const reminderMessage = this.generateStandReminderMessage(userName, standStatus.standHours, standStatus.standHoursGoal);
|
||||||
|
|
||||||
|
// 发送站立提醒通知
|
||||||
|
await notificationService.sendImmediateNotification({
|
||||||
|
title: '站立提醒',
|
||||||
|
body: reminderMessage,
|
||||||
|
data: {
|
||||||
|
type: 'stand_reminder',
|
||||||
|
currentStandHours: standStatus.standHours,
|
||||||
|
standHoursGoal: standStatus.standHoursGoal,
|
||||||
|
timestamp: Date.now()
|
||||||
|
},
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('站立提醒通知发送成功');
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查站立状态并发送提醒失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成站立提醒消息
|
||||||
|
*/
|
||||||
|
private static generateStandReminderMessage(userName: string, currentStandHours: number, goalHours: number): string {
|
||||||
|
const currentHour = new Date().getHours();
|
||||||
|
const progress = Math.round((currentStandHours / goalHours) * 100);
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
`${userName},该站起来活动一下了!当前已完成${progress}%的站立目标`,
|
||||||
|
`${userName},久坐伤身,起来走走吧~已站立${currentStandHours}/${goalHours}小时`,
|
||||||
|
`${userName},站立一会儿对健康有益,目前进度${currentStandHours}/${goalHours}小时`,
|
||||||
|
`${userName},记得起身活动哦!今日站立进度${progress}%`
|
||||||
|
];
|
||||||
|
|
||||||
|
// 根据时间选择不同的消息
|
||||||
|
const messageIndex = currentHour % messages.length;
|
||||||
|
return messages[messageIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消所有站立提醒通知
|
||||||
|
*/
|
||||||
|
static async cancelStandReminders(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await GeneralNotificationHelpers.cancelNotificationsByType('stand_reminder');
|
||||||
|
console.log('已取消所有站立提醒通知');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('取消站立提醒通知失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知模板
|
* 通知模板
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user