202 lines
6.3 KiB
TypeScript
202 lines
6.3 KiB
TypeScript
import { fetchRecentWorkouts, WorkoutData } from '@/utils/health';
|
||
import AsyncStorage from '@/utils/kvStore';
|
||
import { NativeEventEmitter, NativeModules } from 'react-native';
|
||
import { analyzeWorkoutAndSendNotification } from './workoutNotificationService';
|
||
|
||
const { HealthKitManager } = NativeModules;
|
||
const workoutEmitter = new NativeEventEmitter(HealthKitManager);
|
||
|
||
const INITIAL_LOOKBACK_WINDOW_MS = 24 * 60 * 60 * 1000; // 24小时
|
||
const DEFAULT_LOOKBACK_WINDOW_MS = 12 * 60 * 60 * 1000; // 12小时
|
||
|
||
class WorkoutMonitorService {
|
||
private static instance: WorkoutMonitorService;
|
||
private isInitialized = false;
|
||
private lastProcessedWorkoutId: string | null = null;
|
||
private processingTimeout: any = null;
|
||
private eventListenerSubscription: any = null;
|
||
|
||
static getInstance(): WorkoutMonitorService {
|
||
if (!WorkoutMonitorService.instance) {
|
||
WorkoutMonitorService.instance = new WorkoutMonitorService();
|
||
}
|
||
return WorkoutMonitorService.instance;
|
||
}
|
||
|
||
async initialize(): Promise<void> {
|
||
if (this.isInitialized) {
|
||
console.log('锻炼监听服务已初始化');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 获取上次处理的锻炼ID
|
||
await this.loadLastProcessedWorkoutId();
|
||
|
||
// 启动 iOS 原生锻炼监听器
|
||
await HealthKitManager.startWorkoutObserver();
|
||
|
||
// 监听锻炼更新事件
|
||
this.eventListenerSubscription = workoutEmitter.addListener(
|
||
'workoutUpdate',
|
||
this.handleWorkoutUpdate.bind(this)
|
||
);
|
||
|
||
this.isInitialized = true;
|
||
console.log('锻炼监听服务初始化成功');
|
||
} catch (error) {
|
||
console.error('锻炼监听服务初始化失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async stop(): Promise<void> {
|
||
try {
|
||
// 停止原生监听器
|
||
await HealthKitManager.stopWorkoutObserver();
|
||
|
||
// 移除事件监听器
|
||
if (this.eventListenerSubscription) {
|
||
this.eventListenerSubscription.remove();
|
||
this.eventListenerSubscription = null;
|
||
}
|
||
|
||
// 清理定时器
|
||
if (this.processingTimeout) {
|
||
clearTimeout(this.processingTimeout);
|
||
this.processingTimeout = null;
|
||
}
|
||
|
||
this.isInitialized = false;
|
||
console.log('锻炼监听服务已停止');
|
||
} catch (error) {
|
||
console.error('停止锻炼监听服务失败:', error);
|
||
}
|
||
}
|
||
|
||
private async loadLastProcessedWorkoutId(): Promise<void> {
|
||
try {
|
||
const storedId = await AsyncStorage.getItem('@last_processed_workout_id');
|
||
this.lastProcessedWorkoutId = storedId;
|
||
console.log('上次处理的锻炼ID:', this.lastProcessedWorkoutId);
|
||
} catch (error) {
|
||
console.error('加载上次处理的锻炼ID失败:', error);
|
||
}
|
||
}
|
||
|
||
private async saveLastProcessedWorkoutId(workoutId: string): Promise<void> {
|
||
try {
|
||
await AsyncStorage.setItem('@last_processed_workout_id', workoutId);
|
||
this.lastProcessedWorkoutId = workoutId;
|
||
} catch (error) {
|
||
console.error('保存上次处理的锻炼ID失败:', error);
|
||
}
|
||
}
|
||
|
||
private async handleWorkoutUpdate(event: any): Promise<void> {
|
||
console.log('收到锻炼更新事件:', event);
|
||
|
||
// 防抖处理,避免短时间内重复处理
|
||
if (this.processingTimeout) {
|
||
clearTimeout(this.processingTimeout);
|
||
}
|
||
|
||
this.processingTimeout = setTimeout(async () => {
|
||
try {
|
||
await this.checkForNewWorkouts();
|
||
} catch (error) {
|
||
console.error('检查新锻炼失败:', error);
|
||
}
|
||
}, 5000); // 5秒延迟,确保 HealthKit 数据已完全更新
|
||
}
|
||
|
||
private async checkForNewWorkouts(): Promise<void> {
|
||
try {
|
||
console.log('检查新的锻炼记录...');
|
||
|
||
const lookbackWindowMs = this.lastProcessedWorkoutId
|
||
? DEFAULT_LOOKBACK_WINDOW_MS
|
||
: INITIAL_LOOKBACK_WINDOW_MS;
|
||
const startDate = new Date(Date.now() - lookbackWindowMs);
|
||
const endDate = new Date();
|
||
|
||
console.log(
|
||
`锻炼查询窗口: ${Math.round(lookbackWindowMs / (1000 * 60 * 60))} 小时 (${startDate.toISOString()} - ${endDate.toISOString()})`
|
||
);
|
||
|
||
const recentWorkouts = await fetchRecentWorkouts({
|
||
startDate: startDate.toISOString(),
|
||
endDate: endDate.toISOString(),
|
||
limit: 10
|
||
});
|
||
|
||
console.log(`找到 ${recentWorkouts.length} 条最近的锻炼记录`);
|
||
|
||
if (this.lastProcessedWorkoutId && !recentWorkouts.some(workout => workout.id === this.lastProcessedWorkoutId)) {
|
||
console.warn('上次处理的锻炼记录不在当前查询窗口内,可能存在漏报风险');
|
||
}
|
||
|
||
const newWorkouts: WorkoutData[] = [];
|
||
for (const workout of recentWorkouts) {
|
||
if (workout.id === this.lastProcessedWorkoutId) {
|
||
break;
|
||
}
|
||
newWorkouts.push(workout);
|
||
}
|
||
|
||
if (newWorkouts.length === 0) {
|
||
console.log('没有检测到新的锻炼记录');
|
||
return;
|
||
}
|
||
|
||
console.log(`检测到 ${newWorkouts.length} 条新的锻炼记录,将按时间顺序处理`);
|
||
|
||
// 先处理最旧的锻炼,确保通知顺序正确
|
||
for (const workout of newWorkouts.reverse()) {
|
||
console.log('处理新锻炼:', {
|
||
id: workout.id,
|
||
type: workout.workoutActivityTypeString,
|
||
duration: workout.duration,
|
||
startDate: workout.startDate
|
||
});
|
||
|
||
await this.processNewWorkout(workout);
|
||
}
|
||
|
||
await this.saveLastProcessedWorkoutId(newWorkouts[0].id);
|
||
console.log('锻炼处理完成,最新处理的锻炼ID:', newWorkouts[0].id);
|
||
} catch (error) {
|
||
console.error('检查新锻炼失败:', error);
|
||
}
|
||
}
|
||
|
||
private async processNewWorkout(workout: WorkoutData): Promise<void> {
|
||
try {
|
||
console.log('开始处理新锻炼:', workout.id);
|
||
|
||
// 分析锻炼并发送通知
|
||
await analyzeWorkoutAndSendNotification(workout);
|
||
|
||
console.log('新锻炼处理完成:', workout.id);
|
||
} catch (error) {
|
||
console.error('处理新锻炼失败:', error);
|
||
}
|
||
}
|
||
|
||
// 手动触发检查(用于测试)
|
||
async manualCheck(): Promise<void> {
|
||
console.log('手动触发锻炼检查...');
|
||
await this.checkForNewWorkouts();
|
||
}
|
||
|
||
// 获取服务状态
|
||
getStatus(): { initialized: boolean; lastProcessedWorkoutId: string | null } {
|
||
return {
|
||
initialized: this.isInitialized,
|
||
lastProcessedWorkoutId: this.lastProcessedWorkoutId
|
||
};
|
||
}
|
||
}
|
||
|
||
export const workoutMonitorService = WorkoutMonitorService.getInstance();
|