diff --git a/app/(tabs)/coach.tsx b/app/(tabs)/coach.tsx index 1bec07f..455c6ea 100644 --- a/app/(tabs)/coach.tsx +++ b/app/(tabs)/coach.tsx @@ -32,6 +32,8 @@ import { useCosUpload } from '@/hooks/useCosUpload'; import { deleteConversation, getConversationDetail, listConversations, type AiConversationListItem } from '@/services/aiCoach'; import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession'; import { api, getAuthToken, postTextStream } from '@/services/api'; +import { selectLatestMoodRecordByDate } from '@/store/moodSlice'; +import { generateWelcomeMessage, hasRecordedMoodToday } from '@/utils/welcomeMessage'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import { ActionSheet } from '../../components/ui/ActionSheet'; @@ -170,109 +172,18 @@ export default function CoachScreen() { const userProfile = useAppSelector((s) => s.user?.profile); const { upload } = useCosUpload(); - // 生成个性化欢迎消息 - const generateWelcomeMessage = useCallback(() => { - const hour = new Date().getHours(); - const name = userProfile?.name || '朋友'; - const botName = (params?.name || 'Seal').toString(); + // 获取今日心情记录 + const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format + const todayMoodRecord = useAppSelector(selectLatestMoodRecordByDate(today)); + const hasRecordedMoodTodayValue = hasRecordedMoodToday(todayMoodRecord?.checkinDate); - // 时段问候 - let timeGreeting = ''; - if (hour >= 5 && hour < 9) { - timeGreeting = '早上好'; - } else if (hour >= 9 && hour < 12) { - timeGreeting = '上午好'; - } else if (hour >= 12 && hour < 14) { - timeGreeting = '中午好'; - } else if (hour >= 14 && hour < 18) { - timeGreeting = '下午好'; - } else if (hour >= 18 && hour < 22) { - timeGreeting = '晚上好'; - } else { - timeGreeting = '夜深了'; - } - - // 欢迎消息模板 - const welcomeMessages = [ - { - condition: () => hour >= 5 && hour < 9, - messages: [ - `${timeGreeting},${name}!🐳 我是你的小海豹${botName},新的一天开始啦!让我们一起游向健康的目标吧~`, - `${timeGreeting}!🌅 早晨的阳光真好呢,我是${botName},你的专属小海豹健康伙伴!要不要先制定今天的健康计划呢?`, - `${timeGreeting},${name}!🐋 小海豹${botName}来报到啦!今天想从哪个方面开始我们的健康之旅呢?营养、运动还是生活管理,我都可以帮你哦~` - ] - }, - { - condition: () => hour >= 9 && hour < 12, - messages: [ - `${timeGreeting},${name}!🐳 上午是身体最活跃的时候呢,小海豹${botName}在这里为你加油!有什么健康目标需要我帮你规划吗?`, - `${timeGreeting}!☀️ 工作忙碌的上午,别忘了给身体一些关爱哦~我是你的小海豹${botName},随时准备为你提供营养建议和运动指导!`, - `${timeGreeting},${name}!🐋 作为你的小海豹伙伴${botName},我想说:每一个健康的选择都在让我们的身体更棒呢!今天想从哪个方面开始呢?` - ] - }, - { - condition: () => hour >= 12 && hour < 14, - messages: [ - `${timeGreeting},${name}!🍽️ 午餐时间到啦!小海豹${botName}提醒你,合理的营养搭配能让下午充满能量哦~`, - `${timeGreeting}!🌊 忙碌的上午结束了,该给身体补充能量啦!我是你的小海豹${botName},无论是饮食调整还是运动安排,都可以找我商量哦~`, - `${timeGreeting},${name}!🐳 午间时光,小海豹${botName}建议你关注饮食均衡,也要适度放松一下呢~` - ] - }, - { - condition: () => hour >= 14 && hour < 18, - messages: [ - `${timeGreeting},${name}!🌊 下午是运动的黄金时段呢!小海豹${botName}可以为你制定个性化的健身计划,让我们一起游向更好的身材吧~`, - `${timeGreeting}!🐋 午后时光,正是关注健康的好时机!我是你的小海豹${botName},从营养到运动,我都能为你提供贴心指导哦~`, - `${timeGreeting},${name}!🐳 下午时光,身心健康同样重要呢!作为你的小海豹${botName},我在这里支持你的每一个健康目标~` - ] - }, - { - condition: () => hour >= 18 && hour < 22, - messages: [ - `${timeGreeting},${name}!🌙 忙碌了一天,现在是放松身心的好时候呢!小海豹${botName}可以为你提供放松建议和恢复方案哦~`, - `${timeGreeting}!🌊 夜幕降临,这是一天中最适合总结的时刻!我是你的小海豹${botName},让我们一起回顾今天的健康表现,规划明天的目标吧~`, - `${timeGreeting},${name}!🐳 晚间时光属于你自己,也是关爱身体的珍贵时间!作为你的小海豹${botName},我想陪你聊聊如何更好地管理健康生活呢~` - ] - }, - { - condition: () => hour >= 22 || hour < 5, - messages: [ - `${timeGreeting},${name}!🌙 优质睡眠是健康的基石呢!小海豹${botName}提醒你,如果需要睡眠优化建议,随时可以问我哦~`, - `夜深了,${name}。🌊 充足的睡眠对身体恢复很重要呢!我是你的小海豹${botName},有什么关于睡眠健康的问题都可以咨询我~`, - `夜深了,愿你能拥有甜甜的睡眠。🐳 我是你的小海豹${botName},明天我们继续在健康管理的海洋里同行。晚安,${name}!` - ] - } - ]; - - // 特殊情况的消息 - const specialMessages = [ - { - condition: () => !userProfile?.weight && !userProfile?.height, - message: `你好,${name}!🐳 我是你的小海豹${botName}!我注意到你还没有完善健康档案呢,不如先聊聊你的健康目标和身体状况,这样小海豹就能为你制定更贴心的健康方案啦~` - }, - { - condition: () => userProfile && (!userProfile.pilatesPurposes || userProfile.pilatesPurposes.length === 0), - message: `${timeGreeting},${name}!🐋 作为你的小海豹${botName},我想更好地了解你的健康需求呢!告诉我你希望在营养摄入、身材管理、健身锻炼或生活管理方面实现什么目标吧~` - } - ]; - - // 检查特殊情况 - for (const special of specialMessages) { - if (special.condition()) { - return special.message; - } - } - - // 根据时间选择合适的消息组 - const timeGroup = welcomeMessages.find(group => group.condition()); - if (timeGroup) { - const messages = timeGroup.messages; - return messages[Math.floor(Math.random() * messages.length)]; - } - - // 默认消息 - return `你好,我是你的小海豹${botName}!🐳 可以向我咨询营养摄入、身材管理、健身锻炼、生活管理等各方面的健康问题哦~`; - }, [userProfile, params?.name]); + // 转换用户配置文件数据类型以匹配欢迎消息函数的需求 + const transformedUserProfile = userProfile ? { + name: userProfile.name, + weight: userProfile.weight ? Number(userProfile.weight) : undefined, + height: userProfile.height ? Number(userProfile.height) : undefined, + pilatesPurposes: userProfile.pilatesPurposes + } : undefined; const chips = useMemo(() => [ // { key: 'posture', label: '体态评估', action: () => router.push('/ai-posture-assessment') }, @@ -317,15 +228,35 @@ export default function CoachScreen() { const timer = setTimeout(scrollToEnd, 100); return () => clearTimeout(timer); }, []); + // 使用 ref 存储最新值以避免依赖项导致的死循环 + const latestValuesRef = useRef({ + transformedUserProfile, + paramsName: params?.name || 'Seal', + hasRecordedMoodTodayValue + }); + + // 更新 ref 的值 + useEffect(() => { + latestValuesRef.current = { + transformedUserProfile, + paramsName: params?.name || 'Seal', + hasRecordedMoodTodayValue + }; + }, [transformedUserProfile, params?.name, hasRecordedMoodTodayValue]); + // 初始化欢迎消息 const initializeWelcomeMessage = useCallback(() => { + const { transformedUserProfile, paramsName, hasRecordedMoodTodayValue } = latestValuesRef.current; const welcomeMessage: ChatMessage = { id: 'm_welcome', role: 'assistant', - content: generateWelcomeMessage(), + content: generateWelcomeMessage({ + userProfile: transformedUserProfile, + hasRecordedMoodToday: hasRecordedMoodTodayValue + }), }; setMessages([welcomeMessage]); - }, [generateWelcomeMessage]); + }, []); // 空依赖项,通过 ref 获取最新值 // 启动页面时尝试恢复当次应用会话缓存 useEffect(() => { @@ -417,7 +348,7 @@ export default function CoachScreen() { showSub = Keyboard.addListener('keyboardWillChangeFrame', (e: any) => { try { if (e?.endCoordinates?.height) { - const height = Math.max(0, e.endCoordinates.height); + const height = Math.max(0, e.endCoordinates.height - 100); setKeyboardOffset(height); } } catch (error) { @@ -570,7 +501,14 @@ export default function CoachScreen() { attachments: undefined, })); setConversationId(detail.conversationId); - setMessages(mapped.length ? mapped : [{ id: 'm_welcome', role: 'assistant', content: generateWelcomeMessage() }]); + setMessages(mapped.length ? mapped : [{ + id: 'm_welcome', + role: 'assistant', + content: generateWelcomeMessage({ + userProfile: transformedUserProfile, + hasRecordedMoodToday: hasRecordedMoodTodayValue + }) + }]); setHistoryVisible(false); setTimeout(scrollToEnd, 0); } catch (e) { @@ -587,7 +525,14 @@ export default function CoachScreen() { await deleteConversation(id); if (conversationId === id) { setConversationId(undefined); - setMessages([{ id: 'm_welcome', role: 'assistant', content: generateWelcomeMessage() }]); + setMessages([{ + id: 'm_welcome', + role: 'assistant', + content: generateWelcomeMessage({ + userProfile: transformedUserProfile, + hasRecordedMoodToday: hasRecordedMoodTodayValue + }) + }]); } await refreshHistory(historyPage); } catch (e) { @@ -1856,7 +1801,7 @@ export default function CoachScreen() { }} > - {botName} + 海豹管家 {/* 使用次数显示 */} { - console.log('tasksError', tasksError); - console.log('createError', createError); - console.log('completeError', completeError); - console.log('skipError', skipError); if (tasksError) { Alert.alert('错误', tasksError); @@ -154,24 +150,26 @@ export default function GoalsScreen() { setShowCreateModal(false); // 获取用户名 - const userName = userProfile?.name || '小海豹'; + const userName = userProfile?.name || '主人'; // 创建目标成功后,设置定时推送 try { - const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( - { - title: goalData.title, - repeatType: goalData.repeatType, - frequency: goalData.frequency, - hasReminder: goalData.hasReminder, - reminderTime: goalData.reminderTime, - customRepeatRule: goalData.customRepeatRule, - startTime: goalData.startTime, - }, - userName - ); + if (goalData.hasReminder) { + const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( + { + title: goalData.title, + repeatType: goalData.repeatType, + frequency: goalData.frequency, + hasReminder: goalData.hasReminder, + reminderTime: goalData.reminderTime, + customRepeatRule: goalData.customRepeatRule, + startTime: goalData.startTime, + }, + userName + ); + console.log(`目标"${goalData.title}"的定时推送已创建,通知ID:`, notificationIds); + } - console.log(`目标"${goalData.title}"的定时推送已创建,通知ID:`, notificationIds); } catch (notificationError) { console.error('创建目标定时推送失败:', notificationError); // 通知创建失败不影响目标创建的成功 diff --git a/app/_layout.tsx b/app/_layout.tsx index 7cc00e5..a128e5e 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -7,11 +7,12 @@ import 'react-native-reanimated'; import PrivacyConsentModal from '@/components/PrivacyConsentModal'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; -import { useColorScheme } from '@/hooks/useColorScheme'; import { clearAiCoachSessionCache } from '@/services/aiCoachSession'; import { backgroundTaskManager } from '@/services/backgroundTaskManager'; +import { notificationService } from '@/services/notifications'; import { store } from '@/store'; import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice'; +import { NutritionNotificationHelpers } from '@/utils/notificationHelpers'; import React from 'react'; import RNExitApp from 'react-native-exit-app'; @@ -21,7 +22,7 @@ import { Provider } from 'react-redux'; function Bootstrapper({ children }: { children: React.ReactNode }) { const dispatch = useAppDispatch(); - const { privacyAgreed } = useAppSelector((state) => state.user); + const { privacyAgreed, profile } = useAppSelector((state) => state.user); const [showPrivacyModal, setShowPrivacyModal] = React.useState(false); const [userDataLoaded, setUserDataLoaded] = React.useState(false); @@ -34,15 +35,30 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { const initializeBackgroundTasks = async () => { try { await backgroundTaskManager.initialize(); - console.log('后台任务管理器初始化成功'); } catch (error) { console.error('后台任务管理器初始化失败:', error); } }; + const initializeNotifications = async () => { + try { + // 初始化通知服务 + await notificationService.initialize(); + + // 只有在用户数据加载完成后且用户名存在时才注册午餐提醒 + if (userDataLoaded && profile?.name) { + await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name); + console.log('通知服务初始化成功,午餐提醒已注册'); + } + } catch (error) { + console.error('通知服务初始化失败:', error); + } + }; + loadUserData(); initializeBackgroundTasks(); + initializeNotifications(); // 冷启动时清空 AI 教练会话缓存 clearAiCoachSessionCache(); }, [dispatch]); @@ -54,6 +70,23 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { } }, [userDataLoaded, privacyAgreed]); + // 当用户数据加载完成且用户名存在时,注册午餐提醒 + React.useEffect(() => { + const registerLunchReminder = async () => { + if (userDataLoaded && profile?.name) { + try { + await notificationService.initialize(); + await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name); + console.log('午餐提醒已注册'); + } catch (error) { + console.error('注册午餐提醒失败:', error); + } + } + }; + + registerLunchReminder(); + }, [userDataLoaded, profile?.name]); + const handlePrivacyAgree = () => { dispatch(setPrivacyAgreed()); setShowPrivacyModal(false); @@ -76,7 +109,6 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { } export default function RootLayout() { - const colorScheme = useColorScheme(); const [loaded] = useFonts({ SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), }); diff --git a/components/NotificationTest.tsx b/components/NotificationTest.tsx index a7f2356..b02bc9b 100644 --- a/components/NotificationTest.tsx +++ b/components/NotificationTest.tsx @@ -24,6 +24,7 @@ export const NotificationTest: React.FC = () => { sendWorkoutReminder, sendGoalAchievement, sendMoodCheckinReminder, + debugNotificationStatus, } = useNotifications(); const [scheduledNotifications, setScheduledNotifications] = useState([]); @@ -132,6 +133,15 @@ export const NotificationTest: React.FC = () => { } }; + const handleDebugNotificationStatus = async () => { + try { + await debugNotificationStatus(); + Alert.alert('调试完成', '请查看控制台输出'); + } catch (error) { + Alert.alert('错误', '调试失败'); + } + }; + return ( @@ -203,6 +213,13 @@ export const NotificationTest: React.FC = () => { 获取已安排通知 + + 调试通知状态 + + Promise; sendGoalAchievement: (title: string, body: string) => Promise; sendMoodCheckinReminder: (title: string, body: string, date?: Date) => Promise; + debugNotificationStatus: () => Promise; } export const useNotifications = (): UseNotificationsReturn => { @@ -134,6 +135,10 @@ export const useNotifications = (): UseNotificationsReturn => { } }, []); + const debugNotificationStatus = useCallback(async () => { + return notificationService.debugNotificationStatus(); + }, []); + // 组件挂载时自动初始化 useEffect(() => { initialize(); @@ -153,5 +158,6 @@ export const useNotifications = (): UseNotificationsReturn => { sendWorkoutReminder, sendGoalAchievement, sendMoodCheckinReminder, + debugNotificationStatus, }; }; diff --git a/services/notifications.ts b/services/notifications.ts index e4cdf3d..e440415 100644 --- a/services/notifications.ts +++ b/services/notifications.ts @@ -1,5 +1,4 @@ import * as Notifications from 'expo-notifications'; -import { Platform } from 'react-native'; // 配置通知处理方式 Notifications.setNotificationHandler({ @@ -15,17 +14,50 @@ Notifications.setNotificationHandler({ export interface NotificationData { title: string; body: string; - data?: Record; + data?: Record; sound?: boolean; priority?: 'default' | 'normal' | 'high'; vibrate?: number[]; } +interface DateTrigger { + type: Notifications.SchedulableTriggerInputTypes.DATE; + date: number; +} + +interface RepeatingTrigger { + type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL; + seconds: number; + repeats?: boolean; +} + +interface DailyTrigger { + type: Notifications.SchedulableTriggerInputTypes.DAILY; + hour: number; + minute: number; +} + +interface WeeklyTrigger { + type: Notifications.SchedulableTriggerInputTypes.WEEKLY; + hour: number; + minute: number; + weekday: number; +} + +interface MonthlyTrigger { + type: Notifications.SchedulableTriggerInputTypes.MONTHLY; + hour: number; + minute: number; + day: number; +} + +type CalendarTrigger = DailyTrigger | WeeklyTrigger | MonthlyTrigger; + export class NotificationService { private static instance: NotificationService; private isInitialized = false; - private constructor() {} + private constructor() { } public static getInstance(): NotificationService { if (!NotificationService.instance) { @@ -44,28 +76,32 @@ export class NotificationService { // 请求通知权限 const { status: existingStatus } = await Notifications.getPermissionsAsync(); let finalStatus = existingStatus; - + if (existingStatus !== 'granted') { const { status } = await Notifications.requestPermissionsAsync(); finalStatus = status; } - + if (finalStatus !== 'granted') { console.warn('推送通知权限未授予'); return; } // 获取推送令牌(用于远程推送,本地推送不需要) - if (Platform.OS !== 'web') { - const token = await Notifications.getExpoPushTokenAsync({ - projectId: 'your-project-id', // 需要替换为实际的Expo项目ID - }); - console.log('推送令牌:', token.data); - } + // if (Platform.OS !== 'web') { + // const token = await Notifications.getExpoPushTokenAsync({ + // projectId: 'your-project-id', // 需要替换为实际的Expo项目ID + // }); + // console.log('推送令牌:', token.data); + // } // 设置通知监听器 this.setupNotificationListeners(); - + + // 检查已存在的通知 + const existingNotifications = await this.getAllScheduledNotifications(); + console.log('已存在的通知数量:', existingNotifications.length); + this.isInitialized = true; console.log('推送通知服务初始化成功'); } catch (error) { @@ -96,7 +132,7 @@ export class NotificationService { private handleNotificationResponse(response: Notifications.NotificationResponse): void { const { notification } = response; const data = notification.request.content.data; - + // 根据通知类型处理不同的逻辑 if (data?.type === 'workout_reminder') { // 处理运动提醒 @@ -111,6 +147,11 @@ export class NotificationService { // 处理目标提醒通知 console.log('用户点击了目标提醒通知', data); // 这里可以添加导航到目标页面的逻辑 + } else if (data?.type === 'lunch_reminder') { + // 处理午餐提醒通知 + console.log('用户点击了午餐提醒通知', data); + // 这里可以添加导航到午餐记录页面的逻辑 + } } @@ -133,7 +174,7 @@ export class NotificationService { }, trigger: trigger || null, // null表示立即发送 }); - + console.log('本地通知已安排,ID:', notificationId); return notificationId; } catch (error) { @@ -167,10 +208,11 @@ export class NotificationService { vibrate: notification.vibrate, }, trigger: { + type: Notifications.SchedulableTriggerInputTypes.DATE, date: date.getTime(), - } as any, + } as DateTrigger, }); - + console.log('定时通知已安排,ID:', notificationId); return notificationId; } catch (error) { @@ -196,7 +238,7 @@ export class NotificationService { ): Promise { try { // 计算总秒数 - const totalSeconds = + const totalSeconds = (interval.seconds || 0) + (interval.minutes || 0) * 60 + (interval.hours || 0) * 3600 + @@ -219,11 +261,12 @@ export class NotificationService { vibrate: notification.vibrate, }, trigger: { + type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: totalSeconds, repeats: true, - } as any, + } as RepeatingTrigger, }); - + console.log('重复通知已安排,ID:', notificationId); return notificationId; } catch (error) { @@ -238,7 +281,7 @@ export class NotificationService { async scheduleCalendarRepeatingNotification( notification: NotificationData, options: { - type: 'daily' | 'weekly' | 'monthly'; + type: Notifications.SchedulableTriggerInputTypes; hour: number; minute: number; weekdays?: number[]; // 0-6,0为周日,仅用于weekly类型 @@ -246,38 +289,38 @@ export class NotificationService { } ): Promise { try { - let trigger: any; + let trigger: CalendarTrigger; switch (options.type) { - case 'daily': + case Notifications.SchedulableTriggerInputTypes.DAILY: trigger = { + type: Notifications.SchedulableTriggerInputTypes.DAILY, hour: options.hour, minute: options.minute, - repeats: true, }; break; - case 'weekly': + case Notifications.SchedulableTriggerInputTypes.WEEKLY: if (options.weekdays && options.weekdays.length > 0) { trigger = { + type: Notifications.SchedulableTriggerInputTypes.WEEKLY, hour: options.hour, minute: options.minute, weekday: options.weekdays[0], // Expo只支持单个weekday - repeats: true, }; } else { trigger = { + type: Notifications.SchedulableTriggerInputTypes.DAILY, hour: options.hour, minute: options.minute, - repeats: true, }; } break; - case 'monthly': + case Notifications.SchedulableTriggerInputTypes.MONTHLY: trigger = { + type: Notifications.SchedulableTriggerInputTypes.MONTHLY, hour: options.hour, minute: options.minute, day: options.dayOfMonth || 1, - repeats: true, }; break; default: @@ -295,7 +338,7 @@ export class NotificationService { }, trigger, }); - + console.log(`${options.type}重复通知已安排,ID:`, notificationId); return notificationId; } catch (error) { @@ -336,6 +379,7 @@ export class NotificationService { async getAllScheduledNotifications(): Promise { try { const notifications = await Notifications.getAllScheduledNotificationsAsync(); + return notifications; } catch (error) { console.error('获取已安排通知失败:', error); @@ -343,6 +387,7 @@ export class NotificationService { } } + /** * 获取通知权限状态 */ @@ -380,6 +425,7 @@ export const NotificationTypes = { MOOD_CHECKIN: 'mood_checkin', NUTRITION_REMINDER: 'nutrition_reminder', PROGRESS_UPDATE: 'progress_update', + LUNCH_REMINDER: 'lunch_reminder', } as const; // 便捷方法 @@ -391,7 +437,7 @@ export const sendWorkoutReminder = (title: string, body: string, date?: Date) => sound: true, priority: 'high', }; - + if (date) { return notificationService.scheduleNotificationAtDate(notification, date); } else { @@ -407,7 +453,7 @@ export const sendGoalAchievement = (title: string, body: string) => { sound: true, priority: 'high', }; - + return notificationService.sendImmediateNotification(notification); }; @@ -419,7 +465,7 @@ export const sendMoodCheckinReminder = (title: string, body: string, date?: Date sound: true, priority: 'normal', }; - + if (date) { return notificationService.scheduleNotificationAtDate(notification, date); } else { diff --git a/utils/notificationHelpers.ts b/utils/notificationHelpers.ts index 749a743..7450644 100644 --- a/utils/notificationHelpers.ts +++ b/utils/notificationHelpers.ts @@ -1,3 +1,4 @@ +import * as Notifications from 'expo-notifications'; import { NotificationData, notificationService } from '../services/notifications'; /** @@ -36,7 +37,7 @@ export class WorkoutNotificationHelpers { static async scheduleDailyWorkoutReminder(userName: string, hour: number = 9, minute: number = 0) { const reminderTime = new Date(); reminderTime.setHours(hour, minute, 0, 0); - + // 如果今天的时间已经过了,设置为明天 if (reminderTime.getTime() <= Date.now()) { reminderTime.setDate(reminderTime.getDate() + 1); @@ -105,12 +106,12 @@ export class GoalNotificationHelpers { ); } - /** - * 根据目标设置创建定时推送 - * @param goalData 目标数据 - * @param userName 用户名 - * @returns 通知ID数组 - */ + /** + * 根据目标设置创建定时推送 + * @param goalData 目标数据 + * @param userName 用户名 + * @returns 通知ID数组 + */ static async scheduleGoalNotifications( goalData: { title: string; @@ -137,13 +138,13 @@ export class GoalNotificationHelpers { try { // 解析提醒时间 const [hours, minutes] = goalData.reminderTime.split(':').map(Number); - + // 创建通知内容 const notification: NotificationData = { title: '目标提醒', body: `${userName},该完成您的目标"${goalData.title}"了!`, - data: { - type: 'goal_reminder', + data: { + type: 'goal_reminder', goalTitle: goalData.title, repeatType: goalData.repeatType, frequency: goalData.frequency @@ -159,7 +160,7 @@ export class GoalNotificationHelpers { const dailyId = await notificationService.scheduleCalendarRepeatingNotification( notification, { - type: 'daily', + type: Notifications.SchedulableTriggerInputTypes.DAILY, hour: hours, minute: minutes, } @@ -175,7 +176,7 @@ export class GoalNotificationHelpers { const weeklyId = await notificationService.scheduleCalendarRepeatingNotification( notification, { - type: 'weekly', + type: Notifications.SchedulableTriggerInputTypes.WEEKLY, hour: hours, minute: minutes, weekdays: [weekday], @@ -189,7 +190,7 @@ export class GoalNotificationHelpers { const weeklyId = await notificationService.scheduleCalendarRepeatingNotification( notification, { - type: 'weekly', + type: Notifications.SchedulableTriggerInputTypes.WEEKLY, hour: hours, minute: minutes, } @@ -206,7 +207,7 @@ export class GoalNotificationHelpers { const monthlyId = await notificationService.scheduleCalendarRepeatingNotification( notification, { - type: 'monthly', + type: Notifications.SchedulableTriggerInputTypes.MONTHLY, hour: hours, minute: minutes, dayOfMonth: dayOfMonth, @@ -220,7 +221,7 @@ export class GoalNotificationHelpers { const monthlyId = await notificationService.scheduleCalendarRepeatingNotification( notification, { - type: 'monthly', + type: Notifications.SchedulableTriggerInputTypes.MONTHLY, hour: hours, minute: minutes, dayOfMonth: 1, @@ -249,10 +250,10 @@ export class GoalNotificationHelpers { static async cancelGoalNotifications(goalTitle: string): Promise { try { const notifications = await notificationService.getAllScheduledNotifications(); - + for (const notification of notifications) { - if (notification.content.data?.type === 'goal_reminder' && - notification.content.data?.goalTitle === goalTitle) { + if (notification.content.data?.type === 'goal_reminder' && + notification.content.data?.goalTitle === goalTitle) { await notificationService.cancelNotification(notification.identifier); console.log(`已取消目标"${goalTitle}"的通知:${notification.identifier}`); } @@ -287,7 +288,7 @@ export class MoodNotificationHelpers { 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); @@ -323,6 +324,96 @@ export class NutritionNotificationHelpers { }); } + /** + * 安排每日午餐提醒 + * @param userName 用户名 + * @param hour 小时 (默认12点) + * @param minute 分钟 (默认0分) + * @returns 通知ID + */ + static async scheduleDailyLunchReminder( + userName: string, + hour: number = 12, + minute: number = 0 + ): Promise { + try { + // 检查是否已经存在午餐提醒 + const existingNotifications = await notificationService.getAllScheduledNotifications(); + + + console.log('existingNotifications', existingNotifications); + const existingLunchReminder = existingNotifications.find( + notification => + notification.content.data?.type === 'lunch_reminder' && + notification.content.data?.isDailyReminder === true + ); + + if (existingLunchReminder) { + console.log('午餐提醒已存在,跳过重复注册:', existingLunchReminder.identifier); + return existingLunchReminder.identifier; + } + + // 创建午餐提醒通知 + const notificationId = await notificationService.scheduleCalendarRepeatingNotification( + { + title: '午餐记录提醒', + body: `${userName},记得记录今天的午餐情况哦!`, + data: { + type: 'lunch_reminder', + isDailyReminder: true, + meal: '午餐' + }, + 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 sendLunchReminder(userName: string) { + return notificationService.sendImmediateNotification({ + title: '午餐记录提醒', + body: `${userName},记得记录今天的午餐情况哦!`, + data: { type: 'lunch_reminder', meal: '午餐' }, + sound: true, + priority: 'normal', + }); + } + + /** + * 取消午餐提醒 + */ + static async cancelLunchReminder(): Promise { + try { + const notifications = await notificationService.getAllScheduledNotifications(); + + for (const notification of notifications) { + if (notification.content.data?.type === 'lunch_reminder' && + notification.content.data?.isDailyReminder === true) { + await notificationService.cancelNotification(notification.identifier); + console.log('已取消午餐提醒:', notification.identifier); + } + } + } catch (error) { + console.error('取消午餐提醒失败:', error); + throw error; + } + } + /** * 安排营养记录提醒 */ @@ -339,7 +430,7 @@ export class NutritionNotificationHelpers { for (const mealTime of mealTimes) { const reminderTime = new Date(); reminderTime.setHours(mealTime.hour, mealTime.minute, 0, 0); - + // 如果今天的时间已经过了,设置为明天 if (reminderTime.getTime() <= Date.now()) { reminderTime.setDate(reminderTime.getDate() + 1); @@ -411,7 +502,7 @@ export class GeneralNotificationHelpers { */ static async cancelNotificationsByType(type: string) { const notifications = await notificationService.getAllScheduledNotifications(); - + for (const notification of notifications) { if (notification.content.data?.type === type) { await notificationService.cancelNotification(notification.identifier); @@ -424,7 +515,7 @@ export class GeneralNotificationHelpers { */ static async sendBatchNotifications(notifications: NotificationData[]) { const results = []; - + for (const notification of notifications) { try { const id = await notificationService.sendImmediateNotification(notification); @@ -433,7 +524,7 @@ export class GeneralNotificationHelpers { results.push({ success: false, error, notification }); } } - + return results; } } @@ -491,5 +582,12 @@ export const NotificationTemplates = { sound: true, priority: 'normal' as const, }), + lunch: (userName: string) => ({ + title: '午餐记录提醒', + body: `${userName},记得记录今天的午餐情况哦!`, + data: { type: 'lunch_reminder', meal: '午餐' }, + sound: true, + priority: 'normal' as const, + }), }, }; diff --git a/utils/welcomeMessage.ts b/utils/welcomeMessage.ts new file mode 100644 index 0000000..9225e04 --- /dev/null +++ b/utils/welcomeMessage.ts @@ -0,0 +1,142 @@ +import dayjs from 'dayjs'; + +// 用户配置文件类型 +type UserProfile = { + name?: string; + weight?: number; + height?: number; + pilatesPurposes?: string[]; +}; + +// 参数类型 +type GenerateWelcomeMessageParams = { + userProfile?: UserProfile; + hasRecordedMoodToday?: boolean; +}; + +/** + * 生成个性化欢迎消息 + * @param params 参数对象,包含用户配置和今日是否已记录心情 + * @returns 个性化欢迎消息字符串 + */ +export function generateWelcomeMessage(params: GenerateWelcomeMessageParams): string { + const { userProfile, hasRecordedMoodToday = false } = params; + const hour = new Date().getHours(); + const name = userProfile?.name || '朋友'; + + // 时段问候 + let timeGreeting = ''; + if (hour >= 5 && hour < 9) { + timeGreeting = '早上好'; + } else if (hour >= 9 && hour < 12) { + timeGreeting = '上午好'; + } else if (hour >= 12 && hour < 14) { + timeGreeting = '中午好'; + } else if (hour >= 14 && hour < 18) { + timeGreeting = '下午好'; + } else if (hour >= 18 && hour < 22) { + timeGreeting = '晚上好'; + } else { + timeGreeting = '夜深了'; + } + + // 欢迎消息模板 + const welcomeMessages = [ + { + condition: () => hour >= 5 && hour < 9, + messages: [ + `${timeGreeting},${name}!🐳 我是你的小海豹,新的一天开始啦!让我们一起游向健康的目标吧~`, + `${timeGreeting}!🌅 早晨的阳光真好呢,我是你的专属小海豹健康伙伴!要不要先制定今天的健康计划呢?`, + `${timeGreeting},${name}!🐋 小海豹来报到啦!今天想从哪个方面开始我们的健康之旅呢?营养、运动还是生活管理,我都可以帮你哦~` + ] + }, + { + condition: () => hour >= 9 && hour < 12, + messages: [ + `${timeGreeting},${name}!🐳 上午是身体最活跃的时候呢,小海豹在这里为你加油!有什么健康目标需要我帮你规划吗?`, + `${timeGreeting}!☀️ 工作忙碌的上午,别忘了给身体一些关爱哦~我是你的小海豹,随时准备为你提供营养建议和运动指导!`, + `${timeGreeting},${name}!🐋 作为你的小海豹伙伴,我想说:每一个健康的选择都在让我们的身体更棒呢!今天想从哪个方面开始呢?` + ] + }, + { + condition: () => hour >= 12 && hour < 14, + messages: [ + `${timeGreeting},${name}!🍽️ 午餐时间到啦!小海豹提醒你,合理的营养搭配能让下午充满能量哦~`, + `${timeGreeting}!🌊 忙碌的上午结束了,该给身体补充能量啦!我是你的小海豹,无论是饮食调整还是运动安排,都可以找我商量哦~`, + `${timeGreeting},${name}!🐳 午间时光,小海豹建议你关注饮食均衡,也要适度放松一下呢~` + ] + }, + { + condition: () => hour >= 14 && hour < 18, + messages: [ + `${timeGreeting},${name}!🌊 下午是运动的黄金时段呢!小海豹可以为你制定个性化的健身计划,让我们一起游向更好的身材吧~`, + `${timeGreeting}!🐋 午后时光,正是关注健康的好时机!我是你的小海豹,从营养到运动,我都能为你提供贴心指导哦~`, + `${timeGreeting},${name}!🐳 下午时光,身心健康同样重要呢!作为你的小海豹,我在这里支持你的每一个健康目标~` + ] + }, + { + condition: () => hour >= 18 && hour < 22, + messages: [ + `${timeGreeting},${name}!🌙 忙碌了一天,现在是放松身心的好时候呢!小海豹可以为你提供放松建议和恢复方案哦~`, + `${timeGreeting}!🌊 夜幕降临,这是一天中最适合总结的时刻!我是你的小海豹,让我们一起回顾今天的健康表现,规划明天的目标吧~`, + `${timeGreeting},${name}!🐳 晚间时光属于你自己,也是关爱身体的珍贵时间!作为你的小海豹,我想陪你聊聊如何更好地管理健康生活呢~` + ] + }, + { + condition: () => hour >= 22 || hour < 5, + messages: [ + `${timeGreeting},${name}!🌙 优质睡眠是健康的基石呢!小海豹提醒你,如果需要睡眠优化建议,随时可以问我哦~`, + `夜深了,${name}。🌊 充足的睡眠对身体恢复很重要呢!我是你的小海豹,有什么关于睡眠健康的问题都可以咨询我~`, + `夜深了,愿你能拥有甜甜的睡眠。🐳 我是你的小海豹,明天我们继续在健康管理的海洋里同行。晚安,${name}!` + ] + } + ]; + + // 特殊情况的消息 + const specialMessages = [ + { + condition: () => !userProfile?.weight && !userProfile?.height, + message: `你好,${name}!🐳 我是你的小海豹!我注意到你还没有完善健康档案呢,不如先聊聊你的健康目标和身体状况,这样小海豹就能为你制定更贴心的健康方案啦~` + }, + { + condition: () => userProfile && (!userProfile.pilatesPurposes || userProfile.pilatesPurposes.length === 0), + message: `${timeGreeting},${name}!🐋 作为你的小海豹,我想更好地了解你的健康需求呢!告诉我你希望在营养摄入、身材管理、健身锻炼或生活管理方面实现什么目标吧~` + }, + // 新增:晚上20点后且未记录心情的特殊提示 + { + condition: () => hour >= 20 && !hasRecordedMoodToday, + message: `${timeGreeting},${name}!🌙 夜深了,小海豹注意到你今天还没有记录心情呢。记录心情有助于更好地了解自己的情绪变化,对健康管理很有帮助哦~要不要现在记录一下今天的心情?` + } + ]; + + // 检查特殊情况 + for (const special of specialMessages) { + if (special.condition()) { + return special.message; + } + } + + // 根据时间选择合适的消息组 + const timeGroup = welcomeMessages.find(group => group.condition()); + if (timeGroup) { + const messages = timeGroup.messages; + return messages[Math.floor(Math.random() * messages.length)]; + } + + // 默认消息 + return `你好,我是你的小海豹!🐳 可以向我咨询营养摄入、身材管理、健身锻炼、生活管理等各方面的健康问题哦~`; +} + +/** + * 检查用户今天是否已经记录了心情 + * @param lastMoodDate 上次记录心情的日期 + * @returns 今天是否已记录心情 + */ +export function hasRecordedMoodToday(lastMoodDate?: string | Date): boolean { + if (!lastMoodDate) return false; + + const today = dayjs().format('YYYY-MM-DD'); + const lastMoodDay = dayjs(lastMoodDate).format('YYYY-MM-DD'); + + return today === lastMoodDay; +} \ No newline at end of file