feat(hrv): 添加心率变异性监控和压力评估功能

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

View File

@@ -16,21 +16,23 @@ interface LogEntry {
* 3. 改进的错误处理和重试机制
* 4. 优化的 ID 生成 - 确保唯一性
* 5. 写入确认机制 - 返回 Promise 让调用者知道日志是否成功保存
* 6. 修复日志丢失问题 - 确保所有日志都能被正确保存
*/
class Logger {
private static instance: Logger;
private readonly maxLogs = 1000; // 最多保存1000条日志
private readonly maxLogs = 2000; // 最多保存2000条日志
private readonly storageKey = '@app_logs';
// 内存队列相关
private memoryQueue: LogEntry[] = [];
private readonly queueMaxSize = 50; // 达到50条日志时触发批量写入
private readonly queueMaxSize = 10; // 达到10条日志时触发批量写入
private readonly flushInterval = 5000; // 5秒自动刷新一次
private flushTimer: ReturnType<typeof setInterval> | null = null;
// 写入锁相关
private isWriting = false;
private writePromise: Promise<void> | null = null;
private pendingFlushes: Array<() => void> = []; // 等待刷新完成的回调队列
// ID 生成相关
private idCounter = 0;
@@ -39,6 +41,9 @@ class Logger {
// 重试相关
private readonly maxRetries = 3;
private readonly retryDelay = 1000; // 1秒
// 应用退出标志
private isShuttingDown = false;
static getInstance(): Logger {
if (!Logger.instance) {
@@ -81,16 +86,38 @@ class Logger {
private setupAppExitHandler(): void {
// 这是一个最佳努力的清理,不是所有场景都能捕获
if (typeof process !== 'undefined' && process.on) {
const cleanup = () => {
const cleanup = async () => {
this.isShuttingDown = true;
if (this.memoryQueue.length > 0) {
// 同步刷新(应用退出时)
this.flushQueueSync();
console.log('[Logger] App exiting, flushing remaining logs...');
try {
// 在退出时等待刷新完成
await this.flushQueue();
console.log('[Logger] Logs flushed successfully on exit');
} catch (error) {
console.error('[Logger] Failed to flush logs on exit:', error);
}
}
};
process.on('exit', cleanup);
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
process.on('exit', () => {
// exit 事件不能异步,只能尝试
if (this.memoryQueue.length > 0) {
console.warn('[Logger] Process exiting with unflushed logs');
}
});
// 这些信号可以异步处理
process.on('SIGINT', async () => {
await cleanup();
process.exit(0);
});
process.on('SIGTERM', async () => {
await cleanup();
process.exit(0);
});
}
}
@@ -207,12 +234,12 @@ class Logger {
/**
* 刷新队列到存储(异步,带锁)
* 修复:确保在写入失败时不丢失日志,并正确处理并发刷新请求
*/
private async flushQueue(): Promise<void> {
// 如果正在写入,等待当前写入完成
// 如果正在写入,返回当前写入 Promise
if (this.isWriting && this.writePromise) {
await this.writePromise;
return;
return this.writePromise;
}
// 如果队列为空,直接返回
@@ -223,10 +250,9 @@ class Logger {
// 设置写入锁
this.isWriting = true;
// 保存要写入的日志(避免在写入过程中队列被修改
// 复制要写入的日志(但不立即清空队列,避免失败时丢失
const logsToWrite = [...this.memoryQueue];
this.memoryQueue = [];
this.writePromise = (async () => {
try {
// 获取现有日志
@@ -238,19 +264,27 @@ class Logger {
// 保存到存储
await this.saveLogs(allLogs);
console.log(`[Logger] Successfully flushed ${logsToWrite.length} logs to storage`);
// 写入成功后才从队列中移除这些日志
// 使用 filter 而不是直接赋值,因为在写入过程中可能有新日志添加
const writtenIds = new Set(logsToWrite.map(log => log.id));
this.memoryQueue = this.memoryQueue.filter(log => !writtenIds.has(log.id));
console.log(`[Logger] Successfully flushed ${logsToWrite.length} logs to storage, remaining: ${this.memoryQueue.length}`);
// 通知等待的刷新请求
this.notifyPendingFlushes();
} catch (error) {
console.error('[Logger] Failed to flush queue:', error);
// 写入失败,将日志放回队列(保留在内存中)
this.memoryQueue.unshift(...logsToWrite);
// 限制队列大小,避免内存溢出
// 写入失败时,日志已经在队列中,无需重新添加
// 但要限制队列大小,避免内存溢出
if (this.memoryQueue.length > this.maxLogs) {
const overflow = this.memoryQueue.length - this.maxLogs;
console.warn(`[Logger] Queue overflow, dropping ${overflow} oldest logs`);
this.memoryQueue = this.memoryQueue.slice(-this.maxLogs);
}
throw error; // 重新抛出错误,让调用者知道刷新失败
} finally {
// 释放写入锁
this.isWriting = false;
@@ -258,37 +292,34 @@ class Logger {
}
})();
await this.writePromise;
return this.writePromise;
}
/**
* 同步刷新队列(应用退出时使用)
* 通知等待刷新完成的回调
*/
private flushQueueSync(): void {
if (this.memoryQueue.length === 0) {
private notifyPendingFlushes(): void {
const callbacks = [...this.pendingFlushes];
this.pendingFlushes = [];
callbacks.forEach(callback => callback());
}
/**
* 等待当前刷新完成
*/
private async waitForFlush(): Promise<void> {
if (!this.isWriting) {
return;
}
try {
// 注意:这是一个阻塞操作,仅在应用退出时使用
const logsToWrite = [...this.memoryQueue];
this.memoryQueue = [];
// 这里我们无法使用异步操作,只能尝试
console.log(`[Logger] Attempting to flush ${logsToWrite.length} logs synchronously`);
// 实际上在 React Native 中很难做到真正的同步保存
// 这里只是一个最佳努力的尝试
this.flushQueue().catch(error => {
console.error('[Logger] Sync flush failed:', error);
});
} catch (error) {
console.error('[Logger] Failed to flush queue synchronously:', error);
}
return new Promise<void>((resolve) => {
this.pendingFlushes.push(resolve);
});
}
/**
* 添加日志到队列
* 修复:确保在达到阈值时正确处理刷新,避免日志丢失
*/
private async addLog(level: LogEntry['level'], message: string, data?: any): Promise<void> {
// 序列化数据
@@ -320,10 +351,13 @@ class Logger {
// 检查是否需要刷新队列
if (this.memoryQueue.length >= this.queueMaxSize) {
// 不等待刷新完成,避免阻塞调用者
this.flushQueue().catch(error => {
try {
// 等待刷新完成,确保日志不会丢失
await this.flushQueue();
} catch (error) {
// 刷新失败时,日志仍在队列中,会在下次刷新时重试
console.error('[Logger] Failed to flush queue after size threshold:', error);
});
}
}
}
@@ -351,10 +385,21 @@ class Logger {
*/
async getAllLogs(): Promise<LogEntry[]> {
// 先刷新队列
await this.flushQueue();
try {
await this.flushQueue();
} catch (error) {
console.warn('[Logger] Failed to flush before getting logs:', error);
}
// 然后获取存储中的日志
return await this.getLogs();
const storedLogs = await this.getLogs();
// 如果还有未刷新的日志(刷新失败的情况),也包含进来
if (this.memoryQueue.length > 0) {
return [...storedLogs, ...this.memoryQueue];
}
return storedLogs;
}
/**
@@ -365,6 +410,9 @@ class Logger {
// 清空内存队列
this.memoryQueue = [];
// 等待当前写入完成
await this.waitForFlush();
// 清除存储
await AsyncStorage.removeItem(this.storageKey);
@@ -379,11 +427,8 @@ class Logger {
* 导出日志
*/
async exportLogs(): Promise<string> {
// 先刷新队列
await this.flushQueue();
// 然后获取并导出日志
const logs = await this.getLogs();
// 获取所有日志(包括未刷新的)
const logs = await this.getAllLogs();
return JSON.stringify(logs, null, 2);
}
@@ -397,25 +442,36 @@ class Logger {
/**
* 获取队列状态(用于调试)
*/
getQueueStatus(): { queueSize: number; isWriting: boolean } {
getQueueStatus(): { queueSize: number; isWriting: boolean; isShuttingDown: boolean } {
return {
queueSize: this.memoryQueue.length,
isWriting: this.isWriting
isWriting: this.isWriting,
isShuttingDown: this.isShuttingDown
};
}
/**
* 清理资源
*/
destroy(): void {
async destroy(): Promise<void> {
// 设置关闭标志
this.isShuttingDown = true;
// 停止定时器
if (this.flushTimer) {
clearInterval(this.flushTimer);
this.flushTimer = null;
}
// 最后刷新一次
// 最后刷新一次,确保所有日志都被保存
if (this.memoryQueue.length > 0) {
this.flushQueueSync();
try {
console.log('[Logger] Destroying logger, flushing remaining logs...');
await this.flushQueue();
console.log('[Logger] Logger destroyed successfully');
} catch (error) {
console.error('[Logger] Failed to flush logs on destroy:', error);
}
}
}
}
@@ -433,6 +489,10 @@ export const log = {
// 额外的工具函数
flush: () => logger.flush(),
getQueueStatus: () => logger.getQueueStatus(),
getAllLogs: () => logger.getAllLogs(),
clearLogs: () => logger.clearLogs(),
exportLogs: () => logger.exportLogs(),
destroy: () => logger.destroy(),
};
export type { LogEntry };

65
utils/stress.ts Normal file
View File

@@ -0,0 +1,65 @@
/**
* HRV 与压力指数转换工具
*
* 提供统一的 HRV -> 压力指数映射及压力等级判定
*/
export type StressLevel = 'low' | 'moderate' | 'high';
export interface StressLevelInfo {
level: StressLevel;
label: string;
description: string;
}
const MIN_HRV = 30;
const MAX_HRV = 130;
/**
* 将 HRVms转换为压力指数0-100数值越高表示压力越大
*/
export function convertHrvToStressIndex(hrv: number | null | undefined): number | null {
if (hrv == null || hrv <= 0) {
return null;
}
const clamped = Math.max(MIN_HRV, Math.min(MAX_HRV, hrv));
const normalized = 100 - ((clamped - MIN_HRV) / (MAX_HRV - MIN_HRV)) * 100;
return Math.round(normalized);
}
/**
* 根据压力指数获取压力等级信息
*/
export function getStressLevelInfo(stressIndex: number | null): StressLevelInfo {
if (stressIndex == null) {
return {
level: 'moderate',
label: '压力未知',
description: '暂无有效的 HRV 数据',
};
}
if (stressIndex >= 70) {
return {
level: 'high',
label: '压力偏高',
description: '建议立即放松,关注呼吸与休息',
};
}
if (stressIndex >= 40) {
return {
level: 'moderate',
label: '压力适中',
description: '保持当前节奏,注意劳逸结合',
};
}
return {
level: 'low',
label: '状态放松',
description: '身心良好,继续保持',
};
}