/** * HealthKit 数据同步服务 * * 实现 HealthKit 与服务端之间的双向数据同步 * - 从 HealthKit 同步身高、体重、出生日期到服务端 * - 从服务端同步身高、体重、出生日期到 HealthKit * - 防止循环同步的机制 */ import { fetchPersonalHealthData, saveHeight, saveWeight } from '@/utils/health'; import AsyncStorage from '@/utils/kvStore'; import dayjs from 'dayjs'; // 同步状态存储键 const SYNC_STATUS_KEY = '@healthkit_sync_status'; const SYNC_LOCK_KEY = '@healthkit_sync_lock'; // 同步状态类型 interface SyncStatus { lastSyncTime: number; lastSyncDirection: 'healthkit_to_server' | 'server_to_healthkit' | null; lastSyncData: { height?: number; weight?: number; birthDate?: string; }; } // 同步锁(防止并发同步) let syncLock = false; /** * 获取同步锁 */ async function acquireSyncLock(): Promise { if (syncLock) { console.log('同步操作正在进行中,跳过'); return false; } // 检查持久化的锁(防止应用重启后的并发) const persistedLock = await AsyncStorage.getItem(SYNC_LOCK_KEY); if (persistedLock) { const lockTime = parseInt(persistedLock); const now = Date.now(); // 如果锁超过5分钟,认为是过期锁,可以清除 if (now - lockTime < 5 * 60 * 1000) { console.log('检测到持久化同步锁,跳过'); return false; } else { console.log('清除过期的同步锁'); await AsyncStorage.removeItem(SYNC_LOCK_KEY); } } syncLock = true; await AsyncStorage.setItem(SYNC_LOCK_KEY, Date.now().toString()); return true; } /** * 释放同步锁 */ async function releaseSyncLock(): Promise { syncLock = false; await AsyncStorage.removeItem(SYNC_LOCK_KEY); } /** * 获取上次同步状态 */ async function getLastSyncStatus(): Promise { try { const status = await AsyncStorage.getItem(SYNC_STATUS_KEY); if (status) { return JSON.parse(status); } return null; } catch (error) { console.error('获取同步状态失败:', error); return null; } } /** * 保存同步状态 */ async function saveSyncStatus(status: SyncStatus): Promise { try { await AsyncStorage.setItem(SYNC_STATUS_KEY, JSON.stringify(status)); } catch (error) { console.error('保存同步状态失败:', error); } } /** * 判断数据是否需要同步(比较数据是否有变化) */ function shouldSyncData( currentData: { height?: number; weight?: number; birthDate?: string }, lastSyncData: { height?: number; weight?: number; birthDate?: string } ): boolean { // 身高变化(允许1cm误差) if (currentData.height && lastSyncData.height) { if (Math.abs(currentData.height - lastSyncData.height) > 1) { return true; } } else if (currentData.height !== lastSyncData.height) { return true; } // 体重变化(允许0.1kg误差) if (currentData.weight && lastSyncData.weight) { if (Math.abs(currentData.weight - lastSyncData.weight) > 0.1) { return true; } } else if (currentData.weight !== lastSyncData.weight) { return true; } // 出生日期变化 if (currentData.birthDate !== lastSyncData.birthDate) { // 出生日期通常不会变,但仍然检查 if (currentData.birthDate && lastSyncData.birthDate) { const date1 = dayjs(currentData.birthDate).format('YYYY-MM-DD'); const date2 = dayjs(lastSyncData.birthDate).format('YYYY-MM-DD'); if (date1 !== date2) { return true; } } else if (currentData.birthDate || lastSyncData.birthDate) { return true; } } return false; } /** * 从 HealthKit 同步数据到服务端 * * @param updateUserProfile - 更新用户资料的函数(从 Redux userSlice) * @param currentUserProfile - 当前用户资料(用于 diff 比较) * @returns 是否成功同步 */ export async function syncHealthKitToServer( updateUserProfile: (data: { height?: number; weight?: number; birthDate?: number; // Unix timestamp in seconds }) => Promise, currentUserProfile?: { height?: string | number; weight?: string | number; birthDate?: string | number; } ): Promise { console.log('=== 开始从 HealthKit 同步数据到服务端 ==='); // 获取同步锁 const acquired = await acquireSyncLock(); if (!acquired) { return false; } try { // 获取上次同步状态 const lastSync = await getLastSyncStatus(); // 检查是否刚刚从服务端同步过(防止循环) if (lastSync?.lastSyncDirection === 'server_to_healthkit') { const timeSinceLastSync = Date.now() - lastSync.lastSyncTime; // 如果2分钟内刚从服务端同步过,跳过本次同步 if (timeSinceLastSync < 2 * 60 * 1000) { console.log('刚从服务端同步过数据,跳过 HealthKit -> 服务端同步'); return false; } } // 从 HealthKit 获取数据 const healthData = await fetchPersonalHealthData(); console.log('从 HealthKit 获取的数据:', healthData); // 检查是否有数据需要同步 if (!healthData.height && !healthData.weight && !healthData.dateOfBirth) { console.log('HealthKit 中没有个人健康数据,跳过同步'); return false; } // 准备当前 HealthKit 数据 const currentHealthKitData = { height: healthData.height || undefined, weight: healthData.weight || undefined, birthDate: healthData.dateOfBirth || undefined }; // 1. 首先与服务端当前数据进行比较(如果提供了) if (currentUserProfile) { // 转换服务端数据格式以便比较 const serverData = { height: currentUserProfile.height ? (typeof currentUserProfile.height === 'string' ? parseFloat(currentUserProfile.height) : currentUserProfile.height) : undefined, weight: currentUserProfile.weight ? (typeof currentUserProfile.weight === 'string' ? parseFloat(currentUserProfile.weight) : currentUserProfile.weight) : undefined, birthDate: currentUserProfile.birthDate ? (typeof currentUserProfile.birthDate === 'number' ? new Date(currentUserProfile.birthDate * 1000).toISOString() : currentUserProfile.birthDate) : undefined }; const needsSyncWithServer = shouldSyncData(currentHealthKitData, serverData); if (!needsSyncWithServer) { console.log('HealthKit 数据与服务端数据一致,跳过同步'); // 更新同步状态(即使没有同步,也记录检查时间) await saveSyncStatus({ lastSyncTime: Date.now(), lastSyncDirection: 'healthkit_to_server', lastSyncData: currentHealthKitData }); return false; } console.log('检测到 HealthKit 数据与服务端数据存在差异,需要同步'); } // 2. 然后与上次同步的数据进行比较 if (lastSync?.lastSyncData) { const needsSync = shouldSyncData(currentHealthKitData, lastSync.lastSyncData); if (!needsSync) { console.log('数据与上次同步时一致,跳过同步'); return false; } } // 准备同步到服务端的数据 const serverData: { height?: number; weight?: number; birthDate?: number; } = {}; if (healthData.height) { serverData.height = healthData.height; } if (healthData.weight) { serverData.weight = healthData.weight; } if (healthData.dateOfBirth) { // 将 ISO 字符串转换为 Unix 时间戳(秒) const timestamp = new Date(healthData.dateOfBirth).getTime() / 1000; serverData.birthDate = Math.floor(timestamp); } // 同步到服务端 console.log('准备同步到服务端的数据:', serverData); await updateUserProfile(serverData); console.log('成功同步数据到服务端'); // 保存同步状态 await saveSyncStatus({ lastSyncTime: Date.now(), lastSyncDirection: 'healthkit_to_server', lastSyncData: { height: healthData.height || undefined, weight: healthData.weight || undefined, birthDate: healthData.dateOfBirth || undefined } }); return true; } catch (error) { console.error('从 HealthKit 同步数据到服务端失败:', error); return false; } finally { // 释放同步锁 await releaseSyncLock(); } } /** * 从服务端同步数据到 HealthKit * * @param serverData - 服务端的用户数据 * @returns 是否成功同步 */ export async function syncServerToHealthKit( serverData: { height?: number; weight?: number; birthDate?: number | string; // Unix timestamp (seconds) or ISO string } ): Promise { console.log('=== 开始从服务端同步数据到 HealthKit ==='); // 获取同步锁 const acquired = await acquireSyncLock(); if (!acquired) { return false; } try { // 获取上次同步状态 const lastSync = await getLastSyncStatus(); // 检查是否刚刚从 HealthKit 同步过(防止循环) if (lastSync?.lastSyncDirection === 'healthkit_to_server') { const timeSinceLastSync = Date.now() - lastSync.lastSyncTime; // 如果2分钟内刚从 HealthKit 同步过,跳过本次同步 if (timeSinceLastSync < 2 * 60 * 1000) { console.log('刚从 HealthKit 同步过数据,跳过 服务端 -> HealthKit 同步'); return false; } } // 准备要同步的数据 const syncData: { height?: number; weight?: number; birthDate?: string; } = {}; if (serverData.height) { // 确保身高是数字类型 syncData.height = typeof serverData.height === 'string' ? parseFloat(serverData.height) : serverData.height; } if (serverData.weight) { // 确保体重是数字类型 syncData.weight = typeof serverData.weight === 'string' ? parseFloat(serverData.weight) : serverData.weight; } if (serverData.birthDate) { // 如果是时间戳,转换为 ISO 字符串 if (typeof serverData.birthDate === 'number') { syncData.birthDate = new Date(serverData.birthDate * 1000).toISOString(); } else { syncData.birthDate = serverData.birthDate; } } console.log('准备同步到 HealthKit 的数据:', syncData); // 检查数据是否有变化 if (lastSync?.lastSyncData) { const needsSync = shouldSyncData(syncData, lastSync.lastSyncData); if (!needsSync) { console.log('数据未变化,跳过同步'); return false; } } // 同步到 HealthKit const results: boolean[] = []; if (syncData.height) { console.log('同步身高到 HealthKit:', syncData.height, 'cm'); const success = await saveHeight(syncData.height); results.push(success); if (success) { console.log('身高同步成功'); } else { console.error('身高同步失败'); } } if (syncData.weight) { // 确保体重值是数字类型 const weightValue = typeof syncData.weight === 'string' ? parseFloat(syncData.weight) : syncData.weight; console.log('同步体重到 HealthKit:', weightValue, 'kg'); const success = await saveWeight(weightValue); results.push(success); if (success) { console.log('体重同步成功'); } else { console.error('体重同步失败'); } } // 注意:出生日期在 HealthKit 中是只读的,无法写入 if (syncData.birthDate) { console.log('注意:出生日期在 HealthKit 中是只读的,无法同步'); } // 如果至少有一项同步成功,则认为同步成功 const success = results.length > 0 && results.some(r => r); if (success) { // 保存同步状态 await saveSyncStatus({ lastSyncTime: Date.now(), lastSyncDirection: 'server_to_healthkit', lastSyncData: syncData }); console.log('成功从服务端同步数据到 HealthKit'); } return success; } catch (error) { console.error('从服务端同步数据到 HealthKit 失败:', error); return false; } finally { // 释放同步锁 await releaseSyncLock(); } } /** * 清除同步状态(用于测试或重置) */ export async function clearSyncStatus(): Promise { try { await AsyncStorage.removeItem(SYNC_STATUS_KEY); await AsyncStorage.removeItem(SYNC_LOCK_KEY); syncLock = false; console.log('已清除同步状态'); } catch (error) { console.error('清除同步状态失败:', error); } } /** * 获取同步状态信息(用于调试) */ export async function getSyncStatusInfo(): Promise { return getLastSyncStatus(); }