feat: 集成后台任务管理功能及相关组件

- 新增后台任务管理器,支持任务的注册、执行和状态监控
- 实现自定义Hook,简化后台任务的使用和管理
- 添加示例任务,包括数据同步、健康数据更新和通知检查等
- 更新文档,详细描述后台任务系统的实现和使用方法
- 优化相关组件,确保用户体验和界面一致性
This commit is contained in:
2025-08-24 09:46:11 +08:00
parent 4f2bd76b8f
commit 23aa15f76e
17 changed files with 1289 additions and 119 deletions

View File

@@ -17,7 +17,9 @@
"NSCameraUsageDescription": "应用需要使用相机以拍摄您的体态照片用于AI测评。",
"NSPhotoLibraryUsageDescription": "应用需要访问相册以选择您的体态照片用于AI测评。",
"NSPhotoLibraryAddUsageDescription": "应用需要写入相册以保存拍摄的体态照片(可选)。",
"UIBackgroundModes": ["remote-notification"]
"UIBackgroundModes": [
"remote-notification"
]
}
},
"android": {
@@ -62,12 +64,15 @@
{
"icon": "./assets/images/Sealife.jpeg",
"color": "#ffffff",
"sounds": ["./assets/sounds/notification.wav"]
"sounds": [
"./assets/sounds/notification.wav"
]
}
]
],
"expo-background-task"
],
"experiments": {
"typedRoutes": true
}
}
}
}

View File

@@ -13,6 +13,7 @@ import { Colors } from '@/constants/Colors';
import { getTabBarBottomPadding } from '@/constants/TabBar';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useBackgroundTasks } from '@/hooks/useBackgroundTasks';
import { useColorScheme } from '@/hooks/useColorScheme';
import { calculateNutritionSummary, getDietRecords, NutritionSummary } from '@/services/dietRecords';
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
@@ -131,7 +132,7 @@ export default function ExploreScreen() {
// 营养数据状态
const [nutritionSummary, setNutritionSummary] = useState<NutritionSummary | null>(null);
const [isNutritionLoading, setIsNutritionLoading] = useState(false);
const { registerTask } = useBackgroundTasks();
// 心情相关状态
const dispatch = useAppDispatch();
const [isMoodLoading, setIsMoodLoading] = useState(false);
@@ -299,6 +300,21 @@ export default function ExploreScreen() {
}, [selectedIndex])
);
useEffect(() => {
// 注册任务
registerTask({
id: 'health-data-task',
name: 'health-data-task',
handler: async () => {
try {
await loadHealthData();
} catch (error) {
console.error('健康数据任务执行失败:', error);
}
},
});
}, []);
// 日期点击时,加载对应日期数据
const onSelectDate = (index: number, date: Date) => {
setSelectedIndex(index);

View File

@@ -2,14 +2,14 @@ import { DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import 'react-native-reanimated';
import 'react-native-gesture-handler';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import 'react-native-reanimated';
import PrivacyConsentModal from '@/components/PrivacyConsentModal';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { clearAiCoachSessionCache } from '@/services/aiCoachSession';
import { backgroundTaskManager } from '@/services/backgroundTaskManager';
import { store } from '@/store';
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
import React from 'react';
@@ -31,7 +31,18 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
setUserDataLoaded(true);
};
const initializeBackgroundTasks = async () => {
try {
await backgroundTaskManager.initialize();
console.log('后台任务管理器初始化成功');
} catch (error) {
console.error('后台任务管理器初始化失败:', error);
}
};
loadUserData();
initializeBackgroundTasks();
// 冷启动时清空 AI 教练会话缓存
clearAiCoachSessionCache();
}, [dispatch]);
@@ -96,6 +107,7 @@ export default function RootLayout() {
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
<Stack.Screen name="nutrition/records" options={{ headerShown: false }} />
<Stack.Screen name="background-tasks-test" options={{ headerShown: true, title: '后台任务测试' }} />
<Stack.Screen name="+not-found" />
</Stack>
<StatusBar style="dark" />

View File

@@ -8,7 +8,7 @@ import { selectLatestMoodRecordByDate } from '@/store/moodSlice';
import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import { router, useFocusEffect, useLocalSearchParams } from 'expo-router';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
Dimensions,
SafeAreaView,
@@ -56,6 +56,14 @@ export default function MoodCalendarScreen() {
const dispatch = useAppDispatch();
const { fetchMoodRecords, fetchMoodHistoryRecords } = useMoodData();
// 使用 useRef 来存储函数引用,避免依赖循环
const fetchMoodRecordsRef = useRef(fetchMoodRecords);
const fetchMoodHistoryRecordsRef = useRef(fetchMoodHistoryRecords);
// 更新 ref 值
fetchMoodRecordsRef.current = fetchMoodRecords;
fetchMoodHistoryRecordsRef.current = fetchMoodHistoryRecords;
const { selectedDate } = params;
const initialDate = selectedDate ? dayjs(selectedDate as string).toDate() : new Date();
@@ -66,9 +74,9 @@ export default function MoodCalendarScreen() {
const moodRecords = useAppSelector(state => state.mood.moodRecords);
// 获取选中日期的数据
const selectedDateString = selectedDay ? dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD') : null;
const selectedDateMood = useAppSelector(state => {
if (!selectedDateString) return null;
if (!selectedDay) return null;
const selectedDateString = dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD');
return selectLatestMoodRecordByDate(selectedDateString)(state);
});
@@ -80,29 +88,24 @@ export default function MoodCalendarScreen() {
const { calendar, today, month, year } = generateCalendarData(currentMonth);
// 加载整个月份的心情数据
const loadMonthMoodData = async (targetMonth: Date) => {
const loadMonthMoodData = useCallback(async (targetMonth: Date) => {
try {
const startDate = dayjs(targetMonth).startOf('month').format('YYYY-MM-DD');
const endDate = dayjs(targetMonth).endOf('month').format('YYYY-MM-DD');
const historyData = await fetchMoodHistoryRecords({ startDate, endDate });
// 历史记录已经通过 fetchMoodHistoryRecords 自动存储到 Redux store 中
// 不需要额外的处理
await fetchMoodHistoryRecordsRef.current({ startDate, endDate });
} catch (error) {
console.error('加载月份心情数据失败:', error);
}
};
}, []);
// 加载选中日期的心情记录
const loadDailyMoodCheckins = async (dateString: string) => {
const loadDailyMoodCheckins = useCallback(async (dateString: string) => {
try {
await fetchMoodRecords(dateString);
// 不需要手动设置 selectedDateMood因为它现在从 Redux store 中获取
await fetchMoodRecordsRef.current(dateString);
} catch (error) {
console.error('加载心情记录失败:', error);
}
};
}, []);
// 初始化选中日期
useEffect(() => {
@@ -112,15 +115,16 @@ export default function MoodCalendarScreen() {
setSelectedDay(date.date());
const dateString = date.format('YYYY-MM-DD');
loadDailyMoodCheckins(dateString);
loadMonthMoodData(date.toDate());
} else {
const today = new Date();
setCurrentMonth(today);
setSelectedDay(today.getDate());
const dateString = dayjs().format('YYYY-MM-DD');
loadDailyMoodCheckins(dateString);
loadMonthMoodData(today);
}
loadMonthMoodData(currentMonth);
}, [selectedDate]);
}, [selectedDate, loadDailyMoodCheckins, loadMonthMoodData]);
// 监听页面焦点变化,当从编辑页面返回时刷新数据
useFocusEffect(
@@ -129,14 +133,14 @@ export default function MoodCalendarScreen() {
const refreshData = async () => {
if (selectedDay) {
const selectedDateString = dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD');
await fetchMoodRecords(selectedDateString);
await fetchMoodRecordsRef.current(selectedDateString);
}
const startDate = dayjs(currentMonth).startOf('month').format('YYYY-MM-DD');
const endDate = dayjs(currentMonth).endOf('month').format('YYYY-MM-DD');
await fetchMoodHistoryRecords({ startDate, endDate });
await fetchMoodHistoryRecordsRef.current({ startDate, endDate });
};
refreshData();
}, [currentMonth, selectedDay, fetchMoodRecords, fetchMoodHistoryRecords])
}, [currentMonth, selectedDay])
);
// 月份切换函数

