Files
digital-pilates/services/backgroundTaskManager.ts
richarjiang 79ddd41a49 feat(workout): 新增锻炼历史记录功能与健康数据集成
- 新增锻炼历史页面,展示最近一个月的锻炼记录详情
- 添加锻炼汇总卡片组件,在统计页面显示当日锻炼数据
- 集成HealthKit锻炼数据获取,支持多种运动类型和详细信息
- 完善锻炼数据处理工具,包含统计分析和格式化功能
- 优化后台任务,随机选择挑战发送鼓励通知
- 版本升级至1.0.16
2025-10-02 22:13:59 +08:00

379 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { listChallenges } from '@/services/challengesApi';
import { store } from '@/store';
import { getWaterIntakeFromHealthKit } from '@/utils/health';
import AsyncStorage from '@/utils/kvStore';
import { log } from '@/utils/logger';
import { ChallengeNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
import { getWaterGoalFromStorage } from '@/utils/userPreferences';
import dayjs from 'dayjs';
import * as BackgroundTask from 'expo-background-task';
import * as TaskManager from 'expo-task-manager';
import { TaskManagerTaskBody } from 'expo-task-manager';
export const BACKGROUND_TASK_IDENTIFIER = 'com.anonymous.digitalpilates.task';
// 检查通知权限
async function 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 function executeWaterReminderTask(): Promise<void> {
try {
console.log('执行喝水提醒后台任务...');
// 获取当前状态
const state = store.getState();
const waterStats = state.water.todayStats;
const userProfile = state.user.profile;
// 优先使用 Redux 中的目标,若无则读取本地存储
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<void> {
try {
console.log('执行挑战鼓励提醒后台任务...');
const state = store.getState();
const normalizedUserName = state.user.profile?.name?.trim();
const userName = normalizedUserName && normalizedUserName.length > 0 ? normalizedUserName : '朋友';
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 sendTestNotification(): Promise<void> {
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 executeBackgroundTasks(): Promise<void> {
console.log('开始执行后台任务...');
try {
// 检查应用权限和用户设置
const hasPermission = await checkNotificationPermissions();
if (!hasPermission) {
console.log('没有通知权限,跳过后台任务');
return;
}
// await sendTestNotification()
// 执行喝水提醒检查任务 - 已禁用,改为由用户手动在设置页面管理
await executeWaterReminderTask();
// 执行站立提醒检查任务
// await executeStandReminderTask();
await executeChallengeReminderTask();
console.log('后台任务执行完成');
} catch (error) {
console.error('执行后台任务失败:', error);
}
}
/**
* 后台任务管理器
* 负责配置和管理应用的后台任务执行
*/
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 {
// 定义后台任务
TaskManager.defineTask(BACKGROUND_TASK_IDENTIFIER, async (body: TaskManagerTaskBody) => {
try {
log.info(`[BackgroundTask] 后台任务执行, 任务 ID: ${BACKGROUND_TASK_IDENTIFIER}`);
await executeBackgroundTasks();
} catch (error) {
console.error('[BackgroundTask] 任务执行失败:', error);
return BackgroundTask.BackgroundTaskResult.Failed;
}
return BackgroundTask.BackgroundTaskResult.Success;
});
if (await TaskManager.isTaskRegisteredAsync(BACKGROUND_TASK_IDENTIFIER)) {
log.info('[BackgroundTask] 任务已注册');
return
}
log.info('[BackgroundTask] 任务未注册, 开始注册...');
// 注册后台任务
await BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDENTIFIER, {
minimumInterval: 15 * 2,
});
this.isInitialized = true;
log.info('后台任务管理器初始化完成');
} catch (error) {
console.error('初始化后台任务管理器失败:', error);
throw error;
}
}
/**
* 停止后台任务
*/
async stop(): Promise<void> {
try {
await BackgroundTask.unregisterTaskAsync(BACKGROUND_TASK_IDENTIFIER);
console.log('后台任务已停止');
} catch (error) {
console.error('停止后台任务失败:', error);
}
}
/**
* 获取后台任务状态
*/
async getStatus(): Promise<BackgroundTask.BackgroundTaskStatus> {
try {
const status = await BackgroundTask.getStatusAsync();
return status || BackgroundTask.BackgroundTaskStatus.Restricted;
} catch (error) {
console.error('获取后台任务状态失败:', error);
return BackgroundTask.BackgroundTaskStatus.Restricted;
}
}
/**
* 检查后台任务状态
*/
async checkStatus(): Promise<string> {
const status = await this.getStatus();
switch (status) {
case BackgroundTask.BackgroundTaskStatus.Available:
return '可用';
case BackgroundTask.BackgroundTaskStatus.Restricted:
return '受限制';
default:
return '未知';
}
}
async triggerTaskForTesting(): Promise<void> {
await BackgroundTask.triggerTaskWorkerForTestingAsync();
}
/**
* 测试后台任务
*/
async testBackgroundTask(): Promise<void> {
console.log('开始测试后台任务...');
try {
// 手动触发后台任务执行
await executeBackgroundTasks();
console.log('后台任务测试完成');
} catch (error) {
console.error('后台任务测试失败:', 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;
}
}
}
/**
* 后台任务事件类型
*/
export interface BackgroundTaskEvent {
taskId: string;
timestamp: number;
success: boolean;
error?: string;
}