Files
digital-pilates/services/backgroundTaskManagerV2.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

712 lines
25 KiB
TypeScript
Raw Permalink 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 { NativeEventEmitter, NativeModules, Platform, type EmitterSubscription } from 'react-native';
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 { logger } from '@/utils/logger';
import { ChallengeNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
import { getWaterGoalFromStorage, getWaterReminderEnabled } from '@/utils/userPreferences';
import dayjs from 'dayjs';
export const BACKGROUND_TASK_IDENTIFIER = 'com.anonymous.digitalpilates.task';
const DEFAULT_RESCHEDULE_INTERVAL_SECONDS = 60 * 15; // 15 minutes
const BACKGROUND_EVENT = 'BackgroundTaskBridge.execute';
const EXPIRATION_EVENT = 'BackgroundTaskBridge.expire';
const NativeBackgroundModule = NativeModules.BackgroundTaskBridge;
const isIosBackgroundModuleAvailable = Platform.OS === 'ios' && NativeBackgroundModule;
// 检查通知权限
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;
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;
}
try {
const state = store.getState();
if (!state) {
console.log('Redux store 未初始化,跳过后台任务');
return;
}
} catch (error) {
console.log('无法访问 Redux store跳过后台任务:', error);
return;
}
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);
throw error;
}
}
export class BackgroundTaskManagerV2 {
private static instance: BackgroundTaskManagerV2;
private isInitialized = false;
private executingPromise: Promise<void> | null = null;
private eventSubscription?: EmitterSubscription;
private expirationSubscription?: EmitterSubscription;
static getInstance(): BackgroundTaskManagerV2 {
if (!BackgroundTaskManagerV2.instance) {
BackgroundTaskManagerV2.instance = new BackgroundTaskManagerV2();
}
return BackgroundTaskManagerV2.instance;
}
async initialize(): Promise<void> {
if (this.isInitialized) {
logger.info('[BackgroundTaskManagerV2] 后台任务管理器已初始化,跳过重复初始化');
return;
}
logger.info('[BackgroundTaskManagerV2] ====== 开始初始化后台任务管理器 ======');
if (!isIosBackgroundModuleAvailable) {
logger.warn('[BackgroundTaskManagerV2] iOS 原生后台模块不可用,跳过初始化');
logger.warn('[BackgroundTaskManagerV2] Platform:', Platform.OS);
this.isInitialized = false;
return;
}
logger.info('[BackgroundTaskManagerV2] 原生模块可用,开始注册事件监听器');
const emitter = new NativeEventEmitter(NativeBackgroundModule);
this.eventSubscription = emitter.addListener(BACKGROUND_EVENT, (payload) => {
logger.info('[BackgroundTaskManagerV2] ✅ 收到后台任务执行事件', payload);
this.handleBackgroundExecution();
});
this.expirationSubscription = emitter.addListener(EXPIRATION_EVENT, (payload) => {
logger.warn('[BackgroundTaskManagerV2] ⚠️ 后台任务即将过期', payload);
// 处理任务过期情况,确保重新调度
this.handleTaskExpiration();
});
if (typeof NativeBackgroundModule.markJSReady === 'function') {
try {
await NativeBackgroundModule.markJSReady();
logger.info('[BackgroundTaskManagerV2] 已通知原生层 JS 监听器就绪');
} catch (readyError) {
logger.warn('[BackgroundTaskManagerV2] 通知原生层 JS 准备状态失败', readyError);
}
}
logger.info('[BackgroundTaskManagerV2] 事件监听器注册完成');
try {
// 检查后台刷新状态
logger.info('[BackgroundTaskManagerV2] 检查后台刷新权限状态...');
const status = await this.getStatus();
logger.info('[BackgroundTaskManagerV2] 后台刷新状态:', status);
if (status === 'denied') {
logger.error('[BackgroundTaskManagerV2] ❌ 后台刷新被拒绝!');
logger.error('[BackgroundTaskManagerV2] 请在 设置 > Out Live > 后台App刷新 中启用');
this.isInitialized = false;
return;
}
if (status === 'restricted') {
logger.warn('[BackgroundTaskManagerV2] ⚠️ 后台刷新被限制(可能是家长控制)');
this.isInitialized = false;
return;
}
logger.info('[BackgroundTaskManagerV2] 配置后台任务...');
await NativeBackgroundModule.configure({
identifier: BACKGROUND_TASK_IDENTIFIER,
taskType: 'processing', // 使用 processing 类型,与 Info.plist 中的配置匹配
requiresNetworkConnectivity: false,
requiresExternalPower: false,
defaultDelay: DEFAULT_RESCHEDULE_INTERVAL_SECONDS,
});
this.isInitialized = true;
logger.info('[BackgroundTaskManagerV2] ✅ 后台任务配置成功');
// 立即调度一次后台任务
logger.info('[BackgroundTaskManagerV2] 调度首次后台任务...');
await this.scheduleNextTask();
// 检查待处理的任务请求
const pendingRequests = await this.getPendingRequests();
logger.info('[BackgroundTaskManagerV2] 当前待处理的任务请求数量:', pendingRequests.length);
if (pendingRequests.length > 0) {
logger.info('[BackgroundTaskManagerV2] 待处理任务详情:', JSON.stringify(pendingRequests, null, 2));
}
logger.info('[BackgroundTaskManagerV2] ====== 初始化完成 ======');
} catch (error: any) {
// BGTaskSchedulerErrorDomain 错误码 1 表示后台任务功能不可用
// 这在模拟器上是正常的,因为模拟器不完全支持后台任务
const errorMessage = error?.message || String(error);
const isBGTaskUnavailable = errorMessage.includes('BGTaskSchedulerErrorDomain') &&
(errorMessage.includes('错误1') || errorMessage.includes('code 1'));
if (isBGTaskUnavailable) {
logger.warn('[BackgroundTaskManagerV2] ⚠️ 后台任务功能在当前环境不可用');
logger.warn('[BackgroundTaskManagerV2] 这是模拟器的正常限制,在真机上会正常工作');
logger.warn('[BackgroundTaskManagerV2] 建议:在真机上测试后台任务功能');
this.removeListeners();
this.isInitialized = false;
// 不抛出错误,因为这是预期行为
return;
}
// 其他错误情况,尝试恢复
logger.error('[BackgroundTaskManagerV2] ❌ 初始化失败', error);
logger.error('[BackgroundTaskManagerV2] 错误详情:', errorMessage);
try {
logger.info('[BackgroundTaskManagerV2] 尝试恢复...');
await this.attemptRecovery();
logger.info('[BackgroundTaskManagerV2] ✅ 恢复成功');
} catch (recoveryError) {
logger.error('[BackgroundTaskManagerV2] ❌ 恢复失败,放弃初始化', recoveryError);
this.removeListeners();
throw error;
}
}
}
async scheduleNextTask(): Promise<void> {
if (!isIosBackgroundModuleAvailable) {
return;
}
try {
await NativeBackgroundModule.schedule({
delay: DEFAULT_RESCHEDULE_INTERVAL_SECONDS
});
logger.info('[BackgroundTaskManagerV2] 已调度下一次后台任务');
} catch (error) {
logger.error('[BackgroundTaskManagerV2] 调度后台任务失败', error);
}
}
private async handleBackgroundExecution(): Promise<void> {
if (this.executingPromise) {
logger.warn('[BackgroundTaskManagerV2] ⚠️ 已有后台任务在执行,忽略重复触发');
return;
}
const startTime = Date.now();
logger.info('[BackgroundTaskManagerV2] ===== 开始执行后台任务 =====');
logger.info(`[BackgroundTaskManagerV2] 执行时间: ${new Date().toLocaleString()}`);
logger.info('[BackgroundTaskManagerV2] 检查通知权限...');
// 记录任务开始时间
await AsyncStorage.setItem('@last_background_task_start', Date.now().toString());
this.executingPromise = executeBackgroundTasks()
.then(async () => {
const executionTime = Date.now() - startTime;
logger.info(`[BackgroundTaskManagerV2] ✅ 后台任务执行成功,耗时: ${executionTime}ms`);
if (isIosBackgroundModuleAvailable) {
try {
// 确保任务完成前有足够的时间完成所有操作
logger.info('[BackgroundTaskManagerV2] 等待1秒确保所有操作完成...');
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒确保所有操作完成
await NativeBackgroundModule.complete(true, DEFAULT_RESCHEDULE_INTERVAL_SECONDS);
logger.info('[BackgroundTaskManagerV2] ✅ 已标记后台任务成功完成并重新调度');
// 记录成功执行的时间戳
const successTime = Date.now();
await AsyncStorage.setItem('@last_background_execution_success', successTime.toString());
logger.info(`[BackgroundTaskManagerV2] ✅ 成功执行时间戳: ${new Date(successTime).toLocaleString()}`);
// 检查后台任务状态
const status = await this.getStatus();
const statusText = await this.checkStatus();
logger.info(`[BackgroundTaskManagerV2] 后台任务状态检查: ${status} (${statusText})`);
} catch (error) {
logger.error('[BackgroundTaskManagerV2] ❌ 标记后台任务成功完成失败', error);
// 即使标记失败,也尝试手动重新调度
await this.scheduleNextTask();
}
}
})
.catch(async (error) => {
const executionTime = Date.now() - startTime;
logger.error(`[BackgroundTaskManagerV2] ❌ 后台任务执行失败,耗时: ${executionTime}ms`, error);
if (isIosBackgroundModuleAvailable) {
try {
await NativeBackgroundModule.complete(false, DEFAULT_RESCHEDULE_INTERVAL_SECONDS);
logger.info('[BackgroundTaskManagerV2] 已标记后台任务失败并重新调度');
} catch (completionError) {
logger.error('[BackgroundTaskManagerV2] ❌ 标记后台任务失败状态时出错', completionError);
// 即使标记失败,也尝试手动重新调度
await this.scheduleNextTask();
}
}
})
.finally(() => {
this.executingPromise = null;
logger.info('[BackgroundTaskManagerV2] ===== 后台任务处理完成 =====');
});
await this.executingPromise;
}
private async handleTaskExpiration(): Promise<void> {
logger.warn('[BackgroundTaskManagerV2] 处理后台任务过期');
// 任务过期时,确保重新调度下一次任务
try {
await this.scheduleNextTask();
logger.info('[BackgroundTaskManagerV2] 已为过期的任务重新调度');
} catch (error) {
logger.error('[BackgroundTaskManagerV2] 为过期任务重新调度失败', error);
}
}
private async attemptRecovery(): Promise<void> {
logger.info('[BackgroundTaskManagerV2] 尝试恢复后台任务功能');
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 2000));
// 取消所有现有任务
if (isIosBackgroundModuleAvailable) {
try {
await NativeBackgroundModule.cancelAll();
logger.info('[BackgroundTaskManagerV2] 已取消所有现有后台任务');
} catch (error) {
logger.warn('[BackgroundTaskManagerV2] 取消现有任务失败', error);
}
}
// 重新配置
await NativeBackgroundModule.configure({
identifier: BACKGROUND_TASK_IDENTIFIER,
taskType: 'refresh',
requiresNetworkConnectivity: false,
requiresExternalPower: false,
defaultDelay: DEFAULT_RESCHEDULE_INTERVAL_SECONDS,
});
logger.info('[BackgroundTaskManagerV2] 后台任务功能恢复成功');
}
async stop(): Promise<void> {
if (!isIosBackgroundModuleAvailable) {
return;
}
try {
await NativeBackgroundModule.cancelAll();
} catch (error) {
logger.error('[BackgroundTaskManagerV2] 停止后台任务失败', error);
} finally {
this.removeListeners();
this.isInitialized = false;
}
}
private removeListeners(): void {
this.eventSubscription?.remove();
this.expirationSubscription?.remove();
this.eventSubscription = undefined;
this.expirationSubscription = undefined;
}
async getStatus(): Promise<string> {
if (!isIosBackgroundModuleAvailable) {
return Platform.OS;
}
try {
const status = await NativeBackgroundModule.backgroundRefreshStatus();
return status;
} catch (error) {
logger.error('[BackgroundTaskManagerV2] 获取后台任务状态失败', error);
return 'unknown';
}
}
async checkStatus(): Promise<string> {
const status = await this.getStatus();
switch (status) {
case 'available':
return '可用';
case 'restricted':
return '受限制';
case 'denied':
return '被拒绝';
default:
return '未知';
}
}
async triggerTaskForTesting(): Promise<void> {
if (!isIosBackgroundModuleAvailable) {
logger.info('[BackgroundTaskManagerV2] 原生模块不可用,直接执行后台任务逻辑');
await executeBackgroundTasks();
return;
}
try {
logger.info('[BackgroundTaskManagerV2] 尝试模拟触发后台任务...');
await NativeBackgroundModule.simulateLaunch();
logger.info('[BackgroundTaskManagerV2] ✅ 模拟触发成功');
} catch (error: any) {
const errorMessage = error?.message || String(error);
const errorCode = error?.code || '';
// 检查是否是模拟器不支持的错误
if (errorCode === 'SIMULATOR_NOT_SUPPORTED') {
logger.warn('[BackgroundTaskManagerV2] ⚠️ 模拟器不支持后台任务');
logger.warn('[BackgroundTaskManagerV2] 这是正常的限制,请在真机上测试');
logger.info('[BackgroundTaskManagerV2] 作为替代,直接执行后台任务逻辑...');
// 在模拟器上直接执行后台任务逻辑作为测试
await executeBackgroundTasks();
return;
}
// 检查是否是监听器未注册的错误
if (errorCode === 'NO_LISTENERS') {
logger.warn('[BackgroundTaskManagerV2] ⚠️ JS 监听器未注册');
logger.warn('[BackgroundTaskManagerV2] 可能是应用还未完全初始化');
logger.info('[BackgroundTaskManagerV2] 尝试直接执行后台任务逻辑...');
await executeBackgroundTasks();
return;
}
logger.error('[BackgroundTaskManagerV2] ❌ 模拟后台任务触发失败', error);
logger.error('[BackgroundTaskManagerV2] 错误代码:', errorCode);
logger.error('[BackgroundTaskManagerV2] 错误信息:', errorMessage);
throw error;
}
}
async testBackgroundTask(): Promise<void> {
await this.triggerTaskForTesting();
}
async getLastBackgroundCheckTime(): Promise<number | null> {
try {
const lastCheck = await AsyncStorage.getItem('@last_background_water_check');
return lastCheck ? parseInt(lastCheck, 10) : null;
} catch (error) {
console.error('获取最后后台检查时间失败:', error);
return null;
}
}
async getPendingRequests(): Promise<any[]> {
if (!isIosBackgroundModuleAvailable) {
return [];
}
try {
const requests = await NativeBackgroundModule.getPendingRequests();
return Array.isArray(requests) ? requests : [];
} catch (error) {
logger.error('[BackgroundTaskManagerV2] 获取待处理的后台任务请求失败', error);
return [];
}
}
}
export type BackgroundTaskEvent = {
taskId: string;
timestamp: number;
success: boolean;
error?: string;
};
export const BackgroundTaskManager = BackgroundTaskManagerV2;