feat: 更新应用名称为“Out Live”,删除推送通知使用指南和喝水记录API修复测试文档,优化饮水设置页面,添加登录状态检查

This commit is contained in:
2025-09-07 10:03:37 +08:00
parent 2e7daae519
commit aaa34a7a07
13 changed files with 105 additions and 550 deletions

View File

@@ -1,375 +0,0 @@
# 推送通知功能使用指南
## 快速开始
### 1. 安装依赖
推送通知功能已经集成到项目中,无需额外安装依赖。
### 2. 基本使用
在组件中使用推送通知功能:
```typescript
import { useNotifications } from '@/hooks/useNotifications';
function MyComponent() {
const { sendNotification, sendWorkoutReminder } = useNotifications();
const handleSendNotification = async () => {
await sendNotification({
title: '测试通知',
body: '这是一个测试通知',
sound: true,
priority: 'high'
});
};
const handleSendWorkoutReminder = async () => {
await sendWorkoutReminder('运动提醒', '该开始今天的普拉提训练了!');
};
return (
// 你的组件内容
);
}
```
### 3. 使用辅助函数
```typescript
import { WorkoutNotificationHelpers, GoalNotificationHelpers } from '@/utils/notificationHelpers';
// 发送运动开始提醒
await WorkoutNotificationHelpers.sendWorkoutStartReminder('张三');
// 发送目标达成通知
await GoalNotificationHelpers.sendGoalAchievementNotification('张三', '每周运动3次');
// 安排每日运动提醒
await WorkoutNotificationHelpers.scheduleDailyWorkoutReminder('张三', 9, 0); // 每天上午9点
```
## 功能特性
### ✅ 已实现功能
- [x] 立即发送通知
- [x] 定时发送通知
- [x] 重复发送通知
- [x] 通知权限管理
- [x] 通知点击处理
- [x] 通知取消功能
- [x] 多种通知类型支持
- [x] 通知模板系统
- [x] 批量通知发送
- [x] 通知状态查询
### 📱 支持的通知类型
1. **运动相关**
- 运动开始提醒
- 运动完成通知
- 每日运动提醒
2. **目标相关**
- 目标达成通知
- 目标进度更新
- 目标截止提醒
3. **心情相关**
- 心情打卡提醒
- 每日心情提醒
4. **营养相关**
- 营养记录提醒
- 三餐提醒
5. **通用通知**
- 欢迎通知
- 成就通知
- 系统维护通知
## 详细使用示例
### 1. 在运动页面中使用
```typescript
import { WorkoutNotificationHelpers } from '@/utils/notificationHelpers';
// 运动开始时
const handleWorkoutStart = async () => {
await WorkoutNotificationHelpers.sendWorkoutStartReminder(userName);
// 开始运动逻辑
};
// 运动完成时
const handleWorkoutComplete = async (duration: number) => {
await WorkoutNotificationHelpers.sendWorkoutCompleteNotification(userName, duration);
// 完成运动逻辑
};
```
### 2. 在目标页面中使用
```typescript
import { GoalNotificationHelpers } from '@/utils/notificationHelpers';
// 目标达成时
const handleGoalAchieved = async (goalName: string) => {
await GoalNotificationHelpers.sendGoalAchievementNotification(userName, goalName);
// 目标达成逻辑
};
// 目标进度更新时
const handleGoalProgress = async (goalName: string, progress: number) => {
if (progress >= 50 && progress < 100) {
await GoalNotificationHelpers.sendGoalProgressNotification(userName, goalName, progress);
}
};
```
### 3. 在心情页面中使用
```typescript
import { MoodNotificationHelpers } from '@/utils/notificationHelpers';
// 安排每日心情打卡提醒
const setupMoodReminder = async () => {
await MoodNotificationHelpers.scheduleDailyMoodReminder(userName, 20, 0); // 每天晚上8点
};
```
### 4. 在营养页面中使用
```typescript
import { NutritionNotificationHelpers } from '@/utils/notificationHelpers';
// 安排营养记录提醒
const setupNutritionReminders = async () => {
await NutritionNotificationHelpers.scheduleNutritionReminders(userName);
};
```
## 通知模板使用
```typescript
import { NotificationTemplates } from '@/utils/notificationHelpers';
import { notificationService } from '@/services/notifications';
// 使用模板发送通知
const sendWorkoutNotification = async (userName: string) => {
const notification = NotificationTemplates.workout.start(userName);
await notificationService.sendImmediateNotification(notification);
};
const sendGoalNotification = async (userName: string, goalName: string) => {
const notification = NotificationTemplates.goal.achievement(userName, goalName);
await notificationService.sendImmediateNotification(notification);
};
```
## 权限管理
### 检查权限状态
```typescript
import { useNotifications } from '@/hooks/useNotifications';
function MyComponent() {
const { permissionStatus, requestPermission } = useNotifications();
const handleRequestPermission = async () => {
const status = await requestPermission();
if (status === 'granted') {
console.log('通知权限已授予');
} else {
console.log('通知权限被拒绝');
}
};
return (
<View>
<Text>权限状态: {permissionStatus}</Text>
<Button title="请求权限" onPress={handleRequestPermission} />
</View>
);
}
```
### 处理权限被拒绝
```typescript
const handleNotificationToggle = async (value: boolean) => {
if (value) {
try {
const status = await requestPermission();
if (status === 'granted') {
setNotificationEnabled(true);
} else {
Alert.alert(
'权限被拒绝',
'请在系统设置中开启通知权限以获得最佳体验',
[
{ text: '取消', style: 'cancel' },
{ text: '去设置', onPress: () => Linking.openSettings() }
]
);
}
} catch (error) {
Alert.alert('错误', '请求通知权限失败');
}
}
};
```
## 通知管理
### 取消特定通知
```typescript
import { notificationService } from '@/services/notifications';
// 取消特定通知
await notificationService.cancelNotification(notificationId);
// 取消所有通知
await notificationService.cancelAllNotifications();
// 取消特定类型的通知
import { GeneralNotificationHelpers } from '@/utils/notificationHelpers';
await GeneralNotificationHelpers.cancelNotificationsByType('workout_reminder');
```
### 查询已安排的通知
```typescript
import { notificationService } from '@/services/notifications';
// 获取所有已安排的通知
const notifications = await notificationService.getAllScheduledNotifications();
console.log('已安排的通知数量:', notifications.length);
notifications.forEach(notification => {
console.log('通知ID:', notification.identifier);
console.log('通知标题:', notification.content.title);
console.log('通知内容:', notification.content.body);
console.log('通知类型:', notification.content.data?.type);
});
```
## 测试功能
### 使用测试组件
项目中包含了一个完整的测试组件 `NotificationTest.tsx`,可以用来测试所有通知功能:
```typescript
import { NotificationTest } from '@/components/NotificationTest';
// 在开发环境中使用
function TestScreen() {
return <NotificationTest />;
}
```
### 手动测试
```typescript
// 测试立即通知
await sendNotification({
title: '测试通知',
body: '这是一个测试通知',
sound: true,
priority: 'high'
});
// 测试定时通知5秒后
const futureDate = new Date(Date.now() + 5000);
await scheduleNotification(
{
title: '定时测试',
body: '这是一个5秒后的定时通知',
sound: true
},
futureDate
);
// 测试重复通知(每分钟)
await scheduleRepeatingNotification(
{
title: '重复测试',
body: '这是一个每分钟重复的通知',
sound: true
},
{ minutes: 1 }
);
```
## 最佳实践
### 1. 通知内容设计
- **标题**: 简洁明了不超过50个字符
- **内容**: 具体有用不超过200个字符
- **时机**: 选择合适的发送时间
- **频率**: 避免过于频繁的通知
### 2. 用户体验
- 提供通知开关选项
- 允许用户自定义通知时间
- 支持通知类型的选择
- 提供通知历史记录
### 3. 错误处理
- 始终处理权限请求失败
- 提供用户友好的错误提示
- 实现降级处理方案
### 4. 性能优化
- 避免同时发送大量通知
- 及时清理不需要的通知
- 合理使用重复通知
## 故障排除
### 常见问题
1. **通知不显示**
- 检查权限是否已授予
- 确认应用是否在前台
- 验证通知配置是否正确
2. **定时通知不触发**
- 检查设备是否重启
- 确认应用是否被系统杀死
- 验证时间设置是否正确
3. **权限被拒绝**
- 引导用户到系统设置
- 提供权限说明
- 实现降级处理方案
### 调试技巧
```typescript
// 启用详细日志
console.log('通知权限状态:', await notificationService.getPermissionStatus());
console.log('已安排通知:', await notificationService.getAllScheduledNotifications());
// 测试通知发送
await notificationService.sendImmediateNotification({
title: '调试通知',
body: '这是一个调试通知',
sound: true
});
```
## 总结
推送通知功能已经完整集成到项目中,提供了丰富的功能和良好的用户体验。开发者可以根据具体需求灵活使用各种通知功能,为用户提供个性化的提醒服务。
如有任何问题或建议,请参考 `docs/notification-implementation.md` 文档或联系开发团队。

