// src/services/notificationMetricsService.js /** * 通知性能监控服务 * 追踪推送到达率、点击率、响应时间等指标 */ import { logger } from '../utils/logger'; const STORAGE_KEY = 'notification_metrics'; const MAX_HISTORY_DAYS = 30; // 保留最近 30 天数据 class NotificationMetricsService { constructor() { this.metrics = this.loadMetrics(); } /** * 从 localStorage 加载指标数据 */ loadMetrics() { try { const data = localStorage.getItem(STORAGE_KEY); if (data) { const parsed = JSON.parse(data); // 清理过期数据 this.cleanOldData(parsed); return parsed; } } catch (error) { logger.error('notificationMetricsService', 'loadMetrics', error); } // 返回默认结构 return this.getDefaultMetrics(); } /** * 获取默认指标结构 */ getDefaultMetrics() { return { summary: { totalSent: 0, totalReceived: 0, totalClicked: 0, totalDismissed: 0, totalResponseTime: 0, // 总响应时间(毫秒) }, byType: { announcement: { sent: 0, received: 0, clicked: 0, dismissed: 0 }, stock_alert: { sent: 0, received: 0, clicked: 0, dismissed: 0 }, event_alert: { sent: 0, received: 0, clicked: 0, dismissed: 0 }, analysis_report: { sent: 0, received: 0, clicked: 0, dismissed: 0 }, }, byPriority: { urgent: { sent: 0, received: 0, clicked: 0, dismissed: 0 }, important: { sent: 0, received: 0, clicked: 0, dismissed: 0 }, normal: { sent: 0, received: 0, clicked: 0, dismissed: 0 }, }, hourlyDistribution: Array(24).fill(0), // 每小时推送分布 dailyData: {}, // 按日期存储的每日数据 lastUpdated: Date.now(), }; } /** * 保存指标数据到 localStorage */ saveMetrics() { try { this.metrics.lastUpdated = Date.now(); localStorage.setItem(STORAGE_KEY, JSON.stringify(this.metrics)); logger.debug('notificationMetricsService', 'Metrics saved', this.metrics.summary); } catch (error) { logger.error('notificationMetricsService', 'saveMetrics', error); } } /** * 清理过期数据 */ cleanOldData(metrics) { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - MAX_HISTORY_DAYS); const cutoffDateStr = cutoffDate.toISOString().split('T')[0]; if (metrics.dailyData) { Object.keys(metrics.dailyData).forEach(date => { if (date < cutoffDateStr) { delete metrics.dailyData[date]; } }); } } /** * 获取当前日期字符串 */ getTodayDateStr() { return new Date().toISOString().split('T')[0]; } /** * 获取当前小时 */ getCurrentHour() { return new Date().getHours(); } /** * 确保每日数据结构存在 */ ensureDailyData(dateStr) { if (!this.metrics.dailyData[dateStr]) { this.metrics.dailyData[dateStr] = { sent: 0, received: 0, clicked: 0, dismissed: 0, }; } } /** * 追踪通知发送 * @param {object} notification - 通知对象 */ trackSent(notification) { const { type, priority } = notification; const today = this.getTodayDateStr(); const hour = this.getCurrentHour(); // 汇总统计 this.metrics.summary.totalSent++; // 按类型统计 if (this.metrics.byType[type]) { this.metrics.byType[type].sent++; } // 按优先级统计 if (priority && this.metrics.byPriority[priority]) { this.metrics.byPriority[priority].sent++; } // 每小时分布 this.metrics.hourlyDistribution[hour]++; // 每日数据 this.ensureDailyData(today); this.metrics.dailyData[today].sent++; this.saveMetrics(); logger.debug('notificationMetricsService', 'Tracked sent', { type, priority }); } /** * 追踪通知接收 * @param {object} notification - 通知对象 */ trackReceived(notification) { const { type, priority, id, timestamp } = notification; const today = this.getTodayDateStr(); // 汇总统计 this.metrics.summary.totalReceived++; // 按类型统计 if (this.metrics.byType[type]) { this.metrics.byType[type].received++; } // 按优先级统计 if (priority && this.metrics.byPriority[priority]) { this.metrics.byPriority[priority].received++; } // 每日数据 this.ensureDailyData(today); this.metrics.dailyData[today].received++; // 存储接收时间(用于计算响应时间) this.storeReceivedTime(id, timestamp || Date.now()); this.saveMetrics(); logger.debug('notificationMetricsService', 'Tracked received', { type, priority }); } /** * 追踪通知点击 * @param {object} notification - 通知对象 */ trackClicked(notification) { const { type, priority, id } = notification; const today = this.getTodayDateStr(); const clickTime = Date.now(); // 汇总统计 this.metrics.summary.totalClicked++; // 按类型统计 if (this.metrics.byType[type]) { this.metrics.byType[type].clicked++; } // 按优先级统计 if (priority && this.metrics.byPriority[priority]) { this.metrics.byPriority[priority].clicked++; } // 每日数据 this.ensureDailyData(today); this.metrics.dailyData[today].clicked++; // 计算响应时间 const receivedTime = this.getReceivedTime(id); if (receivedTime) { const responseTime = clickTime - receivedTime; this.metrics.summary.totalResponseTime += responseTime; this.removeReceivedTime(id); logger.debug('notificationMetricsService', 'Response time', { responseTime }); } this.saveMetrics(); logger.debug('notificationMetricsService', 'Tracked clicked', { type, priority }); } /** * 追踪通知关闭 * @param {object} notification - 通知对象 */ trackDismissed(notification) { const { type, priority, id } = notification; const today = this.getTodayDateStr(); // 汇总统计 this.metrics.summary.totalDismissed++; // 按类型统计 if (this.metrics.byType[type]) { this.metrics.byType[type].dismissed++; } // 按优先级统计 if (priority && this.metrics.byPriority[priority]) { this.metrics.byPriority[priority].dismissed++; } // 每日数据 this.ensureDailyData(today); this.metrics.dailyData[today].dismissed++; // 清理接收时间记录 this.removeReceivedTime(id); this.saveMetrics(); logger.debug('notificationMetricsService', 'Tracked dismissed', { type, priority }); } /** * 存储通知接收时间(用于计算响应时间) */ storeReceivedTime(id, timestamp) { if (!this.receivedTimes) { this.receivedTimes = new Map(); } this.receivedTimes.set(id, timestamp); } /** * 获取通知接收时间 */ getReceivedTime(id) { if (!this.receivedTimes) { return null; } return this.receivedTimes.get(id); } /** * 移除通知接收时间记录 */ removeReceivedTime(id) { if (this.receivedTimes) { this.receivedTimes.delete(id); } } /** * 获取汇总统计 */ getSummary() { const summary = { ...this.metrics.summary }; // 计算平均响应时间 if (summary.totalClicked > 0) { summary.avgResponseTime = Math.round(summary.totalResponseTime / summary.totalClicked); } else { summary.avgResponseTime = 0; } // 计算点击率 if (summary.totalReceived > 0) { summary.clickRate = ((summary.totalClicked / summary.totalReceived) * 100).toFixed(2); } else { summary.clickRate = '0.00'; } // 计算到达率(假设 sent = received) if (summary.totalSent > 0) { summary.deliveryRate = ((summary.totalReceived / summary.totalSent) * 100).toFixed(2); } else { summary.deliveryRate = '100.00'; } return summary; } /** * 获取按类型统计 */ getByType() { const result = {}; Object.keys(this.metrics.byType).forEach(type => { const data = this.metrics.byType[type]; result[type] = { ...data, clickRate: data.received > 0 ? ((data.clicked / data.received) * 100).toFixed(2) : '0.00', }; }); return result; } /** * 获取按优先级统计 */ getByPriority() { const result = {}; Object.keys(this.metrics.byPriority).forEach(priority => { const data = this.metrics.byPriority[priority]; result[priority] = { ...data, clickRate: data.received > 0 ? ((data.clicked / data.received) * 100).toFixed(2) : '0.00', }; }); return result; } /** * 获取每小时分布 */ getHourlyDistribution() { return [...this.metrics.hourlyDistribution]; } /** * 获取每日数据 * @param {number} days - 获取最近多少天的数据 */ getDailyData(days = 7) { const result = []; const today = new Date(); for (let i = days - 1; i >= 0; i--) { const date = new Date(today); date.setDate(date.getDate() - i); const dateStr = date.toISOString().split('T')[0]; const data = this.metrics.dailyData[dateStr] || { sent: 0, received: 0, clicked: 0, dismissed: 0, }; result.push({ date: dateStr, ...data, clickRate: data.received > 0 ? ((data.clicked / data.received) * 100).toFixed(2) : '0.00', }); } return result; } /** * 获取完整指标数据 */ getAllMetrics() { return { summary: this.getSummary(), byType: this.getByType(), byPriority: this.getByPriority(), hourlyDistribution: this.getHourlyDistribution(), dailyData: this.getDailyData(30), lastUpdated: this.metrics.lastUpdated, }; } /** * 重置所有指标 */ reset() { this.metrics = this.getDefaultMetrics(); this.saveMetrics(); logger.info('notificationMetricsService', 'Metrics reset'); } /** * 导出指标数据为 JSON */ exportToJSON() { return JSON.stringify(this.getAllMetrics(), null, 2); } /** * 导出指标数据为 CSV */ exportToCSV() { const summary = this.getSummary(); const dailyData = this.getDailyData(30); let csv = '# Summary\n'; csv += 'Metric,Value\n'; csv += `Total Sent,${summary.totalSent}\n`; csv += `Total Received,${summary.totalReceived}\n`; csv += `Total Clicked,${summary.totalClicked}\n`; csv += `Total Dismissed,${summary.totalDismissed}\n`; csv += `Delivery Rate,${summary.deliveryRate}%\n`; csv += `Click Rate,${summary.clickRate}%\n`; csv += `Avg Response Time,${summary.avgResponseTime}ms\n\n`; csv += '# Daily Data\n'; csv += 'Date,Sent,Received,Clicked,Dismissed,Click Rate\n'; dailyData.forEach(day => { csv += `${day.date},${day.sent},${day.received},${day.clicked},${day.dismissed},${day.clickRate}%\n`; }); return csv; } } // 导出单例 export const notificationMetricsService = new NotificationMetricsService(); export default notificationMetricsService;