feat: 增强通知功能及用户体验

- 在 Bootstrapper 组件中新增通知服务初始化逻辑,注册每日午餐提醒
- 在 CoachScreen 中优化欢迎消息生成逻辑,整合用户配置文件数据
- 更新 GoalsScreen 组件,优化目标创建时的通知设置逻辑
- 在 NotificationTest 组件中添加调试通知状态功能,提升开发便利性
- 新增 NutritionNotificationHelpers 中的午餐提醒功能,支持每日提醒设置
- 更新相关文档,详细描述新功能和使用方法
This commit is contained in:
richarjiang
2025-08-26 09:56:23 +08:00
parent e6bbda9d0f
commit 7f2afdf671
8 changed files with 472 additions and 185 deletions

View File

@@ -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() {
}}
>
<View style={styles.headerLeft}>
<Text style={[styles.headerTitle, { color: theme.text }]}>{botName}</Text>
<Text style={[styles.headerTitle, { color: theme.text }]}></Text>
{/* 使用次数显示 */}
<TouchableOpacity

View File

@@ -118,10 +118,6 @@ export default function GoalsScreen() {
// 处理错误提示
useEffect(() => {
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);
// 通知创建失败不影响目标创建的成功

View File

@@ -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'),
});