Files
digital-pilates/services/pushNotificationManager.ts
richarjiang 21e57634e0 feat(hrv): 添加心率变异性监控和压力评估功能
- 新增 HRV 监听服务,实时监控心率变异性数据
- 实现 HRV 到压力指数的转换算法和压力等级评估
- 添加智能通知服务,在压力偏高时推送健康建议
- 优化日志系统,修复日志丢失问题并增强刷新机制
- 改进个人页面下拉刷新,支持并行数据加载
- 优化勋章数据缓存策略,减少不必要的网络请求
- 重构应用初始化流程,优化权限服务和健康监听服务的启动顺序
- 移除冗余日志输出,提升应用性能
2025-11-18 14:08:20 +08:00

372 lines
11 KiB
TypeScript
Raw 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 { logger } from '@/utils/logger';
import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';
import { DeviceTokenRequest, pushNotificationService } from './pushNotifications';
// 设备令牌管理状态
export enum TokenStatus {
UNKNOWN = 'unknown',
GRANTED = 'granted',
DENIED = 'denied',
REGISTERED = 'registered',
FAILED = 'failed',
}
// 推送通知管理器配置
export interface PushNotificationConfig {
onTokenReceived?: (token: string) => void;
onTokenRefresh?: (token: string) => void;
onError?: (error: Error) => void;
}
/**
* 推送通知管理器类
* 负责iOS推送通知权限请求和设备令牌管理
*/
export class PushNotificationManager {
private static instance: PushNotificationManager;
private isInitialized = false;
private isInitializing = false;
private config: PushNotificationConfig = {};
private currentToken: string | null = null;
private constructor() { }
public static getInstance(): PushNotificationManager {
if (!PushNotificationManager.instance) {
PushNotificationManager.instance = new PushNotificationManager();
}
return PushNotificationManager.instance;
}
/**
* 初始化推送通知管理器
*/
async initialize(config: PushNotificationConfig = {}): Promise<boolean> {
if (this.isInitialized || this.isInitializing) {
return this.isInitialized;
}
try {
this.isInitializing = true;
this.config = config;
console.log('初始化推送通知管理器...');
// 检查设备是否支持推送通知
// 在Expo Go环境中某些推送功能可能受限
if (Platform.OS === 'web') {
console.warn('Web平台不支持推送通知功能');
this.config.onError?.(new Error('Web平台不支持推送通知功能'));
return false;
}
// 配置推送通知处理方式
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldPlaySound: true,
shouldSetBadge: true,
shouldShowBanner: true,
shouldShowList: true,
}),
});
// 请求推送通知权限
const hasPermission = await this.requestPermissions();
if (!hasPermission) {
console.warn('推送通知权限未授予');
return false;
}
// 获取设备令牌
const token = await this.getDeviceToken();
if (!token) {
console.error('获取设备令牌失败');
return false;
}
// 检查是否需要注册令牌
await this.checkAndRegisterToken(token);
// 设置令牌刷新监听器
this.setupTokenRefreshListener();
this.isInitialized = true;
console.log('推送通知管理器初始化成功');
return true;
} catch (error) {
console.error('推送通知管理器初始化失败:', error);
this.config.onError?.(error as Error);
return false;
} finally {
this.isInitializing = false;
}
}
/**
* 请求推送通知权限
*/
async requestPermissions(): Promise<boolean> {
try {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.warn('推送通知权限未授予:', finalStatus);
return false;
}
console.log('推送通知权限已授予');
return true;
} catch (error) {
console.error('请求推送通知权限失败:', error);
this.config.onError?.(error as Error);
return false;
}
}
/**
* 获取设备令牌
*/
async getDeviceToken(): Promise<string | null> {
try {
if (Platform.OS === 'ios') {
// iOS使用APNs令牌
const token = await Notifications.getDevicePushTokenAsync();
this.currentToken = token.data;
console.log('获取到iOS设备令牌:', token.data.substring(0, 20) + '...');
return token.data;
} else {
// Android使用FCM令牌虽然项目只支持iOS但保留代码完整性
const token = await Notifications.getDevicePushTokenAsync();
this.currentToken = token.data;
console.log('获取到Android设备令牌:', token.data.substring(0, 20) + '...');
return token.data;
}
} catch (error) {
console.error('获取设备令牌失败:', error);
this.config.onError?.(error as Error);
return null;
}
}
/**
* 检查并注册设备令牌
* 每次应用启动都会上报token更新用户ID绑定关系
*/
private async checkAndRegisterToken(token: string): Promise<void> {
try {
const storedToken = await pushNotificationService.getStoredDeviceToken();
const isRegistered = await pushNotificationService.isTokenRegistered();
// 如果令牌已改变或未注册,则重新注册
if (!isRegistered || storedToken !== token) {
await this.registerDeviceToken(token);
} else {
// 令牌已注册且未变化更新用户ID绑定关系
await this.updateTokenUserId(token);
}
} catch (error) {
console.error('检查和注册设备令牌失败:', error);
this.config.onError?.(error as Error);
}
}
/**
* 更新设备令牌的用户ID绑定关系
* 用户ID从服务端通过header中的token解析获取
*/
private async updateTokenUserId(token: string): Promise<boolean> {
try {
logger.info('更新设备令牌用户ID绑定:', token.substring(0, 20) + '...');
// 调用服务端API更新token用户ID绑定关系
const response = await pushNotificationService.updateTokenUserId(token);
console.log('设备令牌用户ID绑定更新成功:', response.tokenId);
return true;
} catch (error) {
console.error('更新设备令牌用户ID绑定失败:', error);
// 如果更新用户ID绑定失败尝试重新注册token
console.log('尝试重新注册设备令牌...');
return this.registerDeviceToken(token);
}
}
/**
* 用户登录成功后调用此方法更新token用户ID绑定
*/
async onUserLogin(): Promise<boolean> {
try {
const token = this.currentToken || await this.getDeviceToken();
if (!token) {
console.warn('未找到设备令牌无法更新用户ID绑定');
return false;
}
return await this.updateTokenUserId(token);
} catch (error) {
console.error('用户登录后更新token用户ID绑定失败:', error);
this.config.onError?.(error as Error);
return false;
}
}
/**
* 注册设备令牌到后端
*/
async registerDeviceToken(token?: string): Promise<boolean> {
try {
const deviceToken = token || this.currentToken || await this.getDeviceToken();
if (!deviceToken) {
throw new Error('设备令牌为空');
}
// 构建设备信息
const deviceInfo: DeviceTokenRequest = {
deviceToken,
deviceType: Platform.OS === 'ios' ? 'IOS' : 'ANDROID',
appVersion: Constants.expoConfig?.version || '1.0.0',
osVersion: `${Platform.OS} ${Platform.Version}`,
deviceName: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
};
logger.info('设备信息:', deviceInfo);
// 注册到后端
const response = await pushNotificationService.registerDeviceToken(deviceInfo);
console.log('设备令牌注册成功:', response.tokenId);
this.config.onTokenReceived?.(deviceToken);
return true;
} catch (error) {
console.error('注册设备令牌失败:', error);
this.config.onError?.(error as Error);
return false;
}
}
/**
* 更新设备令牌
*/
async updateDeviceToken(newToken: string): Promise<boolean> {
try {
const oldToken = await pushNotificationService.getStoredDeviceToken();
if (!oldToken) {
console.warn('未找到旧令牌,将执行新注册');
return this.registerDeviceToken(newToken);
}
const updateRequest = {
currentDeviceToken: oldToken,
newDeviceToken: newToken,
appVersion: Constants.expoConfig?.version || '1.0.0',
osVersion: `${Platform.OS} ${Platform.Version}`,
};
const response = await pushNotificationService.updateDeviceToken(updateRequest);
this.currentToken = newToken;
console.log('设备令牌更新成功:', response.tokenId);
this.config.onTokenRefresh?.(newToken);
return true;
} catch (error) {
console.error('更新设备令牌失败:', error);
this.config.onError?.(error as Error);
return false;
}
}
/**
* 注销设备令牌
*/
async unregisterDeviceToken(): Promise<boolean> {
try {
const token = this.currentToken || await pushNotificationService.getStoredDeviceToken();
if (!token) {
console.warn('未找到设备令牌,无需注销');
return true;
}
await pushNotificationService.unregisterDeviceToken(token);
this.currentToken = null;
console.log('设备令牌注销成功');
return true;
} catch (error) {
console.error('注销设备令牌失败:', error);
this.config.onError?.(error as Error);
return false;
}
}
/**
* 设置令牌刷新监听器
*/
private setupTokenRefreshListener(): void {
// 监听令牌变化iOS上通常不会频繁变化
Notifications.addNotificationResponseReceivedListener((response) => {
console.log('收到推送通知响应:', response);
});
}
/**
* 获取当前设备令牌
*/
getCurrentToken(): string | null {
return this.currentToken;
}
/**
* 获取令牌状态
*/
async getTokenStatus(): Promise<TokenStatus> {
try {
const hasPermission = await this.requestPermissions();
if (!hasPermission) {
return TokenStatus.DENIED;
}
const isRegistered = await pushNotificationService.isTokenRegistered();
if (isRegistered) {
return TokenStatus.REGISTERED;
}
const token = await this.getDeviceToken();
return token ? TokenStatus.GRANTED : TokenStatus.FAILED;
} catch (error) {
console.error('获取令牌状态失败:', error);
return TokenStatus.FAILED;
}
}
/**
* 清除所有本地数据
*/
async clearAllData(): Promise<void> {
try {
await pushNotificationService.clearLocalTokenData();
this.currentToken = null;
this.isInitialized = false;
console.log('推送通知管理器数据已清除');
} catch (error) {
console.error('清除推送通知管理器数据失败:', error);
}
}
}
// 导出单例实例
export const pushNotificationManager = PushNotificationManager.getInstance();