View File

@@ -1,28 +1,29 @@
import MoodIntensitySlider from '@/components/MoodIntensitySlider';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { getMoodOptions, MoodType } from '@/services/moodCheckins';
import {
createMoodRecord,
deleteMoodRecord,
fetchDailyMoodCheckins,
selectMoodRecordsByDate,
updateMoodRecord
createMoodRecord,
deleteMoodRecord,
fetchDailyMoodCheckins,
selectMoodRecordsByDate,
updateMoodRecord
} from '@/store/moodSlice';
import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import { router, useLocalSearchParams } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
Alert,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
Alert,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
export default function MoodEditScreen() {
@@ -133,28 +134,8 @@ export default function MoodEditScreen() {
);
};
const renderIntensitySlider = () => {
return (
<View style={styles.intensityContainer}>
<Text style={styles.intensityLabel}>: {intensity}</Text>
<View style={styles.intensitySlider}>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((level) => (
<TouchableOpacity
key={level}
style={[
styles.intensityDot,
intensity >= level && styles.intensityDotActive
]}
onPress={() => setIntensity(level)}
/>
))}
</View>
<View style={styles.intensityLabels}>
<Text style={styles.intensityLabelText}></Text>
<Text style={styles.intensityLabelText}></Text>
</View>
</View>
);
const handleIntensityChange = (value: number) => {
setIntensity(value);
};
// 使用统一的渐变背景色
@@ -210,12 +191,17 @@ export default function MoodEditScreen() {
</View>
{/* 心情强度选择 */}
{selectedMood && (
<View style={styles.intensitySection}>
<Text style={styles.sectionTitle}></Text>
{renderIntensitySlider()}
</View>
)}
<View style={styles.intensitySection}>
<Text style={styles.sectionTitle}></Text>
<MoodIntensitySlider
value={intensity}
onValueChange={handleIntensityChange}
min={1}
max={10}
width={320}
/>
</View>
{/* 心情描述 */}
{selectedMood && (
@@ -383,50 +369,7 @@ const styles = StyleSheet.create({
shadowRadius: 12,
elevation: 6,
},
intensityContainer: {
alignItems: 'center',
},
intensityLabel: {
fontSize: 18,
fontWeight: '700',
color: '#192126',
marginBottom: 16,
},
intensitySlider: {
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
marginBottom: 12,
paddingHorizontal: 10,
},
intensityDot: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: 'rgba(122,90,248,0.1)',
borderWidth: 2,
borderColor: 'rgba(122,90,248,0.2)',
},
intensityDotActive: {
backgroundColor: '#7a5af8',
borderColor: '#7a5af8',
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 2,
},
intensityLabels: {
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
paddingHorizontal: 10,
},
intensityLabelText: {
fontSize: 13,
color: '#5d6676',
fontWeight: '500',
},
descriptionSection: {
backgroundColor: 'rgba(255,255,255,0.95)',
margin: 16,

View File

@@ -256,18 +256,15 @@ const styles = StyleSheet.create({
// 日期选择器样式
dateSelector: {
paddingHorizontal: 20,
paddingVertical: 16,
},
taskListContainer: {
flex: 1,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
overflow: 'hidden',
},
taskList: {
paddingHorizontal: 20,
paddingTop: 20,
paddingBottom: TAB_BAR_HEIGHT + TAB_BAR_BOTTOM_OFFSET + 20,
},
emptyState: {

View File

@@ -216,8 +216,8 @@ const styles = StyleSheet.create({
elevation: 2,
},
goalIcon: {
width: 40,
height: 40,
width: 32,
height: 32,
borderRadius: 20,
backgroundColor: '#F3F4F6',
alignItems: 'center',
@@ -243,7 +243,7 @@ const styles = StyleSheet.create({
marginRight: 12,
},
goalTitle: {
fontSize: 16,
fontSize: 14,
fontWeight: '600',
color: '#1F2937',
marginBottom: 8,

View File

@@ -0,0 +1,298 @@
import * as Haptics from 'expo-haptics';
import { LinearGradient } from 'expo-linear-gradient';
import React from 'react';
import {
StyleSheet,
Text,
View,
} from 'react-native';
import {
PanGestureHandler,
PanGestureHandlerGestureEvent,
} from 'react-native-gesture-handler';
import Animated, {
interpolate,
interpolateColor,
runOnJS,
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
interface MoodIntensitySliderProps {
value: number;
onValueChange: (value: number) => void;
min?: number;
max?: number;
step?: number;
width?: number;
height?: number;
}
export default function MoodIntensitySlider({
value,
onValueChange,
min = 1,
max = 10,
step = 1,
width = 320,
height = 16, // 更粗的进度条
}: MoodIntensitySliderProps) {
const translateX = useSharedValue(0);
const sliderWidth = width - 40; // 减去thumb的宽度
const thumbSize = 36; // 更大的thumb
// 计算初始位置
React.useEffect(() => {
const initialPosition = ((value - min) / (max - min)) * sliderWidth;
translateX.value = withSpring(initialPosition);
}, [value, min, max, sliderWidth, translateX]);
const triggerHaptics = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
};
const gestureHandler = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
{ startX: number; lastValue: number }
>({
onStart: (_, context) => {
context.startX = translateX.value;
context.lastValue = value;
runOnJS(triggerHaptics)();
},
onActive: (event, context) => {
const newX = context.startX + event.translationX;
const clampedX = Math.max(0, Math.min(sliderWidth, newX));
translateX.value = clampedX;
// 计算当前值
const currentValue = Math.round((clampedX / sliderWidth) * (max - min) + min);
// 当值改变时触发震动和回调
if (currentValue !== context.lastValue) {
context.lastValue = currentValue;
runOnJS(triggerHaptics)();
runOnJS(onValueChange)(currentValue);
}
},
onEnd: () => {
// 计算最终值并吸附到最近的步长
const currentValue = Math.round((translateX.value / sliderWidth) * (max - min) + min);
const snapPosition = ((currentValue - min) / (max - min)) * sliderWidth;
translateX.value = withSpring(snapPosition);
runOnJS(triggerHaptics)();
runOnJS(onValueChange)(currentValue);
},
});
const thumbStyle = useAnimatedStyle(() => {
const scale = interpolate(
translateX.value,
[0, sliderWidth],
[1, 1.05]
);
return {
transform: [
{ translateX: translateX.value },
{ scale: withSpring(scale) }
],
};
});
const progressStyle = useAnimatedStyle(() => {
const progressWidth = translateX.value + thumbSize / 2;
return {
width: progressWidth,
};
});
// 动态颜色配置 - 根据进度变化颜色
const getProgressColors = (progress: number) => {
if (progress <= 0.25) {
return ['#22c55e', '#84cc16'] as const; // 绿色到浅绿色
} else if (progress <= 0.5) {
return ['#84cc16', '#eab308'] as const; // 浅绿色到黄色
} else if (progress <= 0.75) {
return ['#eab308', '#f97316'] as const; // 黄色到橙色
} else {
return ['#f97316', '#ef4444'] as const; // 橙色到红色
}
};
const progressColorsStyle = useAnimatedStyle(() => {
const progress = translateX.value / sliderWidth;
return {
backgroundColor: interpolateColor(
progress,
[0, 0.25, 0.5, 0.75, 1],
['#22c55e', '#84cc16', '#eab308', '#f97316', '#ef4444']
),
};
});
return (
<View style={styles.container}>
<View style={[styles.sliderContainer, { width: width }]}>
{/* 背景轨道 - 更粗的灰色轨道 */}
<View style={[styles.track, { height }]}>
<LinearGradient
colors={['#f3f4f6', '#e5e7eb']}
style={[styles.trackGradient, { height }]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
/>
</View>
{/* 进度条 - 动态颜色 */}
<Animated.View style={[styles.progress, { height }, progressStyle, progressColorsStyle]}>
<LinearGradient
colors={getProgressColors(translateX.value / sliderWidth)}
style={[styles.progressGradient, { height }]}
start={{ x: 1, y: 0 }}
end={{ x: 0, y: 0 }}
/>
</Animated.View>
{/* 可拖拽的thumb */}
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={[styles.thumb, { width: thumbSize, height: thumbSize }, thumbStyle]}>
{/* <LinearGradient
colors={['#ffffff', '#f8fafc']}
style={styles.thumbGradient}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/> */}
<View style={styles.thumbInner} />
</Animated.View>
</PanGestureHandler>
</View>
{/* 标签 */}
<View style={[styles.labelsContainer, { width: width }]}>
<Text style={styles.labelText}></Text>
<Text style={styles.labelText}></Text>
</View>
{/* 刻度 */}
<View style={[styles.scaleContainer, { width: width }]}>
{Array.from({ length: max - min + 1 }, (_, i) => i + min).map((num) => (
<View key={num} style={styles.scaleItem}>
<View style={[styles.scaleMark, value === num && styles.scaleMarkActive]} />
</View>
))}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
paddingVertical: 16,
},
sliderContainer: {
height: 50,
justifyContent: 'center',
position: 'relative',
paddingHorizontal: 20,
},
track: {
position: 'absolute',
left: 20,
right: 20,
borderRadius: 8,
overflow: 'hidden',
},
trackGradient: {
flex: 1,
borderRadius: 8,
},
progress: {
position: 'absolute',
left: 20,
borderRadius: 8,
overflow: 'hidden',
},
progressGradient: {
flex: 1,
borderRadius: 8,
},
thumb: {
position: 'absolute',
left: 22,
elevation: 6,
overflow: 'hidden',
},
thumbGradient: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
thumbInner: {
width: 16,
height: 32,
borderRadius: 8,
backgroundColor: '#ffffff',
shadowColor: '#ffffff',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.3,
shadowRadius: 2,
elevation: 2,
},
valueContainer: {
marginTop: 20,
marginBottom: 12,
paddingHorizontal: 16,
paddingVertical: 10,
backgroundColor: '#7a5af8',
borderRadius: 16,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.3,
shadowRadius: 6,
elevation: 6,
},
valueText: {
fontSize: 20,
fontWeight: '800',
color: '#ffffff',
textAlign: 'center',
minWidth: 28,
},
labelsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 8,
paddingHorizontal: 20,
},
labelText: {
fontSize: 14,
color: '#5d6676',
fontWeight: '600',
},
scaleContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 16,
paddingHorizontal: 20,
},
scaleItem: {
alignItems: 'center',
flex: 1,
},
scaleMark: {
width: 2,
height: 10,
backgroundColor: '#e5e7eb',
borderRadius: 1,
},
scaleMarkActive: {
backgroundColor: '#7a5af8',
width: 3,
height: 12,
},
});

View File

@@ -0,0 +1,284 @@
# 后台任务系统实现文档
## 概述
本项目已成功集成iOS后台任务支持使用Expo官方的 `expo-task-manager``expo-background-task` 库。该系统提供了完整的后台任务管理功能,支持任务注册、执行、状态监控等。
## 技术栈
- **expo-task-manager**: Expo官方后台任务管理库
- **expo-background-task**: Expo官方后台任务库
- **React Native**: 跨平台移动应用框架
- **TypeScript**: 类型安全的JavaScript超集
## 文件结构
```
services/
├── backgroundTaskManager.ts # 后台任务管理器核心逻辑
├── backgroundTasks.ts # 示例任务定义
hooks/
├── useBackgroundTasks.ts # 后台任务自定义Hook
components/
├── BackgroundTaskTest.tsx # 后台任务测试组件
```
## 核心功能
### 1. 后台任务管理器 (services/backgroundTaskManager.ts)
#### 主要特性
- **单例模式**: 确保全局只有一个任务管理器实例
- **任务注册**: 支持注册自定义后台任务
- **状态管理**: 完整的任务状态跟踪和持久化
- **错误处理**: 完善的错误处理和日志记录
- **后台获取**: 自动注册后台获取任务
#### 核心方法
```typescript
// 初始化后台任务管理器
await backgroundTaskManager.initialize();
// 注册自定义任务
await backgroundTaskManager.registerTask({
id: 'my-task',
name: '我的任务',
handler: async (data) => {
// 您的任务逻辑
console.log('执行任务:', data);
},
options: {
minimumInterval: 300, // 5分钟最小间隔
stopOnTerminate: false,
startOnBoot: true,
}
});
// 手动执行任务
await backgroundTaskManager.executeTask('my-task', { customData: 'value' });
// 执行所有任务
const results = await backgroundTaskManager.executeAllTasks();
// 获取任务状态
const status = backgroundTaskManager.getTaskStatus('my-task');
```
### 2. 自定义Hook (hooks/useBackgroundTasks.ts)
#### 主要特性
- **状态管理**: 管理任务状态和初始化状态
- **自动初始化**: 组件挂载时自动初始化任务管理器
- **便捷接口**: 提供简化的任务操作方法
- **实时更新**: 任务状态实时更新
#### 使用示例
```typescript
const {
isInitialized,
taskStatuses,
registeredTasks,
registerTask,
executeTask,
executeAllTasks,
} = useBackgroundTasks();
// 注册任务
await registerTask({
id: 'data-sync',
name: '数据同步',
handler: async () => {
// 数据同步逻辑
}
});
// 执行任务
await executeTask('data-sync');
```
### 3. 示例任务 (services/backgroundTasks.ts)
#### 预定义任务类型
- **数据同步任务**: 同步用户数据、运动记录等
- **健康数据更新任务**: 更新步数、心率等健康数据
- **通知检查任务**: 检查是否需要发送通知
- **缓存清理任务**: 清理过期缓存文件
- **用户行为分析任务**: 分析用户使用模式
#### 创建自定义任务
```typescript
import { createCustomTask } from '@/services/backgroundTasks';
const myTask = createCustomTask(
'my-custom-task',
'我的自定义任务',
async (data) => {
// 您的任务逻辑
console.log('执行自定义任务:', data);
},
{
minimumInterval: 120, // 2分钟
stopOnTerminate: false,
startOnBoot: true,
}
);
```
## 使用指南
### 1. 基本使用
```typescript
import { useBackgroundTasks } from '@/hooks/useBackgroundTasks';
import { createCustomTask } from '@/services/backgroundTasks';
const MyComponent = () => {
const { registerTask, executeTask } = useBackgroundTasks();
const handleCreateTask = async () => {
const task = createCustomTask(
'my-task',
'我的任务',
async (data) => {
// 实现您的后台任务逻辑
console.log('后台任务执行中...');
// 例如:数据同步
await syncUserData();
// 例如:健康数据更新
await updateHealthData();
// 例如:发送通知
await checkAndSendNotifications();
}
);
await registerTask(task);
};
const handleExecuteTask = async () => {
await executeTask('my-task', { customData: 'value' });
};
return (
<View>
<Button title="创建任务" onPress={handleCreateTask} />
<Button title="执行任务" onPress={handleExecuteTask} />
</View>
);
};
```
### 2. 任务状态监控
```typescript
const { taskStatuses, getTaskStatus } = useBackgroundTasks();
// 获取特定任务状态
const taskStatus = getTaskStatus('my-task');
console.log('任务状态:', {
isRegistered: taskStatus?.isRegistered,
executionCount: taskStatus?.executionCount,
lastExecution: taskStatus?.lastExecution,
lastError: taskStatus?.lastError,
});
```
### 3. 批量操作
```typescript
const { executeAllTasks, cleanupTaskStatuses } = useBackgroundTasks();
// 执行所有任务
const results = await executeAllTasks();
console.log('执行结果:', results);
// 清理过期任务状态
await cleanupTaskStatuses();
```
## 配置说明
### iOS配置
`app.json` 中已配置后台模式:
```json
{
"expo": {
"ios": {
"infoPlist": {
"UIBackgroundModes": ["remote-notification"]
}
}
}
}
```
### 后台获取配置
系统自动配置后台获取任务,支持:
- 最小间隔时间设置
- 应用终止时继续运行
- 设备重启时自动启动
## 最佳实践
### 1. 任务设计原则
- **轻量级**: 后台任务应该快速执行,避免长时间运行
- **幂等性**: 任务应该可以重复执行而不产生副作用
- **错误处理**: 完善的错误处理和重试机制
- **资源管理**: 合理管理内存和网络资源
### 2. 性能优化
- **最小间隔**: 根据任务重要性设置合适的最小间隔
- **批量处理**: 将多个小任务合并为一个大任务
- **缓存策略**: 合理使用缓存减少重复计算
### 3. 用户体验
- **静默执行**: 后台任务应该静默执行,不打扰用户
- **状态反馈**: 通过UI显示任务执行状态
- **错误提示**: 在任务失败时提供友好的错误提示
## 测试
使用 `BackgroundTaskTest` 组件进行功能测试:
```typescript
import { BackgroundTaskTest } from '@/components/BackgroundTaskTest';
// 在您的页面中使用
<BackgroundTaskTest />
```
该组件提供:
- 任务注册和取消注册测试
- 任务执行测试
- 状态监控测试
- 后台获取状态测试
## 注意事项
1. **iOS限制**: iOS对后台任务有严格限制系统会根据电池状态和用户使用模式调整执行频率
2. **权限要求**: 某些后台任务可能需要特殊权限
3. **调试模式**: 在开发模式下,后台任务行为可能与生产环境不同
4. **网络状态**: 后台任务执行时需要考虑网络状态变化
## 故障排除
### 常见问题
1. **任务不执行**: 检查iOS后台模式配置和任务注册状态
2. **执行频率低**: 系统会根据电池状态自动调整,这是正常行为
3. **任务被终止**: 检查任务执行时间,避免长时间运行
### 调试技巧
1. 使用 `console.log` 记录任务执行状态
2. 检查任务状态和错误信息
3. 使用Xcode查看后台任务日志
4. 测试不同的最小间隔设置

109
hooks/useBackgroundTasks.ts Normal file
View File

@@ -0,0 +1,109 @@
import { BackgroundTaskType as BackgroundTask, backgroundTaskManager, TaskStatusType as TaskStatus } from '@/services/backgroundTaskManager';
import { useCallback, useEffect, useState } from 'react';
export interface UseBackgroundTasksReturn {
// 状态
isInitialized: boolean;
taskStatuses: TaskStatus[];
registeredTasks: BackgroundTask[];
// 方法
registerTask: (task: BackgroundTask) => Promise<void>;
unregisterTask: (taskId: string) => Promise<void>;
executeTask: (taskId: string, data?: any) => Promise<void>;
executeAllTasks: () => Promise<{ [taskId: string]: 'success' | 'failed' }>;
getTaskStatus: (taskId: string) => TaskStatus | undefined;
cleanupTaskStatuses: () => Promise<void>;
// 后台任务状态
backgroundTaskStatus: string | null;
getBackgroundTaskStatus: () => Promise<void>;
}
export const useBackgroundTasks = (): UseBackgroundTasksReturn => {
const [isInitialized, setIsInitialized] = useState(false);
const [taskStatuses, setTaskStatuses] = useState<TaskStatus[]>([]);
const [registeredTasks, setRegisteredTasks] = useState<BackgroundTask[]>([]);
const [backgroundTaskStatus, setBackgroundTaskStatus] = useState<string | null>(null);
// 初始化
useEffect(() => {
const initialize = async () => {
try {
await backgroundTaskManager.initialize();
setIsInitialized(true);
refreshData();
} catch (error) {
console.error('初始化后台任务失败:', error);
}
};
initialize();
}, []);
// 刷新数据
const refreshData = useCallback(() => {
setTaskStatuses(backgroundTaskManager.getAllTaskStatuses());
setRegisteredTasks(backgroundTaskManager.getRegisteredTasks());
}, []);
// 注册任务
const registerTask = useCallback(async (task: BackgroundTask) => {
await backgroundTaskManager.registerTask(task);
refreshData();
}, [refreshData]);
// 取消注册任务
const unregisterTask = useCallback(async (taskId: string) => {
await backgroundTaskManager.unregisterTask(taskId);
refreshData();
}, [refreshData]);
// 执行任务
const executeTask = useCallback(async (taskId: string, data?: any) => {
await backgroundTaskManager.executeTask(taskId, data);
refreshData();
}, [refreshData]);
// 执行所有任务
const executeAllTasks = useCallback(async () => {
const results = await backgroundTaskManager.executeAllTasks();
refreshData();
return results;
}, [refreshData]);
// 获取任务状态
const getTaskStatus = useCallback((taskId: string) => {
return backgroundTaskManager.getTaskStatus(taskId);
}, []);
// 清理任务状态
const cleanupTaskStatuses = useCallback(async () => {
await backgroundTaskManager.cleanupTaskStatuses();
refreshData();
}, [refreshData]);
// 获取后台任务状态
const getBackgroundTaskStatus = useCallback(async () => {
try {
const status = await backgroundTaskManager.getBackgroundTaskStatus();
setBackgroundTaskStatus(status ? status.toString() : null);
} catch (error) {
console.error('获取后台任务状态失败:', error);
}
}, []);
return {
isInitialized,
taskStatuses,
registeredTasks,
registerTask,
unregisterTask,
executeTask,
executeAllTasks,
getTaskStatus,
cleanupTaskStatuses,
backgroundTaskStatus,
getBackgroundTaskStatus,
};
};

View File

@@ -40,6 +40,8 @@ PODS:
- ExpoModulesCore
- ExpoAsset (11.1.7):
- ExpoModulesCore
- ExpoBackgroundTask (0.2.8):
- ExpoModulesCore
- ExpoBlur (14.1.5):
- ExpoModulesCore
- ExpoFileSystem (18.1.11):
@@ -96,6 +98,9 @@ PODS:
- ExpoModulesCore
- ExpoWebBrowser (14.2.0):
- ExpoModulesCore
- EXTaskManager (13.1.6):
- ExpoModulesCore
- UMAppLoader
- fast_float (6.1.4)
- FBLazyVector (0.79.5)
- fmt (11.0.2)
@@ -1949,6 +1954,7 @@ PODS:
- SDWebImage/Core (~> 5.17)
- Sentry/HybridSDK (8.53.2)
- SocketRocket (0.7.1)
- UMAppLoader (5.1.3)
- Yoga (0.0.0)
DEPENDENCIES:
@@ -1961,6 +1967,7 @@ DEPENDENCIES:
- Expo (from `../node_modules/expo`)
- ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
- ExpoAsset (from `../node_modules/expo-asset/ios`)
- ExpoBackgroundTask (from `../node_modules/expo-background-task/ios`)
- ExpoBlur (from `../node_modules/expo-blur/ios`)
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
- ExpoFont (from `../node_modules/expo-font/ios`)
@@ -1976,6 +1983,7 @@ DEPENDENCIES:
- ExpoSymbols (from `../node_modules/expo-symbols/ios`)
- ExpoSystemUI (from `../node_modules/expo-system-ui/ios`)
- ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`)
- EXTaskManager (from `../node_modules/expo-task-manager/ios`)
- fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
@@ -2059,6 +2067,7 @@ DEPENDENCIES:
- RNScreens (from `../node_modules/react-native-screens`)
- "RNSentry (from `../node_modules/@sentry/react-native`)"
- RNSVG (from `../node_modules/react-native-svg`)
- UMAppLoader (from `../node_modules/unimodules-app-loader/ios`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
@@ -2097,6 +2106,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-apple-authentication/ios"
ExpoAsset:
:path: "../node_modules/expo-asset/ios"
ExpoBackgroundTask:
:path: "../node_modules/expo-background-task/ios"
ExpoBlur:
:path: "../node_modules/expo-blur/ios"
ExpoFileSystem:
@@ -2127,6 +2138,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-system-ui/ios"
ExpoWebBrowser:
:path: "../node_modules/expo-web-browser/ios"
EXTaskManager:
:path: "../node_modules/expo-task-manager/ios"
fast_float:
:podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec"
FBLazyVector:
@@ -2289,6 +2302,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@sentry/react-native"
RNSVG:
:path: "../node_modules/react-native-svg"
UMAppLoader:
:path: "../node_modules/unimodules-app-loader/ios"
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
@@ -2302,6 +2317,7 @@ SPEC CHECKSUMS:
Expo: 8685113c16058e8b3eb101dd52d6c8bca260bbea
ExpoAppleAuthentication: 8a661b6f4936affafd830f983ac22463c936dad5
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
ExpoBackgroundTask: 6c1990438e45b5c4bbbc7d75aa6b688d53602fe8
ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9
ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63
ExpoFont: cf508bc2e6b70871e05386d71cab927c8524cc8e
@@ -2317,6 +2333,7 @@ SPEC CHECKSUMS:
ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859
ExpoSystemUI: c2724f9d5af6b1bb74e013efadf9c6a8fae547a2
ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92
EXTaskManager: 280143f6d8e596f28739d74bf34910300dcbd4ea
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
@@ -2412,6 +2429,7 @@ SPEC CHECKSUMS:
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
UMAppLoader: 55159b69750129faa7a51c493cb8ea55a7b64eb9
Yoga: adb397651e1c00672c12e9495babca70777e411e
PODFILE CHECKSUM: 8d79b726cf7814a1ef2e250b7a9ef91c07c77936

View File

@@ -268,6 +268,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXTaskManager/ExpoTaskManager_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
@@ -290,6 +291,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoTaskManager_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",

View File

@@ -61,6 +61,10 @@
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<key>UILaunchStoryboardName</key>
<string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key>

33
package-lock.json generated
View File

@@ -21,6 +21,7 @@
"dayjs": "^1.11.13",
"expo": "~53.0.20",
"expo-apple-authentication": "6.4.2",
"expo-background-task": "~0.2.8",
"expo-blur": "~14.1.5",
"expo-constants": "~17.1.7",
"expo-font": "~13.3.2",
@@ -35,6 +36,7 @@
"expo-status-bar": "~2.2.3",
"expo-symbols": "~0.4.5",
"expo-system-ui": "~5.0.10",
"expo-task-manager": "^13.1.6",
"expo-web-browser": "~14.2.0",
"react": "19.0.0",
"react-dom": "19.0.0",
@@ -7030,6 +7032,18 @@
"react-native": "*"
}
},
"node_modules/expo-background-task": {
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/expo-background-task/-/expo-background-task-0.2.8.tgz",
"integrity": "sha512-dePyskpmyDZeOtbr9vWFh+Nrse0TvF6YitJqnKcd+3P7pDMiDr1V2aT6zHdNOc5iV9vPaDJoH/zdmlarp1uHMQ==",
"license": "MIT",
"dependencies": {
"expo-task-manager": "~13.1.6"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-blur": {
"version": "14.1.5",
"resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.1.5.tgz",
@@ -7320,6 +7334,19 @@
}
}
},
"node_modules/expo-task-manager": {
"version": "13.1.6",
"resolved": "https://registry.npmjs.org/expo-task-manager/-/expo-task-manager-13.1.6.tgz",
"integrity": "sha512-sYNAftpIeZ+j6ur17Jo0OpSTk9ks/MDvTbrNCimXMyjIt69XXYL/kAPYf76bWuxOuN8bcJ8Ef8YvihkwFG9hDA==",
"license": "MIT",
"dependencies": {
"unimodules-app-loader": "~5.1.3"
},
"peerDependencies": {
"expo": "*",
"react-native": "*"
}
},
"node_modules/expo-web-browser": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-14.2.0.tgz",
@@ -13759,6 +13786,12 @@
"node": ">=4"
}
},
"node_modules/unimodules-app-loader": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/unimodules-app-loader/-/unimodules-app-loader-5.1.3.tgz",
"integrity": "sha512-nPUkwfkpJWvdOQrVvyQSUol93/UdmsCVd9Hkx9RgAevmKSVYdZI+S87W73NGKl6QbwK9L1BDSY5OrQuo8Oq15g==",
"license": "MIT"
},
"node_modules/unique-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",

View File

@@ -24,6 +24,7 @@
"dayjs": "^1.11.13",
"expo": "~53.0.20",
"expo-apple-authentication": "6.4.2",
"expo-background-task": "~0.2.8",
"expo-blur": "~14.1.5",
"expo-constants": "~17.1.7",
"expo-font": "~13.3.2",
@@ -32,11 +33,13 @@
"expo-image-picker": "~16.1.4",
"expo-linear-gradient": "^14.1.5",
"expo-linking": "~7.1.7",
"expo-notifications": "~0.31.4",
"expo-router": "~5.1.4",
"expo-splash-screen": "~0.30.10",
"expo-status-bar": "~2.2.3",
"expo-symbols": "~0.4.5",
"expo-system-ui": "~5.0.10",
"expo-task-manager": "^13.1.6",
"expo-web-browser": "~14.2.0",
"react": "19.0.0",
"react-dom": "19.0.0",
@@ -59,8 +62,7 @@
"react-native-web": "~0.20.0",
"react-native-webview": "13.13.5",
"react-native-wheel-picker-expo": "^0.5.4",
"react-redux": "^9.2.0",
"expo-notifications": "~0.31.4"
"react-redux": "^9.2.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",

View File

@@ -0,0 +1,263 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as ExpoBackgroundTask from 'expo-background-task';
import * as TaskManager from 'expo-task-manager';
// 任务类型定义
export interface BackgroundTask {
id: string;
name: string;
handler: (data?: any) => Promise<void>;
options?: {
minimumInterval?: number; // 最小间隔时间(分钟)
stopOnTerminate?: boolean; // 应用终止时是否停止
startOnBoot?: boolean; // 设备重启时是否启动
};
}
// 任务状态
export interface TaskStatus {
id: string;
isRegistered: boolean;
lastExecution?: Date;
nextExecution?: Date;
executionCount: number;
lastError?: string;
}
// 后台任务管理器类
class BackgroundTaskManager {
private static instance: BackgroundTaskManager;
private tasks: Map<string, BackgroundTask> = new Map();
private taskStatuses: Map<string, TaskStatus> = new Map();
private isInitialized = false;
// 单例模式
public static getInstance(): BackgroundTaskManager {
if (!BackgroundTaskManager.instance) {
BackgroundTaskManager.instance = new BackgroundTaskManager();
}
return BackgroundTaskManager.instance;
}
// 初始化后台任务管理器
public async initialize(): Promise<void> {
if (this.isInitialized) {
return;
}
try {
this.isInitialized = true;
// 注册后台任务
await this.registerBackgroundTask();
// 加载已保存的任务状态
await this.loadTaskStatuses();
console.log('后台任务管理器初始化成功');
} catch (error) {
console.error('后台任务管理器初始化失败:', error);
throw error;
}
}
// 注册后台任务
private async registerBackgroundTask(): Promise<void> {
const BACKGROUND_TASK = 'background-task';
console.log('注册后台任务');
// 定义后台任务
TaskManager.defineTask(BACKGROUND_TASK, async () => {
try {
console.log('开始执行后台任务');
// 执行所有注册的任务
const results = await this.executeAllTasks();
console.log('后台任务执行完成:', results);
// 返回成功状态
return ExpoBackgroundTask.BackgroundTaskResult.Success;
} catch (error) {
console.error('后台任务执行失败:', error);
return ExpoBackgroundTask.BackgroundTaskResult.Failed;
}
});
// 注册后台任务
await ExpoBackgroundTask.registerTaskAsync(BACKGROUND_TASK, {
minimumInterval: 15, // 最小间隔60分钟
});
console.log('后台任务注册成功');
}
// 注册自定义任务
public async registerTask(task: BackgroundTask): Promise<void> {
try {
// 检查任务是否已存在
if (this.tasks.has(task.id)) {
console.warn(`任务 ${task.id} 已存在,将被覆盖`);
}
// 保存任务
this.tasks.set(task.id, task);
// 初始化任务状态
if (!this.taskStatuses.has(task.id)) {
this.taskStatuses.set(task.id, {
id: task.id,
isRegistered: true,
executionCount: 0,
});
}
// 保存任务状态
await this.saveTaskStatuses();
console.log(`任务 ${task.id} 注册成功`);
} catch (error) {
console.error(`注册任务 ${task.id} 失败:`, error);
throw error;
}
}
// 取消注册任务
public async unregisterTask(taskId: string): Promise<void> {
try {
// 移除任务
this.tasks.delete(taskId);
// 更新任务状态
const status = this.taskStatuses.get(taskId);
if (status) {
status.isRegistered = false;
await this.saveTaskStatuses();
}
console.log(`任务 ${taskId} 取消注册成功`);
} catch (error) {
console.error(`取消注册任务 ${taskId} 失败:`, error);
throw error;
}
}
// 手动执行任务
public async executeTask(taskId: string, data?: any): Promise<void> {
try {
const task = this.tasks.get(taskId);
if (!task) {
throw new Error(`任务 ${taskId} 不存在`);
}
console.log(`开始执行任务: ${taskId}`);
// 执行任务
await task.handler(data);
// 更新任务状态
const status = this.taskStatuses.get(taskId);
if (status) {
status.lastExecution = new Date();
status.executionCount += 1;
status.lastError = undefined;
await this.saveTaskStatuses();
}
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;
}
}
// 执行所有任务
public async executeAllTasks(): Promise<{ [taskId: string]: 'success' | 'failed' }> {
const results: { [taskId: string]: 'success' | 'failed' } = {};
for (const [taskId, task] of this.tasks) {
try {
await this.executeTask(taskId);
results[taskId] = 'success';
} catch (error) {
console.error(`执行任务 ${taskId} 失败:`, error);
results[taskId] = 'failed';
}
}
return results;
}
// 获取任务状态
public getTaskStatus(taskId: string): TaskStatus | undefined {
return this.taskStatuses.get(taskId);
}
// 获取所有任务状态
public getAllTaskStatuses(): TaskStatus[] {
return Array.from(this.taskStatuses.values());
}
// 获取已注册的任务列表
public getRegisteredTasks(): BackgroundTask[] {
return Array.from(this.tasks.values());
}
// 检查后台任务状态
public async getBackgroundTaskStatus(): Promise<ExpoBackgroundTask.BackgroundTaskStatus | null> {
return await ExpoBackgroundTask.getStatusAsync();
}
// 保存任务状态到本地存储
private async saveTaskStatuses(): Promise<void> {
try {
const statuses = Array.from(this.taskStatuses.values());
await AsyncStorage.setItem('@background_task_statuses', JSON.stringify(statuses));
} catch (error) {
console.error('保存任务状态失败:', error);
}
}
// 从本地存储加载任务状态
private async loadTaskStatuses(): Promise<void> {
try {
const statusesJson = await AsyncStorage.getItem('@background_task_statuses');
if (statusesJson) {
const statuses: TaskStatus[] = JSON.parse(statusesJson);
statuses.forEach(status => {
this.taskStatuses.set(status.id, status);
});
}
} catch (error) {
console.error('加载任务状态失败:', error);
}
}
// 清理过期的任务状态
public async cleanupTaskStatuses(): Promise<void> {
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();
}
}
// 导出单例实例
export const backgroundTaskManager = BackgroundTaskManager.getInstance();
// 导出类型
export type { BackgroundTask as BackgroundTaskType, TaskStatus as TaskStatusType };

180
services/backgroundTasks.ts Normal file
View File

@@ -0,0 +1,180 @@
import { BackgroundTaskType as BackgroundTask, backgroundTaskManager } from './backgroundTaskManager';
// 示例任务:数据同步任务
export const createDataSyncTask = (): BackgroundTask => ({
id: 'data-sync-task',
name: '数据同步任务',
handler: async (data?: any) => {
console.log('开始执行数据同步任务');
try {
// 这里实现您的数据同步逻辑
// 例如:同步用户数据、运动记录、目标进度等
// 模拟数据同步过程
await new Promise(resolve => setTimeout(resolve, 2000));
console.log('数据同步任务执行完成');
} catch (error) {
console.error('数据同步任务执行失败:', error);
throw error;
}
},
options: {
minimumInterval: 5, // 5分钟最小间隔
stopOnTerminate: false,
startOnBoot: true,
},
});
// 示例任务:健康数据更新任务
export const createHealthDataUpdateTask = (): BackgroundTask => ({
id: 'health-data-update-task',
name: '健康数据更新任务',
handler: async (data?: any) => {
console.log('开始执行健康数据更新任务');
try {
// 这里实现您的健康数据更新逻辑
// 例如:更新步数、心率、体重等健康数据
// 模拟健康数据更新过程
await new Promise(resolve => setTimeout(resolve, 1500));
console.log('健康数据更新任务执行完成');
} catch (error) {
console.error('健康数据更新任务执行失败:', error);
throw error;
}
},
options: {
minimumInterval: 10, // 10分钟最小间隔
stopOnTerminate: false,
startOnBoot: true,
},
});
// 示例任务:通知检查任务
export const createNotificationCheckTask = (): BackgroundTask => ({
id: 'notification-check-task',
name: '通知检查任务',
handler: async (data?: any) => {
console.log('开始执行通知检查任务');
try {
// 这里实现您的通知检查逻辑
// 例如:检查是否需要发送运动提醒、目标达成通知等
// 模拟通知检查过程
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('通知检查任务执行完成');
} catch (error) {
console.error('通知检查任务执行失败:', error);
throw error;
}
},
options: {
minimumInterval: 30, // 30分钟最小间隔
stopOnTerminate: false,
startOnBoot: true,
},
});
// 示例任务:缓存清理任务
export const createCacheCleanupTask = (): BackgroundTask => ({
id: 'cache-cleanup-task',
name: '缓存清理任务',
handler: async (data?: any) => {
console.log('开始执行缓存清理任务');
try {
// 这里实现您的缓存清理逻辑
// 例如:清理过期的图片缓存、临时文件等
// 模拟缓存清理过程
await new Promise(resolve => setTimeout(resolve, 3000));
console.log('缓存清理任务执行完成');
} catch (error) {
console.error('缓存清理任务执行失败:', error);
throw error;
}
},
options: {
minimumInterval: 86400, // 24小时最小间隔
stopOnTerminate: false,
startOnBoot: true,
},
});
// 示例任务:用户行为分析任务
export const createUserAnalyticsTask = (): BackgroundTask => ({
id: 'user-analytics-task',
name: '用户行为分析任务',
handler: async (data?: any) => {
console.log('开始执行用户行为分析任务');
try {
// 这里实现您的用户行为分析逻辑
// 例如:分析用户运动习惯、使用模式等
// 模拟用户行为分析过程
await new Promise(resolve => setTimeout(resolve, 2500));
console.log('用户行为分析任务执行完成');
} catch (error) {
console.error('用户行为分析任务执行失败:', error);
throw error;
}
},
options: {
minimumInterval: 60, // 1小时最小间隔
stopOnTerminate: false,
startOnBoot: true,
},
});
// 注册所有默认任务
export const registerDefaultTasks = async (): Promise<void> => {
try {
const tasks = [
createDataSyncTask(),
createHealthDataUpdateTask(),
createNotificationCheckTask(),
createCacheCleanupTask(),
createUserAnalyticsTask(),
];
for (const task of tasks) {
await backgroundTaskManager.registerTask(task);
}
console.log('所有默认任务注册完成');
} catch (error) {
console.error('注册默认任务失败:', error);
throw error;
}
};
// 创建自定义任务的工厂函数
export const createCustomTask = (
id: string,
name: string,
handler: (data?: any) => Promise<void>,
options?: {
minimumInterval?: number;
stopOnTerminate?: boolean;
startOnBoot?: boolean;
}
): BackgroundTask => ({
id,
name,
handler,
options: {
minimumInterval: 300, // 默认5分钟
stopOnTerminate: false,
startOnBoot: true,
...options,
},
});