- 新增iOS原生BackgroundTaskBridge桥接模块,支持后台任务注册、调度和完成 - 重构BackgroundTaskManager为V2版本,集成原生iOS后台任务能力 - 在AppDelegate中注册后台任务处理器,确保应用启动时正确初始化 - 重构锻炼通知消息生成逻辑,使用配置化模板提升可维护性 - 扩展健康数据类型映射,支持更多运动项目的中文显示 - 替换原有backgroundTaskManager引用为backgroundTaskManagerV2
271 lines
8.4 KiB
TypeScript
271 lines
8.4 KiB
TypeScript
import { getWorkoutTypeDisplayName, WorkoutData } from '@/utils/health';
|
||
import { getNotificationEnabled } from '@/utils/userPreferences';
|
||
import {
|
||
getWorkoutNotificationEnabled,
|
||
isNotificationTimeAllowed,
|
||
isWorkoutTypeEnabled
|
||
} from '@/utils/workoutPreferences';
|
||
import { notificationService, NotificationTypes } from './notifications';
|
||
import { getWorkoutDetailMetrics } from './workoutDetail';
|
||
|
||
interface WorkoutEncouragementMessage {
|
||
title: string;
|
||
body: string;
|
||
data: Record<string, any>;
|
||
}
|
||
|
||
export async function analyzeWorkoutAndSendNotification(workout: WorkoutData): Promise<void> {
|
||
try {
|
||
// 检查用户是否启用了通用通知
|
||
const notificationsEnabled = await getNotificationEnabled();
|
||
if (!notificationsEnabled) {
|
||
console.log('用户已禁用通知,跳过锻炼结束通知');
|
||
return;
|
||
}
|
||
|
||
// 检查用户是否启用了锻炼通知
|
||
const workoutNotificationsEnabled = await getWorkoutNotificationEnabled();
|
||
if (!workoutNotificationsEnabled) {
|
||
console.log('用户已禁用锻炼通知,跳过锻炼结束通知');
|
||
return;
|
||
}
|
||
|
||
// 检查时间限制(避免深夜打扰)
|
||
const timeAllowed = await isNotificationTimeAllowed();
|
||
if (!timeAllowed) {
|
||
console.log('当前时间不适合发送通知,跳过锻炼结束通知');
|
||
return;
|
||
}
|
||
|
||
// 检查特定锻炼类型是否启用了通知
|
||
const workoutTypeEnabled = await isWorkoutTypeEnabled(workout.workoutActivityTypeString || '');
|
||
if (!workoutTypeEnabled) {
|
||
console.log('该锻炼类型已禁用通知,跳过锻炼结束通知:', workout.workoutActivityTypeString);
|
||
return;
|
||
}
|
||
|
||
// 获取详细的锻炼指标
|
||
const workoutMetrics = await getWorkoutDetailMetrics(workout);
|
||
|
||
// 生成个性化鼓励消息
|
||
const message = generateEncouragementMessage(workout, workoutMetrics);
|
||
|
||
// 发送通知
|
||
await notificationService.sendImmediateNotification({
|
||
title: message.title,
|
||
body: message.body,
|
||
data: {
|
||
type: NotificationTypes.WORKOUT_COMPLETION,
|
||
workoutId: workout.id,
|
||
workoutType: workout.workoutActivityTypeString,
|
||
duration: workout.duration,
|
||
calories: workout.totalEnergyBurned,
|
||
...message.data
|
||
},
|
||
sound: true,
|
||
priority: 'high'
|
||
});
|
||
|
||
console.log('锻炼结束通知已发送:', message.title);
|
||
} catch (error) {
|
||
console.error('发送锻炼结束通知失败:', error);
|
||
}
|
||
}
|
||
|
||
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 {
|
||
if (!workout) {
|
||
return {
|
||
title: '锻炼完成!',
|
||
body: '恭喜您完成锻炼!',
|
||
data: {}
|
||
};
|
||
}
|
||
|
||
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;
|
||
|
||
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) {
|
||
data.intensity = metrics.mets < 3 ? 'low' : metrics.mets < 6 ? 'moderate' : 'high';
|
||
}
|
||
|
||
return { title, body, data };
|
||
} |