Files
vf_react/src/services/notificationMetricsService.js
2025-10-21 17:50:21 +08:00

451 lines
12 KiB
JavaScript
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.

// 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;