Files
digital-pilates/services/backgroundTaskManager.ts
richarjiang 635d835a50 feat(fasting): 完善断食通知系统并优化错误提示
在应用启动时添加断食通知初始化逻辑,改进错误消息提示,并新增后台任务支持断食通知同步。同时优化挑战加入后的数据刷新流程和会员卡片显示样式。

主要更改:
- 添加断食通知启动检测和初始化
- 改进断食通知错误消息,提供更详细的用户指导
- 新增断食通知后台任务处理
- 优化挑战加入后自动刷新详情和排名数据
- 调整会员价格字体大小以提升视觉效果
2025-11-03 14:13:49 +08:00

495 lines
15 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 { resyncFastingNotifications } from '@/services/fastingNotifications';
import { selectActiveFastingSchedule, selectActiveFastingPlan } from '@/store/fastingSlice';
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('执行喝水提醒后台任务...');
// 获取当前状态,添加错误处理
let state;
try {
state = store.getState();
} catch (error) {
console.log('无法获取 Redux state使用本地存储:', error);
// 使用本地存储作为后备方案
const dailyGoal = await getWaterGoalFromStorage();
if (!dailyGoal || dailyGoal <= 0) {
console.log('没有设置喝水目标,跳过喝水提醒');
return;
}
// 简化的提醒逻辑
await sendSimpleWaterReminder();
return;
}
const waterStats = state.water?.todayStats;
const userProfile = state.user?.profile;
// 优先使用 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('执行挑战鼓励提醒后台任务...');
let userName = '朋友';
try {
const state = store.getState();
const normalizedUserName = state.user?.profile?.name?.trim();
userName = normalizedUserName && normalizedUserName.length > 0 ? normalizedUserName : '朋友';
} catch (error) {
console.log('无法获取用户名,使用默认值:', error);
}
const challenges = await listChallenges();
const joinedChallenges = challenges.filter((challenge) => challenge.isJoined && challenge.progress);
if (!joinedChallenges.length) {
console.log('没有加入的挑战或挑战没有进度,跳过挑战提醒');
return;
}
const todayKey = new Date().toISOString().slice(0, 10);
// 筛选出需要发送通知的挑战(未签到且今天未发送过通知)
const eligibleChallenges = [];
for (const challenge of joinedChallenges) {
const progress = challenge.progress;
if (!progress || progress.checkedInToday) {
continue;
}
const storageKey = `@challenge_encouragement_sent:${challenge.id}`;
const lastSent = await AsyncStorage.getItem(storageKey);
if (lastSent === todayKey) {
continue;
}
eligibleChallenges.push(challenge);
}
// 如果有符合条件的挑战,随机选择一个发送通知
if (eligibleChallenges.length > 0) {
const randomIndex = Math.floor(Math.random() * eligibleChallenges.length);
const selectedChallenge = eligibleChallenges[randomIndex];
try {
await ChallengeNotificationHelpers.sendEncouragementNotification({
userName,
challengeTitle: selectedChallenge.title,
challengeId: selectedChallenge.id,
});
const storageKey = `@challenge_encouragement_sent:${selectedChallenge.id}`;
await AsyncStorage.setItem(storageKey, todayKey);
console.log(`已随机选择并发送挑战鼓励通知: ${selectedChallenge.title}`);
} catch (notificationError) {
console.error('发送挑战鼓励通知失败:', notificationError);
}
} else {
console.log('没有符合条件的挑战需要发送鼓励通知');
}
console.log('挑战鼓励提醒后台任务完成');
} catch (error) {
console.error('执行挑战鼓励提醒后台任务失败:', error);
}
}
// 执行断食通知后台任务
async function executeFastingNotificationTask(): Promise<void> {
try {
console.log('执行断食通知后台任务...');
let state;
try {
state = store.getState();
} catch (error) {
console.log('无法获取 Redux state跳过断食通知任务:', error);
return;
}
const activeSchedule = selectActiveFastingSchedule(state);
const activePlan = selectActiveFastingPlan(state);
if (!activeSchedule || !activePlan) {
console.log('没有激活的断食计划,跳过断食通知任务');
return;
}
// 检查断食计划是否已结束
const end = dayjs(activeSchedule.endISO);
if (end.isBefore(dayjs())) {
console.log('断食计划已结束,跳过断食通知任务');
return;
}
console.log('正在同步断食通知...', {
planId: activePlan.id,
start: activeSchedule.startISO,
end: activeSchedule.endISO,
});
// 重新同步断食通知
await resyncFastingNotifications({
schedule: activeSchedule,
plan: activePlan,
enabled: true,
});
console.log('断食通知后台同步完成');
} catch (error) {
console.error('执行断食通知后台任务失败:', error);
}
}
// 发送测试通知以验证后台任务执行
async function sendTestNotification(): Promise<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 sendSimpleWaterReminder(): Promise<void> {
try {
const userName = '朋友'; // 默认用户名
const Notifications = await import('expo-notifications');
const notificationId = await Notifications.scheduleNotificationAsync({
content: {
title: '💧 该喝水啦!',
body: `${userName},记得补充水分,保持身体健康~`,
data: {
type: 'water_reminder',
url: '/statistics'
},
sound: 'default',
},
trigger: null, // 立即发送
});
console.log('简单喝水提醒已发送ID:', notificationId);
} catch (error) {
console.error('发送简单喝水提醒失败:', error);
}
}
// 后台任务执行函数
async function executeBackgroundTasks(): Promise<void> {
console.log('开始执行后台任务...');
try {
// 检查应用权限和用户设置
const hasPermission = await checkNotificationPermissions();
if (!hasPermission) {
console.log('没有通知权限,跳过后台任务');
return;
}
// 确保 Redux store 已初始化
try {
const state = store.getState();
if (!state) {
console.log('Redux store 未初始化,跳过后台任务');
return;
}
} catch (error) {
console.log('无法访问 Redux store跳过后台任务:', error);
return;
}
// await sendTestNotification() // 可选:启用测试通知
// 检查是否启用测试通知
const testNotificationsEnabled = await AsyncStorage.getItem('@background_test_notifications_enabled') === 'true';
if (testNotificationsEnabled) {
await sendTestNotification();
}
// 执行喝水提醒检查任务
await executeWaterReminderTask();
// 执行挑战鼓励提醒任务
await executeChallengeReminderTask();
// 执行断食通知任务
await executeFastingNotificationTask();
console.log('后台任务执行完成');
} catch (error) {
console.error('执行后台任务失败:', error);
}
}
/**
* 后台任务管理器
* 负责配置和管理应用的后台任务执行
*/
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] 任务已注册');
} else {
log.info('[BackgroundTask] 任务未注册,开始注册...');
await BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDENTIFIER);
log.info('[BackgroundTask] 任务注册完成');
}
// 验证任务状态
const status = await BackgroundTask.getStatusAsync();
log.info(`[BackgroundTask] 任务状态: ${status}`);
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;
}