feat(notifications): 新增晚餐和心情提醒功能,支持HRV压力检测和后台处理

- 新增晚餐提醒(18:00)和心情提醒(21:00)的定时通知
- 实现基于HRV数据的压力检测和智能鼓励通知
- 添加后台任务处理支持,修改iOS后台模式为processing
- 优化营养记录页面使用Redux状态管理,支持实时数据更新
- 重构卡路里计算公式,移除目标卡路里概念,改为基代+运动-饮食
- 新增营养目标动态计算功能,基于用户身体数据智能推荐
- 完善通知点击跳转逻辑,支持多种提醒类型的路由处理
This commit is contained in:
richarjiang
2025-09-01 10:29:13 +08:00
parent fe634ba258
commit a34ca556e8
12 changed files with 867 additions and 189 deletions

View File

@@ -454,6 +454,19 @@ export async function fetchTodayHRV(): Promise<number | null> {
return fetchHRVForDate(dayjs().toDate());
}
// 获取最近几小时内的实时HRV数据
export async function fetchRecentHRV(hoursBack: number = 2): Promise<number | null> {
console.log(`开始获取最近${hoursBack}小时内的HRV数据...`);
const now = new Date();
const options = {
startDate: dayjs(now).subtract(hoursBack, 'hour').toDate().toISOString(),
endDate: now.toISOString()
};
return fetchHeartRateVariability(options);
}
// 更新healthkit中的体重
export async function updateWeight(weight: number) {
return new Promise((resolve) => {

View File

@@ -12,12 +12,12 @@ export function buildCoachDeepLink(params: {
}): string {
const baseUrl = '/coach';
const searchParams = new URLSearchParams();
if (params.action) searchParams.set('action', params.action);
if (params.subAction) searchParams.set('subAction', params.subAction);
if (params.meal) searchParams.set('meal', params.meal);
if (params.message) searchParams.set('message', encodeURIComponent(params.message));
const queryString = searchParams.toString();
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
}
@@ -286,48 +286,6 @@ export class GoalNotificationHelpers {
}
}
/**
* 心情相关的通知辅助函数
*/
export class MoodNotificationHelpers {
/**
* 发送心情打卡提醒
*/
static async sendMoodCheckinReminder(userName: string) {
return notificationService.sendImmediateNotification({
title: '心情打卡',
body: `${userName},记得记录今天的心情状态哦`,
data: { type: 'mood_checkin_reminder' },
sound: true,
priority: 'normal',
});
}
/**
* 安排每日心情打卡提醒
*/
static async scheduleDailyMoodReminder(userName: string, hour: number = 20, minute: number = 0) {
const reminderTime = new Date();
reminderTime.setHours(hour, minute, 0, 0);
// 如果今天的时间已经过了,设置为明天
if (reminderTime.getTime() <= Date.now()) {
reminderTime.setDate(reminderTime.getDate() + 1);
}
return notificationService.scheduleRepeatingNotification(
{
title: '每日心情打卡',
body: `${userName},记得记录今天的心情状态哦`,
data: { type: 'daily_mood_reminder' },
sound: true,
priority: 'normal',
},
{ days: 1 }
);
}
}
/**
* 营养相关的通知辅助函数
*/
@@ -423,8 +381,8 @@ export class NutritionNotificationHelpers {
return notificationService.sendImmediateNotification({
title: '午餐记录提醒',
body: `${userName},记得记录今天的午餐情况哦!`,
data: {
type: 'lunch_reminder',
data: {
type: 'lunch_reminder',
meal: '午餐',
url: coachUrl
},
@@ -453,6 +411,82 @@ export class NutritionNotificationHelpers {
}
}
/**
* 安排每日晚餐提醒
* @param userName 用户名
* @param hour 小时 (默认18点)
* @param minute 分钟 (默认0分)
* @returns 通知ID
*/
static async scheduleDailyDinnerReminder(
userName: string,
hour: number = 18,
minute: number = 0
): Promise<string | null> {
try {
// 检查是否已经存在晚餐提醒
const existingNotifications = await notificationService.getAllScheduledNotifications();
const existingDinnerReminder = existingNotifications.find(
notification =>
notification.content.data?.type === 'dinner_reminder' &&
notification.content.data?.isDailyReminder === true
);
if (existingDinnerReminder) {
console.log('晚餐提醒已存在,跳过重复注册:', existingDinnerReminder.identifier);
return existingDinnerReminder.identifier;
}
// 创建晚餐提醒通知
const notificationId = await notificationService.scheduleCalendarRepeatingNotification(
{
title: '🍽️ 晚餐时光到啦!',
body: `${userName},美好的晚餐时光开始了~记得记录今天的晚餐哦!营养均衡很重要呢 💪`,
data: {
type: 'dinner_reminder',
isDailyReminder: true,
meal: '晚餐',
url: '/nutrition/records' // 直接跳转到营养记录页面
},
sound: true,
priority: 'normal',
},
{
type: Notifications.SchedulableTriggerInputTypes.DAILY,
hour: hour,
minute: minute,
}
);
console.log('每日晚餐提醒已安排ID:', notificationId);
return notificationId;
} catch (error) {
console.error('安排每日晚餐提醒失败:', error);
throw error;
}
}
/**
* 取消晚餐提醒
*/
static async cancelDinnerReminder(): Promise<void> {
try {
const notifications = await notificationService.getAllScheduledNotifications();
for (const notification of notifications) {
if (notification.content.data?.type === 'dinner_reminder' &&
notification.content.data?.isDailyReminder === true) {
await notificationService.cancelNotification(notification.identifier);
console.log('已取消晚餐提醒:', notification.identifier);
}
}
} catch (error) {
console.error('取消晚餐提醒失败:', error);
throw error;
}
}
/**
* 安排营养记录提醒
*/
@@ -478,10 +512,10 @@ export class NutritionNotificationHelpers {
// 构建深度链接
const mealTypeMap: Record<string, string> = {
'早餐': 'breakfast',
'午餐': 'lunch',
'午餐': 'lunch',
'晚餐': 'dinner'
};
const coachUrl = buildCoachDeepLink({
action: 'diet',
subAction: 'card',
@@ -492,8 +526,8 @@ export class NutritionNotificationHelpers {
{
title: `${mealTime.meal}提醒`,
body: `${userName},记得记录您的${mealTime.meal}情况`,
data: {
type: 'meal_reminder',
data: {
type: 'meal_reminder',
meal: mealTime.meal,
url: coachUrl
},
@@ -510,6 +544,102 @@ export class NutritionNotificationHelpers {
}
}
/**
* 心情相关的通知辅助函数
*/
export class MoodNotificationHelpers {
/**
* 安排每日心情提醒
* @param userName 用户名
* @param hour 小时 (默认21点)
* @param minute 分钟 (默认0分)
* @returns 通知ID
*/
static async scheduleDailyMoodReminder(
userName: string,
hour: number = 21,
minute: number = 0
): Promise<string | null> {
try {
// 检查是否已经存在心情提醒
const existingNotifications = await notificationService.getAllScheduledNotifications();
const existingMoodReminder = existingNotifications.find(
notification =>
notification.content.data?.type === 'mood_reminder' &&
notification.content.data?.isDailyReminder === true
);
if (existingMoodReminder) {
console.log('心情提醒已存在,跳过重复注册:', existingMoodReminder.identifier);
return existingMoodReminder.identifier;
}
// 创建心情提醒通知
const notificationId = await notificationService.scheduleCalendarRepeatingNotification(
{
title: '🌙 今天过得怎么样呀?',
body: `${userName},夜深了~来记录一下今天的心情吧!每一份情感都值得被珍藏 ✨💕`,
data: {
type: 'mood_reminder',
isDailyReminder: true,
url: '/mood-statistics' // 跳转到心情统计页面
},
sound: true,
priority: 'normal',
},
{
type: Notifications.SchedulableTriggerInputTypes.DAILY,
hour: hour,
minute: minute,
}
);
console.log('每日心情提醒已安排ID:', notificationId);
return notificationId;
} catch (error) {
console.error('安排每日心情提醒失败:', error);
throw error;
}
}
/**
* 发送心情记录提醒
*/
static async sendMoodReminder(userName: string) {
return notificationService.sendImmediateNotification({
title: '🌙 今天过得怎么样呀?',
body: `${userName},夜深了~来记录一下今天的心情吧!每一份情感都值得被珍藏 ✨💕`,
data: {
type: 'mood_reminder',
url: '/mood-statistics'
},
sound: true,
priority: 'normal',
});
}
/**
* 取消心情提醒
*/
static async cancelMoodReminder(): Promise<void> {
try {
const notifications = await notificationService.getAllScheduledNotifications();
for (const notification of notifications) {
if (notification.content.data?.type === 'mood_reminder' &&
notification.content.data?.isDailyReminder === true) {
await notificationService.cancelNotification(notification.identifier);
console.log('已取消心情提醒:', notification.identifier);
}
}
} catch (error) {
console.error('取消心情提醒失败:', error);
throw error;
}
}
}
/**
* 通用通知辅助函数
*/
@@ -634,11 +764,11 @@ export const NotificationTemplates = {
reminder: (userName: string, meal: string) => {
const mealTypeMap: Record<string, string> = {
'早餐': 'breakfast',
'午餐': 'lunch',
'午餐': 'lunch',
'晚餐': 'dinner',
'加餐': 'snack'
};
const coachUrl = buildCoachDeepLink({
action: 'diet',
subAction: 'card',
@@ -648,8 +778,8 @@ export const NotificationTemplates = {
return {
title: `${meal}提醒`,
body: `${userName},记得记录您的${meal}情况`,
data: {
type: 'meal_reminder',
data: {
type: 'meal_reminder',
meal,
url: coachUrl
},
@@ -667,8 +797,8 @@ export const NotificationTemplates = {
return {
title: '午餐记录提醒',
body: `${userName},记得记录今天的午餐情况哦!`,
data: {
type: 'lunch_reminder',
data: {
type: 'lunch_reminder',
meal: '午餐',
url: coachUrl
},

78
utils/nutrition.ts Normal file
View File

@@ -0,0 +1,78 @@
import dayjs from 'dayjs';
export interface NutritionGoals {
calories: number;
proteinGoal: number;
fatGoal: number;
carbsGoal: number;
fiberGoal: number;
sodiumGoal: number;
}
export interface UserProfileForNutrition {
weight?: string;
height?: string;
birthDate?: Date;
gender?: 'male' | 'female';
}
/**
* 计算用户的营养目标
* 基于Mifflin-St Jeor公式计算基础代谢率然后计算各营养素目标
*/
export const calculateNutritionGoals = (userProfile?: UserProfileForNutrition): NutritionGoals => {
const weight = parseFloat(userProfile?.weight || '70'); // 默认70kg
const height = parseFloat(userProfile?.height || '170'); // 默认170cm
const age = userProfile?.birthDate ?
dayjs().diff(dayjs(userProfile.birthDate), 'year') : 25; // 默认25岁
const isWoman = userProfile?.gender === 'female';
// 基础代谢率计算Mifflin-St Jeor Equation
let bmr;
if (isWoman) {
bmr = 10 * weight + 6.25 * height - 5 * age - 161;
} else {
bmr = 10 * weight + 6.25 * height - 5 * age + 5;
}
// 总热量需求(假设轻度活动)
const totalCalories = bmr * 1.375;
// 计算营养素目标
const proteinGoal = weight * 1.6; // 1.6g/kg
const fatGoal = totalCalories * 0.25 / 9; // 25%来自脂肪9卡/克
const carbsGoal = (totalCalories - proteinGoal * 4 - fatGoal * 9) / 4; // 剩余来自碳水
// 纤维目标成人推荐25-35g/天
const fiberGoal = 25;
// 钠目标WHO推荐<2300mg/天
const sodiumGoal = 2300;
return {
calories: Math.round(totalCalories),
proteinGoal: Math.round(proteinGoal * 10) / 10,
fatGoal: Math.round(fatGoal * 10) / 10,
carbsGoal: Math.round(carbsGoal * 10) / 10,
fiberGoal,
sodiumGoal,
};
};
/**
* 计算剩余可摄入卡路里
* 公式:还能吃 = 基础代谢 + 运动消耗 - 已摄入饮食
*/
export const calculateRemainingCalories = (params: {
basalMetabolism: number;
activeCalories: number;
consumedCalories: number;
}): number => {
const { basalMetabolism, activeCalories, consumedCalories } = params;
// 总消耗 = 基础代谢 + 运动消耗
const totalBurned = basalMetabolism + activeCalories;
// 剩余可摄入 = 总消耗 - 已摄入
return totalBurned - consumedCalories;
};