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

447
services/healthKitSync.ts Normal file
View File

@@ -0,0 +1,447 @@
/**
* 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<boolean> {
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<void> {
syncLock = false;
await AsyncStorage.removeItem(SYNC_LOCK_KEY);
}
/**
* 获取上次同步状态
*/
async function getLastSyncStatus(): Promise<SyncStatus | null> {
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<void> {
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<any>,
currentUserProfile?: {
height?: string | number;
weight?: string | number;
birthDate?: string | number;
}
): Promise<boolean> {
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<boolean> {
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<void> {
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<SyncStatus | null> {
return getLastSyncStatus();
}