feat(background-task): 实现iOS原生后台任务V2系统并重构锻炼通知消息模板

- 新增iOS原生BackgroundTaskBridge桥接模块,支持后台任务注册、调度和完成
- 重构BackgroundTaskManager为V2版本,集成原生iOS后台任务能力
- 在AppDelegate中注册后台任务处理器,确保应用启动时正确初始化
- 重构锻炼通知消息生成逻辑,使用配置化模板提升可维护性
- 扩展健康数据类型映射,支持更多运动项目的中文显示
- 替换原有backgroundTaskManager引用为backgroundTaskManagerV2
This commit is contained in:
richarjiang
2025-11-04 09:41:10 +08:00
parent fbffa07f74
commit f80a1bae78
10 changed files with 1319 additions and 78 deletions

View File

@@ -1,5 +1,5 @@
import AsyncStorage from '@/utils/kvStore';
import { BackgroundTaskManager } from './backgroundTaskManager';
import { BackgroundTaskManager } from './backgroundTaskManagerV2';
/**
* 后台任务调试工具
@@ -174,4 +174,4 @@ export class BackgroundTaskDebugger {
}
}
export const backgroundTaskDebugger = BackgroundTaskDebugger.getInstance();
export const backgroundTaskDebugger = BackgroundTaskDebugger.getInstance();

View File

@@ -0,0 +1,522 @@
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 { log } from '@/utils/logger';
import { ChallengeNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
import { getWaterGoalFromStorage } from '@/utils/userPreferences';
import dayjs from 'dayjs';
export const BACKGROUND_TASK_IDENTIFIER = 'com.anonymous.digitalpilates.task';
const DEFAULT_RESCHEDULE_INTERVAL_SECONDS = 60 * 30; // 30 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('执行喝水提醒后台任务...');
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) {
return;
}
if (!isIosBackgroundModuleAvailable) {
log.warn('[BackgroundTaskManagerV2] iOS 原生后台模块不可用,跳过初始化');
this.isInitialized = false;
return;
}
const emitter = new NativeEventEmitter(NativeBackgroundModule);
this.eventSubscription = emitter.addListener(BACKGROUND_EVENT, (payload) => {
log.info('[BackgroundTaskManagerV2] 收到后台任务事件', payload);
this.handleBackgroundExecution();
});
this.expirationSubscription = emitter.addListener(EXPIRATION_EVENT, (payload) => {
log.warn('[BackgroundTaskManagerV2] 后台任务在完成前即将过期', payload);
});
try {
await NativeBackgroundModule.configure({
identifier: BACKGROUND_TASK_IDENTIFIER,
taskType: 'processing',
requiresNetworkConnectivity: false,
requiresExternalPower: false,
defaultDelay: DEFAULT_RESCHEDULE_INTERVAL_SECONDS,
});
this.isInitialized = true;
log.info('[BackgroundTaskManagerV2] 已初始化并注册 iOS 后台任务');
} catch (error: any) {
// BGTaskSchedulerErrorDomain 错误码 1 表示后台任务功能不可用
// 这在模拟器上是正常的,因为模拟器不完全支持后台任务
const errorMessage = error?.message || String(error);
const isBGTaskUnavailable = errorMessage.includes('BGTaskSchedulerErrorDomain') &&
errorMessage.includes('错误1');
if (isBGTaskUnavailable) {
log.warn('[BackgroundTaskManagerV2] 后台任务功能在当前环境不可用(模拟器限制),将在真机上正常工作');
this.removeListeners();
this.isInitialized = false;
// 不抛出错误,因为这是预期行为
return;
}
log.error('[BackgroundTaskManagerV2] 初始化失败', error);
this.removeListeners();
throw error;
}
}
private async handleBackgroundExecution(): Promise<void> {
if (this.executingPromise) {
log.info('[BackgroundTaskManagerV2] 已有后台任务在执行,忽略重复触发');
return;
}
this.executingPromise = executeBackgroundTasks()
.then(async () => {
if (isIosBackgroundModuleAvailable) {
try {
await NativeBackgroundModule.complete(true, DEFAULT_RESCHEDULE_INTERVAL_SECONDS);
} catch (error) {
log.error('[BackgroundTaskManagerV2] 标记后台任务成功完成失败', error);
}
}
})
.catch(async (error) => {
log.error('[BackgroundTaskManagerV2] 后台任务执行失败', error);
if (isIosBackgroundModuleAvailable) {
try {
await NativeBackgroundModule.complete(false, DEFAULT_RESCHEDULE_INTERVAL_SECONDS);
} catch (completionError) {
log.error('[BackgroundTaskManagerV2] 标记后台任务失败状态时出错', completionError);
}
}
})
.finally(() => {
this.executingPromise = null;
});
await this.executingPromise;
}
async stop(): Promise<void> {
if (!isIosBackgroundModuleAvailable) {
return;
}
try {
await NativeBackgroundModule.cancelAll();
} catch (error) {
log.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) {
log.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) {
await executeBackgroundTasks();
return;
}
try {
await NativeBackgroundModule.simulateLaunch();
} catch (error) {
log.error('[BackgroundTaskManagerV2] 模拟后台任务触发失败', error);
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) {
log.error('[BackgroundTaskManagerV2] 获取待处理的后台任务请求失败', error);
return [];
}
}
}
export type BackgroundTaskEvent = {
taskId: string;
timestamp: number;
success: boolean;
error?: string;
};
export const BackgroundTaskManager = BackgroundTaskManagerV2;

View File

@@ -72,88 +72,198 @@ export async function analyzeWorkoutAndSendNotification(workout: WorkoutData): P
}
}
interface WorkoutMessageConfig {
emoji: string;
titleTemplate: string;
bodyTemplate: (params: {
workoutType: string;
durationMinutes: number;
calories: number;
distanceKm?: string;
averageHeartRate?: number;
mets?: number;
}) => string;
encouragement: string;
dataExtractor?: (workout: WorkoutData, metrics: any) => Record<string, any>;
}
const WORKOUT_MESSAGES: Record<string, WorkoutMessageConfig> = {
running: {
emoji: '🏃‍♂️',
titleTemplate: '跑步完成!',
bodyTemplate: ({ durationMinutes, calories, averageHeartRate }) => {
let body = `您完成了${durationMinutes}分钟的跑步`;
if (calories > 0) {
body += `,消耗${calories}千卡`;
}
if (averageHeartRate) {
body += `(平均心率${averageHeartRate}次/分)`;
}
return body + '';
},
encouragement: '坚持就是胜利!💪',
dataExtractor: (workout, metrics) => ({
heartRateContext: metrics.averageHeartRate ? 'provided' : 'none'
})
},
cycling: {
emoji: '🚴‍♂️',
titleTemplate: '骑行完成!',
bodyTemplate: ({ durationMinutes, calories, distanceKm }) => {
let body = `${durationMinutes}分钟骑行`;
if (distanceKm) {
body += `,行程${distanceKm}公里`;
}
if (calories > 0) {
body += `,消耗${calories}千卡`;
}
return body + '';
},
encouragement: '追寻风的自由!🌟'
},
swimming: {
emoji: '🏊‍♂️',
titleTemplate: '游泳完成!',
bodyTemplate: ({ durationMinutes }) => {
return `水中${durationMinutes}分钟的锻炼完成!`;
},
encouragement: '全身肌肉都得到了锻炼!💦'
},
yoga: {
emoji: '🧘‍♀️',
titleTemplate: '瑜伽完成!',
bodyTemplate: ({ durationMinutes }) => {
return `${durationMinutes}分钟的瑜伽练习`;
},
encouragement: '身心合一,平静致远!🌸'
},
functionalstrengthtraining: {
emoji: '💪',
titleTemplate: '力量训练完成!',
bodyTemplate: ({ durationMinutes, calories, mets }) => {
let body = `${durationMinutes}分钟力量训练`;
if (calories > 0) {
body += `,消耗${calories}千卡`;
}
if (mets && mets > 6) {
body += '(高强度)';
}
return body + '';
},
encouragement: '肌肉正在变得更强壮!🔥',
dataExtractor: (workout, metrics) => ({
strengthLevel: metrics?.mets && metrics.mets > 6 ? 'high' : 'moderate'
})
},
traditionalstrengthtraining: {
emoji: '💪',
titleTemplate: '力量训练完成!',
bodyTemplate: ({ durationMinutes, calories, mets }) => {
let body = `${durationMinutes}分钟力量训练`;
if (calories > 0) {
body += `,消耗${calories}千卡`;
}
if (mets && mets > 6) {
body += '(高强度)';
}
return body + '';
},
encouragement: '肌肉正在变得更强壮!🔥',
dataExtractor: (workout, metrics) => ({
strengthLevel: metrics?.mets && metrics.mets > 6 ? 'high' : 'moderate'
})
},
highintensityintervaltraining: {
emoji: '🔥',
titleTemplate: 'HIIT训练完成',
bodyTemplate: ({ durationMinutes, calories }) => {
let body = `${durationMinutes}分钟高强度间歇训练`;
if (calories > 0) {
body += `,消耗${calories}千卡`;
}
return body + '';
},
encouragement: '心肺功能显著提升!⚡',
dataExtractor: (workout, metrics) => ({
hiitCompleted: true
})
}
};
function getWorkoutMessage(workoutTypeString?: string): WorkoutMessageConfig | null {
if (!workoutTypeString) return null;
const normalizedType = workoutTypeString.toLowerCase();
if (normalizedType.includes('strength')) {
return WORKOUT_MESSAGES.traditionalstrengthtraining;
}
return WORKOUT_MESSAGES[normalizedType] || null;
}
function generateEncouragementMessage(
workout: WorkoutData,
metrics: any
): WorkoutEncouragementMessage {
const workoutType = getWorkoutTypeDisplayName(workout.workoutActivityTypeString);
const durationMinutes = Math.round(workout.duration / 60);
const calories = workout.totalEnergyBurned ? Math.round(workout.totalEnergyBurned) : 0;
// 基于锻炼类型和指标生成个性化消息
let title = '锻炼完成!';
let body = '';
let data: Record<string, any> = {};
switch (workout.workoutActivityTypeString?.toLowerCase()) {
case 'running':
title = '🏃‍♂️ 跑步完成!';
body = `太棒了!您刚刚完成了${durationMinutes}分钟的跑步,消耗了约${calories}千卡热量。`;
if (metrics.averageHeartRate) {
body += `平均心率${metrics.averageHeartRate}次/分。`;
}
body += '坚持运动让身体更健康!💪';
break;
case 'cycling':
title = '🚴‍♂️ 骑行完成!';
body = `骑行${durationMinutes}分钟完成!消耗了约${calories}千卡热量。`;
if (workout.totalDistance) {
const distanceKm = (workout.totalDistance / 1000).toFixed(2);
body += `骑行距离${distanceKm}公里。`;
}
body += '享受骑行的自由吧!🌟';
break;
case 'swimming':
title = '🏊‍♂️ 游泳完成!';
body = `游泳${durationMinutes}分钟完成!消耗了约${calories}千卡热量。`;
body += '全身运动效果极佳,继续保持!💦';
break;
case 'yoga':
title = '🧘‍♀️ 瑜伽完成!';
body = `${durationMinutes}分钟的瑜伽练习完成!提升了柔韧性和内心平静。`;
body += '继续保持这份宁静!🌸';
break;
case 'functionalstrengthtraining':
case 'traditionalstrengthtraining':
title = '💪 力量训练完成!';
body = `力量训练${durationMinutes}分钟完成!消耗了约${calories}千卡热量。`;
if (metrics.mets && metrics.mets > 6) {
body += '高强度训练,效果显著!🔥';
}
body += '肌肉正在变得更强壮!';
break;
case 'highintensityintervaltraining':
title = '🔥 HIIT训练完成';
body = `高强度间歇训练${durationMinutes}分钟完成!消耗了约${calories}千卡热量。`;
body += '心肺功能得到有效提升,您的努力值得称赞!⚡';
break;
default:
title = '🎯 锻炼完成!';
body = `${workoutType}${durationMinutes}分钟完成!消耗了约${calories}千卡热量。`;
body += '坚持运动,健康生活!🌟';
break;
if (!workout) {
return {
title: '锻炼完成!',
body: '恭喜您完成锻炼!',
data: {}
};
}
// 添加心率区间分析(如果有心率数据)
if (metrics.heartRateZones && metrics.heartRateZones.length > 0) {
const dominantZone = metrics.heartRateZones.reduce((prev: any, current: any) =>
current.durationMinutes > prev.durationMinutes ? current : prev
);
const workoutType = getWorkoutTypeDisplayName(workout.workoutActivityTypeString) || '锻炼';
const durationMinutes = workout.duration ? Math.round(workout.duration / 60) : 0;
const calories = workout.totalEnergyBurned ? Math.round(workout.totalEnergyBurned) : 0;
const distanceKm = workout.totalDistance && workout.totalDistance > 0
? (workout.totalDistance / 1000).toFixed(2)
: undefined;
if (dominantZone.durationMinutes > 5) {
data.heartRateZone = dominantZone.key;
data.heartRateZoneLabel = dominantZone.label;
const messageConfig = getWorkoutMessage(workout.workoutActivityTypeString);
let title = '🎯 锻炼完成!';
let body = '';
const data: Record<string, any> = {};
if (messageConfig) {
title = `${messageConfig.emoji} ${messageConfig.titleTemplate}`;
body = messageConfig.bodyTemplate({
workoutType,
durationMinutes,
calories,
distanceKm,
averageHeartRate: metrics?.averageHeartRate,
mets: metrics?.mets
});
body += messageConfig.encouragement;
if (messageConfig.dataExtractor) {
Object.assign(data, messageConfig.dataExtractor(workout, metrics));
}
} else {
body = `${workoutType} ${durationMinutes}分钟完成!`;
if (calories > 0) {
body += `消耗${calories}千卡热量。`;
}
body += '坚持运动,收获健康!🌟';
}
if (metrics?.heartRateZones && Array.isArray(metrics.heartRateZones) && metrics.heartRateZones.length > 0) {
try {
const dominantZone = metrics.heartRateZones.reduce((prev: any, current: any) =>
current?.durationMinutes > prev?.durationMinutes ? current : prev
);
if (dominantZone?.durationMinutes > 5) {
data.heartRateZone = dominantZone.key;
data.heartRateZoneLabel = dominantZone.label;
}
} catch (error) {
console.warn('心率区间分析失败:', error);
}
}
// 添加锻炼强度评估
if (metrics.mets) {
if (metrics?.mets) {
data.intensity = metrics.mets < 3 ? 'low' : metrics.mets < 6 ? 'moderate' : 'high';
}