feat(healthkit): 实现HealthKit与服务端的双向数据同步,包括身高、体重和出生日期的获取与保存

This commit is contained in:
richarjiang
2025-11-19 15:42:50 +08:00
parent dc205ad56e
commit 6039d0a778
8 changed files with 1029 additions and 8 deletions

View File

@@ -15,11 +15,14 @@ import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { BackgroundTaskManager } from '@/services/backgroundTaskManagerV2';
import { syncHealthKitToServer } from '@/services/healthKitSync';
import { setHealthData } from '@/store/healthSlice';
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
import { updateUserProfile } from '@/store/userSlice';
import { fetchTodayWaterStats } from '@/store/waterSlice';
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
import { fetchHealthDataForDate, testHRVDataFetch } from '@/utils/health';
import { logger } from '@/utils/logger';
import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import { debounce } from 'lodash';
@@ -58,6 +61,7 @@ const FloatingCard = ({ children, style }: {
export default function ExploreScreen() {
const { t } = useTranslation();
const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000;
const userProfile = useAppSelector((s) => s.user.profile);
const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard();
@@ -273,8 +277,44 @@ export default function ExploreScreen() {
}
}, [executeLoadAllData, debouncedLoadAllData]);
// 同步 HealthKit 数据到服务端(带智能 diff 比较)
const syncHealthDataToServer = React.useCallback(async () => {
if (!isLoggedIn || !userProfile) {
logger.info('用户未登录,跳过 HealthKit 数据同步');
return;
}
try {
logger.info('开始同步 HealthKit 个人健康数据到服务端...');
// 传入当前用户资料,用于 diff 比较
const success = await syncHealthKitToServer(
async (data) => {
await dispatch(updateUserProfile(data) as any);
},
userProfile // 传入当前用户资料进行比较
);
if (success) {
logger.info('HealthKit 数据同步到服务端成功');
} else {
logger.info('HealthKit 数据同步到服务端跳过(无变化)或失败');
}
} catch (error) {
logger.error('同步 HealthKit 数据到服务端失败:', error);
}
}, [isLoggedIn, dispatch, userProfile]);
// 初始加载时执行数据加载和同步
useEffect(() => {
loadAllData(currentSelectedDate);
// 延迟1秒后执行同步避免影响初始加载性能
const syncTimer = setTimeout(() => {
syncHealthDataToServer();
}, 1000);
return () => clearTimeout(syncTimer);
}, [])

View File

@@ -10,7 +10,7 @@ import PrivacyConsentModal from '@/components/PrivacyConsentModal';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useQuickActions } from '@/hooks/useQuickActions';
import { hrvMonitorService } from '@/services/hrvMonitor';
import { notificationService } from '@/services/notifications';
import { clearBadgeCount, notificationService } from '@/services/notifications';
import { setupQuickActions } from '@/services/quickActions';
import { sleepMonitorService } from '@/services/sleepMonitor';
import { initializeWaterRecordBridge } from '@/services/waterRecordBridge';
@@ -25,6 +25,7 @@ import { initializeHealthPermissions } from '@/utils/health';
import { MoodNotificationHelpers, NutritionNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
import React, { useEffect } from 'react';
import { AppState, AppStateStatus } from 'react-native';
import { DialogProvider } from '@/components/ui/DialogProvider';
import { MembershipModalProvider } from '@/contexts/MembershipModalContext';
@@ -149,6 +150,20 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
initializeBasicServices();
}, [dispatch]);
// ==================== 应用状态监听 - 进入前台时清除角标 ====================
React.useEffect(() => {
const subscription = AppState.addEventListener('change', (nextAppState: AppStateStatus) => {
if (nextAppState === 'active') {
// 应用进入前台时清除角标
clearBadgeCount();
}
});
return () => {
subscription.remove();
};
}, []);
// ==================== 权限相关服务初始化(应用启动时执行)====================
React.useEffect(() => {
// 如果已经初始化过,则跳过(确保只初始化一次)

View File

@@ -5,6 +5,7 @@ import { useColorScheme } from '@/hooks/useColorScheme';
import { useCosUpload } from '@/hooks/useCosUpload';
import { useI18n } from '@/hooks/useI18n';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import { syncServerToHealthKit } from '@/services/healthKitSync';
import { fetchMyProfile, updateUserProfile } from '@/store/userSlice';
import { fetchMaximumHeartRate } from '@/utils/health';
import AsyncStorage from '@/utils/kvStore';
@@ -212,6 +213,18 @@ export default function EditProfileScreen() {
}));
// 拉取最新用户信息,刷新全局状态
await dispatch(fetchMyProfile() as any);
// 同步身高、体重到 HealthKit
console.log('开始同步个人健康数据到 HealthKit...');
const syncSuccess = await syncServerToHealthKit({
height: next.height,
weight: next.weight,
birthDate: next.birthDate ? new Date(next.birthDate).getTime() / 1000 : undefined
});
if (syncSuccess) {
console.log('个人健康数据已同步到 HealthKit');
}
} catch (e: any) {
// 接口失败不阻断本地保存
console.warn('更新用户信息失败', e?.message || e);