feat: 新增喝水提醒功能,支持定期提醒和目标检查

This commit is contained in:
richarjiang
2025-09-02 18:56:40 +08:00
parent ccbc3417bc
commit 70e3152158
6 changed files with 569 additions and 61 deletions

View File

@@ -18,6 +18,8 @@ import { notificationService } from '@/services/notifications';
import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice';
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
import { fetchTodayWaterStats, selectTodayStats } from '@/store/waterSlice';
import { WaterNotificationHelpers } from '@/utils/notificationHelpers';
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
import { ensureHealthPermissions, fetchHealthDataForDate, fetchRecentHRV } from '@/utils/health';
import { getTestHealthData } from '@/utils/mockHealthData';
@@ -26,6 +28,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { useFocusEffect } from '@react-navigation/native';
import dayjs from 'dayjs';
import { debounce } from 'lodash';
import { LinearGradient } from 'expo-linear-gradient';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
@@ -77,20 +80,22 @@ export default function ExploreScreen() {
return getTabBarBottomPadding(tabBarHeight) + (insets?.bottom ?? 0);
}, [tabBarHeight, insets?.bottom]);
// 获取当前选中日期
const getCurrentSelectedDate = () => {
// 获取当前选中日期 - 使用 useMemo 缓存避免重复计算
const currentSelectedDate = useMemo(() => {
const days = getMonthDaysZh();
return days[selectedIndex]?.date?.toDate() ?? new Date();
};
}, [selectedIndex]);
// 获取当前选中日期
const currentSelectedDate = getCurrentSelectedDate();
const currentSelectedDateString = dayjs(currentSelectedDate).format('YYYY-MM-DD');
const currentSelectedDateString = useMemo(() => {
return dayjs(currentSelectedDate).format('YYYY-MM-DD');
}, [currentSelectedDate]);
// 从 Redux 获取指定日期的健康数据
const healthData = useAppSelector(selectHealthDataByDate(currentSelectedDateString));
// 获取今日喝水统计数据
const todayWaterStats = useAppSelector(selectTodayStats);
// 解构健康数据支持mock数据
const mockData = useMockData ? getTestHealthData('mock') : null;
const stepCount: number | null = useMockData ? (mockData?.steps ?? null) : (healthData?.steps ?? null);
@@ -151,8 +156,36 @@ export default function ExploreScreen() {
// 记录最近一次请求的"日期键",避免旧请求覆盖新结果
const latestRequestKeyRef = useRef<string | null>(null);
// 请求状态管理,防止重复请求
const loadingRef = useRef({
health: false,
nutrition: false,
mood: false
});
// 数据缓存时间戳,避免短时间内重复拉取
const dataTimestampRef = useRef<{ [key: string]: number }>({});
const getDateKey = (d: Date) => `${dayjs(d).year()}-${dayjs(d).month() + 1}-${dayjs(d).date()}`;
// 检查数据是否需要刷新2分钟内不重复拉取对营养数据更严格
const shouldRefreshData = (dateKey: string, dataType: string) => {
const cacheKey = `${dateKey}-${dataType}`;
const lastUpdate = dataTimestampRef.current[cacheKey];
const now = Date.now();
// 营养数据使用更短的缓存时间其他数据使用5分钟
const cacheTime = dataType === 'nutrition' ? 2 * 60 * 1000 : 5 * 60 * 1000;
return !lastUpdate || (now - lastUpdate) > cacheTime;
};
// 更新数据时间戳
const updateDataTimestamp = (dateKey: string, dataType: string) => {
const cacheKey = `${dateKey}-${dataType}`;
dataTimestampRef.current[cacheKey] = Date.now();
};
// 从 Redux 获取当前日期的心情记录
const currentMoodCheckin = useAppSelector(selectLatestMoodRecordByDate(
@@ -160,33 +193,74 @@ export default function ExploreScreen() {
));
// 加载心情数据
const loadMoodData = async (targetDate?: Date) => {
const loadMoodData = async (targetDate?: Date, forceRefresh = false) => {
if (!isLoggedIn) return;
try {
setIsMoodLoading(true);
// 确定要查询的日期:优先使用传入的日期,否则使用当前选中索引对应的日期
// 确定要查询的日期
let derivedDate: Date;
if (targetDate) {
derivedDate = targetDate;
} else {
derivedDate = getCurrentSelectedDate();
derivedDate = currentSelectedDate;
}
const requestKey = getDateKey(derivedDate);
// 检查是否正在加载或不需要刷新
if (loadingRef.current.mood) {
console.log('心情数据正在加载中,跳过重复请求');
return;
}
if (!forceRefresh && !shouldRefreshData(requestKey, 'mood')) {
console.log('心情数据缓存未过期,跳过请求');
return;
}
try {
loadingRef.current.mood = true;
setIsMoodLoading(true);
const dateString = dayjs(derivedDate).format('YYYY-MM-DD');
await dispatch(fetchDailyMoodCheckins(dateString));
// 更新缓存时间戳
updateDataTimestamp(requestKey, 'mood');
} catch (error) {
console.error('加载心情数据失败:', error);
} finally {
loadingRef.current.mood = false;
setIsMoodLoading(false);
}
};
const loadHealthData = async (targetDate?: Date) => {
const loadHealthData = async (targetDate?: Date, forceRefresh = false) => {
// 确定要查询的日期
let derivedDate: Date;
if (targetDate) {
derivedDate = targetDate;
} else {
derivedDate = currentSelectedDate;
}
const requestKey = getDateKey(derivedDate);
// 检查是否正在加载或不需要刷新
if (loadingRef.current.health) {
console.log('健康数据正在加载中,跳过重复请求');
return;
}
if (!forceRefresh && !shouldRefreshData(requestKey, 'health')) {
console.log('健康数据缓存未过期,跳过请求');
return;
}
try {
loadingRef.current.health = true;
console.log('=== 开始HealthKit初始化流程 ===');
const ok = await ensureHealthPermissions();
@@ -196,15 +270,6 @@ export default function ExploreScreen() {
return;
}
// 确定要查询的日期:优先使用传入的日期,否则使用当前选中索引对应的日期
let derivedDate: Date;
if (targetDate) {
derivedDate = targetDate;
} else {
derivedDate = getCurrentSelectedDate();
}
const requestKey = getDateKey(derivedDate);
latestRequestKeyRef.current = requestKey;
console.log('权限获取成功,开始获取健康数据...', derivedDate);
@@ -223,8 +288,10 @@ export default function ExploreScreen() {
// 更新HRV数据时间
setHrvUpdateTime(new Date());
setAnimToken((t) => t + 1);
// 更新缓存时间戳
updateDataTimestamp(requestKey, 'health');
} else {
console.log('忽略过期健康数据请求结果key=', requestKey, '最新key=', latestRequestKeyRef.current);
}
@@ -232,55 +299,108 @@ export default function ExploreScreen() {
} catch (error) {
console.error('HealthKit流程出现异常:', error);
} finally {
loadingRef.current.health = false;
}
};
// 加载营养数据
const loadNutritionData = async (targetDate?: Date) => {
try {
// 确定要查询的日期:优先使用传入的日期,否则使用当前选中索引对应的日期
const loadNutritionData = async (targetDate?: Date, forceRefresh = false) => {
if (!isLoggedIn) return;
// 确定要查询的日期
let derivedDate: Date;
if (targetDate) {
derivedDate = targetDate;
} else {
derivedDate = getCurrentSelectedDate();
derivedDate = currentSelectedDate;
}
const requestKey = getDateKey(derivedDate);
// 检查是否正在加载或不需要刷新
if (loadingRef.current.nutrition) {
console.log('营养数据正在加载中,跳过重复请求');
return;
}
if (!forceRefresh && !shouldRefreshData(requestKey, 'nutrition')) {
console.log('营养数据缓存未过期,跳过请求');
return;
}
try {
loadingRef.current.nutrition = true;
console.log('加载营养数据...', derivedDate);
await dispatch(fetchDailyNutritionData(derivedDate));
console.log('营养数据加载完成');
// 更新缓存时间戳
updateDataTimestamp(requestKey, 'nutrition');
} catch (error) {
console.error('营养数据加载失败:', error);
} finally {
loadingRef.current.nutrition = false;
}
};
// 加载所有数据的统一方法
const loadAllData = React.useCallback((targetDate?: Date) => {
const dateToUse = targetDate || getCurrentSelectedDate();
// 实际执行数据加载的方法
const executeLoadAllData = React.useCallback((targetDate?: Date, forceRefresh = false) => {
const dateToUse = targetDate || currentSelectedDate;
if (dateToUse) {
loadHealthData(dateToUse);
console.log('执行数据加载,日期:', dateToUse, '强制刷新:', forceRefresh);
loadHealthData(dateToUse, forceRefresh);
if (isLoggedIn) {
loadNutritionData(dateToUse);
loadMoodData(dateToUse);
loadNutritionData(dateToUse, forceRefresh);
loadMoodData(dateToUse, forceRefresh);
// 加载喝水数据(只加载今日数据用于后台检查)
const isToday = dayjs(dateToUse).isSame(dayjs(), 'day');
if (isToday) {
dispatch(fetchTodayWaterStats());
}
}
}, [isLoggedIn]);
}
}, [isLoggedIn, dispatch]);
useFocusEffect(
React.useCallback(() => {
// 每次聚焦时都拉取当前选中日期的最新数据
loadAllData();
}, [loadAllData])
// 使用 lodash debounce 防抖的加载所有数据方法
const debouncedLoadAllData = React.useMemo(
() => debounce(executeLoadAllData, 300), // 300ms 防抖延迟
[executeLoadAllData]
);
// AppState 监听:应用从后台返回前台时刷新数据
// 对外暴露的 loadAllData 方法
const loadAllData = React.useCallback((targetDate?: Date, forceRefresh = false) => {
if (forceRefresh) {
// 如果是强制刷新,立即执行,不使用防抖
executeLoadAllData(targetDate, forceRefresh);
} else {
// 普通调用使用防抖
debouncedLoadAllData(targetDate, forceRefresh);
}
}, [executeLoadAllData, debouncedLoadAllData]);
// 页面聚焦时的数据加载逻辑
useFocusEffect(
React.useCallback(() => {
// 页面聚焦时加载数据,使用缓存机制避免频繁请求
console.log('页面聚焦,检查是否需要刷新数据...');
loadAllData(currentSelectedDate);
}, [loadAllData, currentSelectedDate])
);
// AppState 监听:应用从后台返回前台时的处理
useEffect(() => {
let appStateChangeTimeout: number;
const handleAppStateChange = (nextAppState: string) => {
if (nextAppState === 'active') {
// 应用从后台返回前台,刷新当前选中日期的数据
console.log('应用从后台返回前台,刷新统计数据...');
loadAllData();
// 延迟执行,避免与 useFocusEffect 重复触发
appStateChangeTimeout = setTimeout(() => {
console.log('应用从后台返回前台,强制刷新统计数据...');
// 从后台返回时强制刷新数据
loadAllData(currentSelectedDate, true);
}, 500);
}
};
@@ -288,19 +408,28 @@ export default function ExploreScreen() {
return () => {
subscription?.remove();
if (appStateChangeTimeout) {
clearTimeout(appStateChangeTimeout);
}
};
}, [loadAllData]);
}, [loadAllData, currentSelectedDate]);
useEffect(() => {
// 注册任务
// 注册后台任务 - 只处理健康数据和压力检查
registerTask({
id: 'health-data-task',
name: 'health-data-task',
handler: async () => {
try {
await loadHealthData();
console.log('后台任务:更新健康数据和检查压力水平...');
// 后台任务只更新健康数据,强制刷新以获取最新数据
await loadHealthData(undefined, true);
checkStressLevelAndNotify()
// 执行压力检查
await checkStressLevelAndNotify();
// 执行喝水目标检查
await checkWaterGoalAndNotify();
} catch (error) {
console.error('健康数据任务执行失败:', error);
}
@@ -382,12 +511,55 @@ export default function ExploreScreen() {
}
}, []);
// 日期点击时,加载对应日期数据
const onSelectDate = (index: number, date: Date) => {
setSelectedIndex(index);
loadAllData(date);
// 检查喝水目标并发送通知
const checkWaterGoalAndNotify = React.useCallback(async () => {
try {
console.log('开始检查喝水目标完成情况...');
// 获取最新的喝水统计数据
if (!todayWaterStats || !todayWaterStats.dailyGoal || todayWaterStats.dailyGoal <= 0) {
console.log('没有设置喝水目标或目标无效,跳过喝水检查');
return;
}
// 获取用户名
const userName = userProfile?.name || '朋友';
const currentHour = new Date().getHours();
// 构造今日统计数据
const waterStatsForCheck = {
totalAmount: todayWaterStats.totalAmount || 0,
dailyGoal: todayWaterStats.dailyGoal,
completionRate: todayWaterStats.completionRate || 0
};
// 调用喝水通知检查函数
const notificationSent = await WaterNotificationHelpers.checkWaterGoalAndNotify(
userName,
waterStatsForCheck,
currentHour
);
if (notificationSent) {
console.log('喝水提醒通知已发送');
} else {
console.log('无需发送喝水提醒通知');
}
} catch (error) {
console.error('检查喝水目标失败:', error);
}
}, [todayWaterStats, userProfile]);
// 日期点击时,加载对应日期数据
const onSelectDate = React.useCallback((index: number, date: Date) => {
setSelectedIndex(index);
console.log('日期切换,加载数据...', date);
// 日期切换时不强制刷新,依赖缓存机制减少不必要的请求
// loadAllData 内部已经实现了防抖,无需额外防抖处理
loadAllData(date, false);
}, [loadAllData]);
return (
<View style={styles.container}>

14
package-lock.json generated
View File

@@ -18,6 +18,7 @@
"@react-navigation/native": "^7.1.6",
"@reduxjs/toolkit": "^2.8.2",
"@sentry/react-native": "^6.20.0",
"@types/lodash": "^4.17.20",
"cos-js-sdk-v5": "^1.6.0",
"dayjs": "^1.11.13",
"expo": "~53.0.20",
@@ -39,6 +40,7 @@
"expo-system-ui": "~5.0.10",
"expo-task-manager": "^13.1.6",
"expo-web-browser": "~14.2.0",
"lodash": "^4.17.21",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.5",
@@ -3853,6 +3855,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.20",
"resolved": "https://mirrors.tencent.com/npm/@types/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.2.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
@@ -9575,6 +9583,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://mirrors.tencent.com/npm/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",

View File

@@ -22,6 +22,7 @@
"@react-navigation/native": "^7.1.6",
"@reduxjs/toolkit": "^2.8.2",
"@sentry/react-native": "^6.20.0",
"@types/lodash": "^4.17.20",
"cos-js-sdk-v5": "^1.6.0",
"dayjs": "^1.11.13",
"expo": "~53.0.20",
@@ -43,6 +44,7 @@
"expo-system-ui": "~5.0.10",
"expo-task-manager": "^13.1.6",
"expo-web-browser": "~14.2.0",
"lodash": "^4.17.21",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.5",

View File

@@ -81,6 +81,78 @@ export const createNotificationCheckTask = (): BackgroundTask => ({
},
});
// 喝水提醒检查任务
export const createWaterReminderTask = (): BackgroundTask => ({
id: 'water-reminder-task',
name: '喝水提醒检查任务',
handler: async (data?: any) => {
console.log('开始执行喝水提醒检查任务');
try {
// 导入必要的模块
const { WaterNotificationHelpers } = await import('@/utils/notificationHelpers');
const { getTodayWaterStats } = await import('@/services/waterRecords');
const AsyncStorage = (await import('@react-native-async-storage/async-storage')).default;
// 获取用户信息
const userProfileJson = await AsyncStorage.getItem('@user_profile');
const userProfile = userProfileJson ? JSON.parse(userProfileJson) : null;
const userName = userProfile?.name || '朋友';
// 检查时间限制早上9点以前和晚上9点以后不通知
const currentHour = new Date().getHours();
if (currentHour < 9 || currentHour >= 21) {
console.log(`当前时间${currentHour}不在通知时间范围内9:00-21:00跳过喝水提醒检查`);
return;
}
// 获取今日喝水统计数据
let todayStats;
try {
todayStats = await getTodayWaterStats();
} catch (error) {
console.log('获取喝水统计数据失败,可能用户未登录或无网络连接:', error);
return;
}
if (!todayStats || !todayStats.dailyGoal || todayStats.dailyGoal <= 0) {
console.log('没有设置喝水目标或目标无效,跳过喝水检查');
return;
}
// 构造今日统计数据
const waterStatsForCheck = {
totalAmount: todayStats.totalAmount || 0,
dailyGoal: todayStats.dailyGoal,
completionRate: todayStats.completionRate || 0
};
// 调用喝水通知检查函数
const notificationSent = await WaterNotificationHelpers.checkWaterGoalAndNotify(
userName,
waterStatsForCheck,
currentHour
);
if (notificationSent) {
console.log('喝水提醒通知已发送');
} else {
console.log('无需发送喝水提醒通知');
}
console.log('喝水提醒检查任务执行完成');
} catch (error) {
console.error('喝水提醒检查任务执行失败:', error);
throw error;
}
},
options: {
minimumInterval: 60, // 60分钟最小间隔
stopOnTerminate: false,
startOnBoot: true,
},
});
// 示例任务:缓存清理任务
export const createCacheCleanupTask = (): BackgroundTask => ({
id: 'cache-cleanup-task',
@@ -142,6 +214,7 @@ export const registerDefaultTasks = async (): Promise<void> => {
createDataSyncTask(),
createHealthDataUpdateTask(),
createNotificationCheckTask(),
createWaterReminderTask(),
createCacheCleanupTask(),
createUserAnalyticsTask(),
];

View File

@@ -170,6 +170,13 @@ export class NotificationService {
if (data?.url) {
router.push(data.url as any);
}
} else if (data?.type === 'water_reminder' || data?.type === 'regular_water_reminder') {
// 处理喝水提醒通知
console.log('用户点击了喝水提醒通知', data);
// 跳转到统计页面查看喝水进度
if (data?.url) {
router.push(data.url as any);
}
}
}
@@ -446,6 +453,8 @@ export const NotificationTypes = {
LUNCH_REMINDER: 'lunch_reminder',
DINNER_REMINDER: 'dinner_reminder',
MOOD_REMINDER: 'mood_reminder',
WATER_REMINDER: 'water_reminder',
REGULAR_WATER_REMINDER: 'regular_water_reminder',
} as const;
// 便捷方法

View File

@@ -638,6 +638,210 @@ export class MoodNotificationHelpers {
}
}
/**
* 喝水相关的通知辅助函数
*/
export class WaterNotificationHelpers {
/**
* 检查喝水目标完成情况并发送提醒
* @param userName 用户名
* @param todayStats 今日喝水统计数据
* @param currentHour 当前小时(用于时间限制检查)
* @returns 是否发送了通知
*/
static async checkWaterGoalAndNotify(
userName: string,
todayStats: { totalAmount: number; dailyGoal: number; completionRate: number },
currentHour: number = new Date().getHours()
): Promise<boolean> {
try {
// 检查时间限制早上9点以前和晚上9点以后不通知
if (currentHour < 9 || currentHour >= 21) {
console.log(`当前时间${currentHour}不在通知时间范围内9:00-21:00跳过喝水提醒`);
return false;
}
// 检查喝水目标是否已达成
if (todayStats.completionRate >= 100) {
console.log('喝水目标已达成,无需发送提醒');
return false;
}
// 检查是否在过去2小时内已经发送过喝水提醒避免重复打扰
const lastNotificationKey = '@last_water_notification';
const AsyncStorage = (await import('@react-native-async-storage/async-storage')).default;
const lastNotificationTime = await AsyncStorage.getItem(lastNotificationKey);
const now = new Date().getTime();
const twoHoursAgo = now - (2 * 60 * 60 * 1000); // 2小时前
if (lastNotificationTime && parseInt(lastNotificationTime) > twoHoursAgo) {
console.log('2小时内已发送过喝水提醒跳过本次通知');
return false;
}
// 计算还需要喝多少水
const remainingAmount = todayStats.dailyGoal - todayStats.totalAmount;
const completionPercentage = Math.round(todayStats.completionRate);
// 根据完成度生成不同的提醒消息
let title = '💧 该喝水啦!';
let body = '';
if (completionPercentage < 30) {
// 完成度低于30%
const encouragingMessages = [
`${userName},今天才喝了${completionPercentage}%的水哦!还需要${Math.round(remainingAmount)}ml记得多补水身体会感谢您的💙`,
`${userName},水分补充进度${completionPercentage}%,再喝${Math.round(remainingAmount)}ml就更健康啦来一大杯清水吧🚰`,
`${userName},喝水进度才${completionPercentage}%呢~身体需要更多水分,还差${Math.round(remainingAmount)}ml一起加油`,
];
body = encouragingMessages[Math.floor(Math.random() * encouragingMessages.length)];
} else if (completionPercentage < 60) {
// 完成度30-60%
const moderateMessages = [
`${userName},喝水进度${completionPercentage}%,还需要${Math.round(remainingAmount)}ml哦保持这个节奏您做得很棒👍`,
`${userName},水分补充已完成${completionPercentage}%,再来${Math.round(remainingAmount)}ml就达标了继续保持💪`,
`${userName},今日饮水${completionPercentage}%完成!距离目标还有${Math.round(remainingAmount)}ml加把劲🌊`,
];
body = moderateMessages[Math.floor(Math.random() * moderateMessages.length)];
} else {
// 完成度60-99%
const almostDoneMessages = [
`${userName},喝水进度${completionPercentage}%,太棒了!最后${Math.round(remainingAmount)}ml就达成目标啦🎉`,
`${userName},已经完成${completionPercentage}%了!还有${Math.round(remainingAmount)}ml就成功了您很快就能达成今天的目标🏆`,
`${userName},水分补充进度${completionPercentage}%,就差最后一点点!再喝${Math.round(remainingAmount)}ml就胜利了🥳`,
];
body = almostDoneMessages[Math.floor(Math.random() * almostDoneMessages.length)];
}
// 发送通知
const notificationId = await notificationService.sendImmediateNotification({
title,
body,
data: {
type: 'water_reminder',
completionRate: completionPercentage,
remainingAmount: Math.round(remainingAmount),
dailyGoal: todayStats.dailyGoal,
currentAmount: todayStats.totalAmount,
url: '/statistics' // 跳转到统计页面查看详情
},
sound: true,
priority: 'normal',
});
// 记录通知发送时间
await AsyncStorage.setItem(lastNotificationKey, now.toString());
console.log(`喝水提醒通知已发送ID: ${notificationId},完成度: ${completionPercentage}%`);
return true;
} catch (error) {
console.error('检查喝水目标并发送通知失败:', error);
return false;
}
}
/**
* 发送立即喝水提醒
* @param userName 用户名
* @param message 自定义消息(可选)
*/
static async sendWaterReminder(userName: string, message?: string) {
const defaultMessage = `${userName},记得要多喝水哦!保持身体水分充足很重要~💧`;
return notificationService.sendImmediateNotification({
title: '💧 喝水提醒',
body: message || defaultMessage,
data: {
type: 'water_reminder',
url: '/statistics'
},
sound: true,
priority: 'normal',
});
}
/**
* 安排定期喝水提醒每2小时一次在9:00-21:00之间
* @param userName 用户名
* @returns 通知ID数组
*/
static async scheduleRegularWaterReminders(userName: string): Promise<string[]> {
try {
const notificationIds: string[] = [];
// 检查是否已经存在定期喝水提醒
const existingNotifications = await notificationService.getAllScheduledNotifications();
const existingWaterReminders = existingNotifications.filter(
notification =>
notification.content.data?.type === 'regular_water_reminder' &&
notification.content.data?.isRegularReminder === true
);
if (existingWaterReminders.length > 0) {
console.log('定期喝水提醒已存在,跳过重复注册');
return existingWaterReminders.map(n => n.identifier);
}
// 创建多个时间点的喝水提醒9:00-21:00每2小时一次
const reminderHours = [9, 11, 13, 15, 17, 19, 21];
for (const hour of reminderHours) {
const notificationId = await notificationService.scheduleCalendarRepeatingNotification(
{
title: '💧 定时喝水提醒',
body: `${userName},该喝水啦!记得补充水分,保持身体健康~`,
data: {
type: 'regular_water_reminder',
isRegularReminder: true,
reminderHour: hour,
url: '/statistics'
},
sound: true,
priority: 'normal',
},
{
type: 'DAILY' as any,
hour: hour,
minute: 0,
}
);
notificationIds.push(notificationId);
console.log(`已安排${hour}:00的定期喝水提醒通知ID: ${notificationId}`);
}
console.log(`定期喝水提醒设置完成,共${notificationIds.length}个通知`);
return notificationIds;
} catch (error) {
console.error('设置定期喝水提醒失败:', error);
throw error;
}
}
/**
* 取消所有喝水提醒
*/
static async cancelAllWaterReminders(): Promise<void> {
try {
const notifications = await notificationService.getAllScheduledNotifications();
for (const notification of notifications) {
if (notification.content.data?.type === 'water_reminder' ||
notification.content.data?.type === 'regular_water_reminder') {
await notificationService.cancelNotification(notification.identifier);
console.log('已取消喝水提醒:', notification.identifier);
}
}
} catch (error) {
console.error('取消喝水提醒失败:', error);
throw error;
}
}
}
/**
* 通用通知辅助函数
*/
@@ -805,4 +1009,38 @@ export const NotificationTemplates = {
};
},
},
water: {
reminder: (userName: string, completionRate: number, remainingAmount: number) => ({
title: '💧 该喝水啦!',
body: `${userName},今日喝水进度${completionRate}%,还需要${Math.round(remainingAmount)}ml记得补充水分`,
data: {
type: 'water_reminder',
completionRate,
remainingAmount: Math.round(remainingAmount),
url: '/statistics'
},
sound: true,
priority: 'normal' as const,
}),
regular: (userName: string) => ({
title: '💧 定时喝水提醒',
body: `${userName},该喝水啦!记得补充水分,保持身体健康~`,
data: {
type: 'regular_water_reminder',
url: '/statistics'
},
sound: true,
priority: 'normal' as const,
}),
achievement: (userName: string) => ({
title: '🎉 喝水目标达成!',
body: `${userName},恭喜您完成了今天的喝水目标!继续保持健康的饮水习惯~`,
data: {
type: 'water_achievement',
url: '/statistics'
},
sound: true,
priority: 'high' as const,
}),
},
};