feat: sockt 弹窗功能添加
This commit is contained in:
450
src/services/notificationMetricsService.js
Normal file
450
src/services/notificationMetricsService.js
Normal file
@@ -0,0 +1,450 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user