Files
digital-pilates/services/backgroundTaskManager.ts
richarjiang 84abfa2506 feat(medication): 重构AI分析为结构化展示并支持喝水提醒个性化配置
- 将药品AI分析从Markdown流式输出重构为结构化数据展示(V2)
- 新增适合人群、不适合人群、主要成分、副作用等分类卡片展示
- 优化AI分析UI布局,采用卡片式设计提升可读性
- 新增药品跳过功能,支持用户标记本次用药为已跳过
- 修复喝水提醒逻辑,支持用户开关控制和自定义时间段配置
- 优化个人资料编辑页面键盘适配,避免输入框被遮挡
- 统一API响应码处理,兼容200和0两种成功状态码
- 更新版本号至1.0.28

BREAKING CHANGE: 药品AI分析接口从流式Markdown输出改为结构化JSON格式,旧版本分析结果将不再显示
2025-11-20 10:10:53 +08:00

502 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 { resyncFastingNotifications } from '@/services/fastingNotifications';
import { store } from '@/store';
import { selectActiveFastingPlan, selectActiveFastingSchedule } from '@/store/fastingSlice';
import { getWaterIntakeFromHealthKit } from '@/utils/health';
import AsyncStorage from '@/utils/kvStore';
import { log } from '@/utils/logger';
import { ChallengeNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
import { getWaterGoalFromStorage, getWaterReminderEnabled } 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 isEnabled = await getWaterReminderEnabled();
if (!isEnabled) {
console.log('喝水提醒未开启,跳过后台任务');
return;
}
// 获取当前状态,添加错误处理
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;
}