Files
digital-pilates/services/workoutMonitor.ts
richarjiang a309123b35 feat(app): add version check system and enhance internationalization support
Add comprehensive app update checking functionality with:
- New VersionCheckContext for managing update detection and notifications
- VersionUpdateModal UI component for presenting update information
- Version service API integration with platform-specific update URLs
- Version check menu item in personal settings with manual/automatic checking

Enhance internationalization across workout features:
- Complete workout type translations for English and Chinese
- Localized workout detail modal with proper date/time formatting
- Locale-aware date formatting in fitness rings detail
- Workout notification improvements with deep linking to specific workout details

Improve UI/UX with better chart rendering, sizing fixes, and enhanced navigation flow. Update app version to 1.1.3 and include app version in API headers for better tracking.
2025-11-29 20:47:16 +08:00

203 lines
6.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { fetchRecentWorkouts, WorkoutData } from '@/utils/health';
import AsyncStorage from '@/utils/kvStore';
import { logger } from '@/utils/logger';
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> {
logger.info('收到锻炼更新事件:', event);
// 防抖处理,避免短时间内重复处理
if (this.processingTimeout) {
clearTimeout(this.processingTimeout);
}
this.processingTimeout = setTimeout(async () => {
try {
await this.checkForNewWorkouts();
} catch (error) {
logger.error('检查新锻炼失败:', error);
}
}, 5000); // 5秒延迟确保 HealthKit 数据已完全更新
}
private async checkForNewWorkouts(): Promise<void> {
try {
logger.info('检查新的锻炼记录...');
const lookbackWindowMs = this.lastProcessedWorkoutId
? DEFAULT_LOOKBACK_WINDOW_MS
: INITIAL_LOOKBACK_WINDOW_MS;
const startDate = new Date(Date.now() - lookbackWindowMs);
const endDate = new Date();
logger.info(
`锻炼查询窗口: ${Math.round(lookbackWindowMs / (1000 * 60 * 60))} 小时 (${startDate.toISOString()} - ${endDate.toISOString()})`
);
const recentWorkouts = await fetchRecentWorkouts({
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
limit: 10
});
logger.info(`找到 ${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) {
logger.info('没有检测到新的锻炼记录');
return;
}
logger.info(`检测到 ${newWorkouts.length} 条新的锻炼记录,将按时间顺序处理`);
// 先处理最旧的锻炼,确保通知顺序正确
for (const workout of newWorkouts.reverse()) {
logger.info('处理新锻炼:', {
id: workout.id,
type: workout.workoutActivityTypeString,
duration: workout.duration,
startDate: workout.startDate
});
await this.processNewWorkout(workout);
}
await this.saveLastProcessedWorkoutId(newWorkouts[0].id);
logger.info('锻炼处理完成最新处理的锻炼ID:', newWorkouts[0].id);
} catch (error) {
logger.error('检查新锻炼失败:', error);
}
}
private async processNewWorkout(workout: WorkoutData): Promise<void> {
try {
logger.info('开始处理新锻炼:', workout.id);
// 分析锻炼并发送通知
await analyzeWorkoutAndSendNotification(workout);
logger.info('新锻炼处理完成:', workout.id);
} catch (error) {
logger.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();