View File

@@ -1,6 +1,6 @@
{
"expo": {
"name": "海豹健康",
"name": "Out Live",
"slug": "digital-pilates",
"version": "1.0.5",
"orientation": "portrait",

View File

@@ -15,7 +15,7 @@ import { useAuthGuard } from '@/hooks/useAuthGuard';
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 { fetchTodayWaterStats } from '@/store/waterSlice';
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
import { ensureHealthPermissions, fetchHealthDataForDate, testHRVDataFetch } from '@/utils/health';
import { getTestHealthData } from '@/utils/mockHealthData';
@@ -89,8 +89,6 @@ export default function ExploreScreen() {
// 从 Redux 获取指定日期的健康数据
const healthData = useAppSelector(selectHealthDataByDate(currentSelectedDateString));
// 获取今日喝水统计数据
const todayWaterStats = useAppSelector(selectTodayStats);
// 解构健康数据支持mock数据
const mockData = useMockData ? getTestHealthData('mock') : null;
@@ -101,7 +99,7 @@ export default function ExploreScreen() {
const sleepDuration = useMockData ? (mockData?.sleepDuration ?? null) : (healthData?.sleepDuration ?? null);
const hrvValue = useMockData ? (mockData?.hrv ?? 0) : (healthData?.hrv ?? 0);
const oxygenSaturation = useMockData ? (mockData?.oxygenSaturation ?? null) : (healthData?.oxygenSaturation ?? null);
const heartRate = useMockData ? (mockData?.heartRate ?? null) : (healthData?.heartRate ?? null);
const fitnessRingsData = useMockData ? {
activeCalories: mockData?.activeCalories ?? 0,
activeCaloriesGoal: mockData?.activeCaloriesGoal ?? 350,
@@ -455,7 +453,7 @@ export default function ExploreScreen() {
{/* 右边文字区域 */}
<View style={styles.headerTextContainer}>
<Text style={styles.headerTitle}></Text>
<Text style={styles.headerTitle}>Out Live</Text>
</View>
{/* 开发环境调试按钮 */}

View File

@@ -1,10 +1,12 @@
import { Colors } from '@/constants/Colors';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useWaterDataByDate } from '@/hooks/useWaterData';
import { getQuickWaterAmount, setQuickWaterAmount } from '@/utils/userPreferences';
import { Ionicons } from '@expo/vector-icons';
import { Picker } from '@react-native-picker/picker';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { router, useLocalSearchParams } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
@@ -32,6 +34,7 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
const { selectedDate } = useLocalSearchParams<{ selectedDate?: string }>();
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const { ensureLoggedIn } = useAuthGuard();
const [dailyGoal, setDailyGoal] = useState<string>('2000');
const [quickAddAmount, setQuickAddAmount] = useState<string>('250');
@@ -47,6 +50,19 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
// 使用新的 hook 来处理指定日期的饮水数据
const { waterRecords, dailyWaterGoal, updateWaterGoal, removeWaterRecord } = useWaterDataByDate(selectedDate);
// 检查登录状态
useEffect(() => {
const checkLoginStatus = async () => {
const isLoggedIn = await ensureLoggedIn();
if (!isLoggedIn) {
// 如果未登录,用户会被重定向到登录页面
return;
}
};
checkLoginStatus();
}, [ensureLoggedIn]);
const goalPresets = [1500, 2000, 2500, 3000, 3500, 4000];
const quickAddPresets = [100, 150, 200, 250, 300, 350, 400, 500];
@@ -203,7 +219,19 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
};
return (
<View style={[styles.container, { backgroundColor: colorTokens.background }]}>
<View style={styles.container}>
{/* 背景渐变 */}
<LinearGradient
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
{/* 装饰性圆圈 */}
<View style={styles.decorativeCircle1} />
<View style={styles.decorativeCircle2} />
<HeaderBar
title="饮水设置"
onBack={() => {
@@ -249,7 +277,7 @@ const WaterSettings: React.FC<WaterSettingsProps> = () => {
<View style={styles.settingLeft}>
<Text style={[styles.settingTitle, { color: colorTokens.text }]}></Text>
<Text style={[styles.settingSubtitle, { color: colorTokens.textSecondary }]}>
"+"
{`设置点击右上角"+"按钮时添加的默认饮水量`}
</Text>
<Text style={[styles.settingValue, { color: colorTokens.textSecondary }]}>{quickAddAmount}ml</Text>
</View>
@@ -382,6 +410,33 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
decorativeCircle1: {
position: 'absolute',
top: 40,
right: 20,
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#0EA5E9',
opacity: 0.1,
},
decorativeCircle2: {
position: 'absolute',
bottom: -15,
left: -15,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#0EA5E9',
opacity: 0.05,
},
keyboardAvoidingView: {
flex: 1,
},
@@ -395,19 +450,19 @@ const styles = StyleSheet.create({
marginBottom: 32,
},
sectionTitle: {
fontSize: 20,
fontWeight: '600',
fontSize: 16,
fontWeight: '500',
marginBottom: 20,
letterSpacing: -0.5,
},
subsectionTitle: {
fontSize: 16,
fontSize: 14,
fontWeight: '500',
marginBottom: 12,
letterSpacing: -0.3,
},
sectionSubtitle: {
fontSize: 14,
fontSize: 12,
fontWeight: '400',
lineHeight: 18,
},
@@ -582,15 +637,18 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 40,
gap: 12,
gap: 16,
},
noRecordsText: {
fontSize: 16,
fontWeight: '600',
fontSize: 15,
fontWeight: '500',
lineHeight: 20,
},
noRecordsSubText: {
fontSize: 14,
fontSize: 13,
textAlign: 'center',
lineHeight: 18,
opacity: 0.7,
},
modalBackdrop: {
...StyleSheet.absoluteFillObject,

View File

@@ -1,3 +1,4 @@
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { useWaterDataByDate } from '@/hooks/useWaterData';
import { getQuickWaterAmount } from '@/utils/userPreferences';
import { useFocusEffect } from '@react-navigation/native';
@@ -26,6 +27,7 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
selectedDate
}) => {
const router = useRouter();
const { ensureLoggedIn } = useAuthGuard();
const { waterStats, dailyWaterGoal, waterRecords, addWaterRecord } = useWaterDataByDate(selectedDate);
const [quickWaterAmount, setQuickWaterAmount] = useState(150); // 默认值,将从用户偏好中加载
@@ -120,6 +122,12 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
// 处理添加喝水 - 右上角按钮直接添加
const handleQuickAddWater = async () => {
// 检查用户是否已登录
const isLoggedIn = await ensureLoggedIn();
if (!isLoggedIn) {
return;
}
// 触发震动反馈
if (process.env.EXPO_OS === 'ios') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
@@ -135,7 +143,13 @@ const WaterIntakeCard: React.FC<WaterIntakeCardProps> = ({
};
// 处理卡片点击 - 跳转到饮水设置页面
const handleCardPress = () => {
const handleCardPress = async () => {
// 检查用户是否已登录
const isLoggedIn = await ensureLoggedIn();
if (!isLoggedIn) {
return;
}
// 触发震动反馈
if (process.env.EXPO_OS === 'ios') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);

View File

@@ -39,7 +39,7 @@ export function WeightHistoryCard() {
const dispatch = useAppDispatch();
const userProfile = useAppSelector((s) => s.user.profile);
const weightHistory = useAppSelector((s) => s.user.weightHistory);
const [isLoading, setIsLoading] = useState(false);
const [showChart, setShowChart] = useState(false);
const [showBMIModal, setShowBMIModal] = useState(false);
@@ -71,12 +71,9 @@ export function WeightHistoryCard() {
const loadWeightHistory = async () => {
try {
setIsLoading(true);
await dispatch(fetchWeightHistory() as any);
} catch (error) {
console.error('加载体重历史失败:', error);
} finally {
setIsLoading(false);
}
};

View File

@@ -3,6 +3,7 @@ import { usePathname, useRouter } from 'expo-router';
import { useCallback } from 'react';
import { Alert } from 'react-native';
import { ROUTES } from '@/constants/Routes';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { api } from '@/services/api';
import { logout as logoutAction } from '@/store/userSlice';
@@ -24,7 +25,7 @@ export function useAuthGuard() {
const ensureLoggedIn = useCallback(async (options?: EnsureOptions): Promise<boolean> => {
if (isLoggedIn) return true;
const redirectTo = options?.redirectTo ?? currentPath ?? '/(tabs)';
const redirectTo = options?.redirectTo ?? currentPath ?? ROUTES.TAB_STATISTICS;
const paramsJson = options?.redirectParams ? JSON.stringify(options.redirectParams) : undefined;
router.push({
@@ -62,7 +63,7 @@ export function useAuthGuard() {
try {
// 调用 Redux action 清除本地状态和缓存
await dispatch(logoutAction()).unwrap();
// 跳转到登录页面
router.push('/auth/login');
} catch (error) {
@@ -95,13 +96,13 @@ export function useAuthGuard() {
try {
// 调用注销账号API
await api.delete('/api/users/delete-account');
// 清除额外的本地数据
await AsyncStorage.multiRemove(['@user_personal_info', '@onboarding_completed']);
// 执行退出登录逻辑
await dispatch(logoutAction()).unwrap();
Alert.alert('账号已注销', '您的账号已成功注销', [
{
text: '确定',

View File

@@ -13,6 +13,8 @@ install! 'cocoapods',
prepare_react_native_project!
project 'digitalpilates.xcodeproj'
target 'digitalpilates' do
use_expo_modules!

View File

@@ -2388,10 +2388,10 @@ SPEC CHECKSUMS:
ExpoSystemUI: 433a971503b99020318518ed30a58204288bab2d
ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92
EXTaskManager: 280143f6d8e596f28739d74bf34910300dcbd4ea
fast_float: 23278fd30b349f976d2014f4aec9e2d7bc1c3806
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52
fmt: b85d977e8fe789fd71c77123f9f4920d88c4d170
glog: 682871fb30f4a65f657bf357581110656ea90b08
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
@@ -2401,7 +2401,7 @@ SPEC CHECKSUMS:
QCloudCore: 6f8c67b96448472d2c6a92b9cfe1bdb5abbb1798
QCloudCOSXML: 92f50a787b4e8d9a7cb6ea8e626775256b4840a7
QCloudTrack: 20b79388365b4c8ed150019c82a56f1569f237f8
RCT-Folly: 031db300533e2dfa954cdc5a859b792d5c14ed7b
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
RCTDeprecation: 5f638f65935e273753b1f31a365db6a8d6dc53b5
RCTRequired: 8b46a520ea9071e2bc47d474aa9ca31b4a935bd8
RCTTypeSafety: cc4740278c2a52cbf740592b0a0a40df1587c9ab
@@ -2490,6 +2490,6 @@ SPEC CHECKSUMS:
Yoga: adb397651e1c00672c12e9495babca70777e411e
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
PODFILE CHECKSUM: 8d79b726cf7814a1ef2e250b7a9ef91c07c77936
PODFILE CHECKSUM: d6bbff2c1fc3ea45fb6bb3b1c3bb766a4f2d5dd4
COCOAPODS: 1.16.2

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@@ -348,7 +348,7 @@
"FB_SONARKIT_ENABLED=1",
);
INFOPLIST_FILE = digitalpilates/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "海豹健康";
INFOPLIST_KEY_CFBundleDisplayName = "Out Live";
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -386,7 +386,7 @@
DEVELOPMENT_TEAM = 756WVXJ6MT;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = x86_64;
INFOPLIST_FILE = digitalpilates/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "海豹健康";
INFOPLIST_KEY_CFBundleDisplayName = "Out Live";
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View File

@@ -11,7 +11,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>海豹健康</string>
<string>Out Live</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@@ -23,7 +23,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0.8</string>
<string>1.0.9</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@@ -1,135 +0,0 @@
# 喝水记录 API 修复测试文档
## 修复内容总结
### 1. 服务层修复 (services/waterRecords.ts)
#### 接口路径修复
- ✅ 更新喝水目标:`/water-goal``/water-records/goal/daily`
- ✅ 获取统计数据:`/water-stats/today``/water-records/stats`
- ✅ 获取指定日期统计:`/water-stats/${date}``/water-records/stats?date=${date}`
#### 数据结构修复
- ✅ 字段名称:`remark``note`
- ✅ 枚举值:`'manual' | 'auto' | 'other'``'Manual' | 'Auto'`
- ✅ 新增字段:`recordedAt` (记录时间)
- ✅ 响应结构:处理标准 API 响应格式 `{ data: {...}, pagination: {...} }`
#### 类型定义更新
```typescript
// 旧版本
interface WaterRecord {
source: 'manual' | 'auto' | 'other';
remark?: string;
}
// 新版本
interface WaterRecord {
source?: 'Manual' | 'Auto';
note?: string;
recordedAt: string;
}
```
### 2. Redux Store 修复 (store/waterSlice.ts)
#### Loading 状态完善
- ✅ 新增:`create`, `update`, `delete` loading 状态
#### 完成率计算修复
- ✅ 统一使用百分比格式:`(totalAmount / dailyGoal) * 100`
- ✅ 所有相关计算都已更新
#### 日期字段处理
- ✅ 优先使用 `recordedAt`,回退到 `createdAt`
### 3. Hooks 修复 (hooks/useWaterData.ts)
#### 函数签名更新
```typescript
// 旧版本
addWaterRecord(amount: number, remark?: string)
// 新版本
addWaterRecord(amount: number, note?: string, recordedAt?: string)
```
#### 完成率计算
- ✅ 返回百分比格式而非小数
### 4. 组件修复
#### WaterIntakeCard.tsx
- ✅ 日期字段:优先使用 `recordedAt`
- ✅ 完成率显示:移除多余的 `* 100` 计算
#### AddWaterModal.tsx
- ✅ 字段名称:`remark``note`
- ✅ 数据结构:添加 `source: 'Manual'`
## 测试要点
### 1. API 调用测试
```javascript
// 测试创建记录
const createResult = await createWaterRecord({
amount: 250,
note: "测试记录",
source: "Manual",
recordedAt: "2023-12-01T10:00:00.000Z"
});
// 测试获取统计
const stats = await getTodayWaterStats();
console.log('完成率应该是百分比:', stats.completionRate); // 应该是 0-100 的数值
// 测试更新目标
const goalResult = await updateWaterGoal({ dailyWaterGoal: 2500 });
```
### 2. Redux 状态测试
```javascript
// 测试完成率计算
// 假设总量 1500ml目标 2000ml
// 期望完成率75 (百分比)
const expectedRate = (1500 / 2000) * 100; // 75
```
### 3. 组件渲染测试
- ✅ 完成率显示正确(不会超过 100%
- ✅ 图表数据使用正确的时间字段
- ✅ 表单提交使用正确的字段名称
## 兼容性说明
### 向后兼容
- ✅ 保留了 `createdAt` 字段的回退逻辑
- ✅ 保留了单日期查询的兼容性处理
- ✅ 保留了原有的选择器函数
### 新功能支持
- ✅ 支持自定义记录时间 (`recordedAt`)
- ✅ 支持新的 API 响应格式
- ✅ 支持百分比格式的完成率
## 需要验证的功能
1. **创建记录**:确保新记录包含正确的字段
2. **更新记录**:确保更新时使用正确的字段名
3. **删除记录**:确保删除后统计数据正确更新
4. **目标设置**:确保目标更新后完成率重新计算
5. **统计查询**:确保返回正确的百分比格式完成率
6. **图表显示**:确保使用正确的时间字段进行分组
## 潜在问题
1. **时区处理**`recordedAt` 字段的时区处理需要注意
2. **数据迁移**:现有数据可能没有 `recordedAt` 字段
3. **API 兼容性**:确保后端 API 已经更新到新版本
## 建议测试流程
1. 单元测试:测试各个函数的输入输出
2. 集成测试:测试 Redux 状态管理
3. 端到端测试:测试完整的用户操作流程
4. API 测试:使用 Postman 或类似工具测试 API 接口