392 lines
12 KiB
TypeScript
392 lines
12 KiB
TypeScript
import { store } from '@/store';
|
||
import AsyncStorage from '@/utils/kvStore';
|
||
import { log } from '@/utils/logger';
|
||
import { listChallenges } from '@/services/challengesApi';
|
||
import { ChallengeNotificationHelpers, StandReminderHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
|
||
import { getWaterIntakeFromHealthKit } from '@/utils/health';
|
||
import { getWaterGoalFromStorage } from '@/utils/userPreferences';
|
||
import * as BackgroundTask from 'expo-background-task';
|
||
import * as TaskManager from 'expo-task-manager';
|
||
import { TaskManagerTaskBody } from 'expo-task-manager';
|
||
import dayjs from 'dayjs';
|
||
|
||
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 executeStandReminderTask(): Promise<void> {
|
||
try {
|
||
console.log('执行站立提醒后台任务...');
|
||
|
||
// 获取当前状态
|
||
const state = store.getState();
|
||
const userProfile = state.user.profile;
|
||
|
||
// 获取用户名
|
||
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);
|
||
}
|
||
}
|
||
|
||
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);
|
||
|
||
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;
|
||
}
|
||
|
||
try {
|
||
await ChallengeNotificationHelpers.sendEncouragementNotification({
|
||
userName,
|
||
challengeTitle: challenge.title,
|
||
challengeId: challenge.id,
|
||
});
|
||
|
||
await AsyncStorage.setItem(storageKey, todayKey);
|
||
} catch (notificationError) {
|
||
console.error('发送挑战鼓励通知失败:', notificationError);
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|