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

403 lines
12 KiB
JavaScript

// src/services/notificationHistoryService.js
/**
* 通知历史记录服务
* 持久化存储通知历史,支持查询、筛选、搜索、导出
*/
import { logger } from '../utils/logger';
const STORAGE_KEY = 'notification_history';
const MAX_HISTORY_SIZE = 500; // 最多保留 500 条历史记录
class NotificationHistoryService {
constructor() {
this.history = this.loadHistory();
}
/**
* 从 localStorage 加载历史记录
*/
loadHistory() {
try {
const data = localStorage.getItem(STORAGE_KEY);
if (data) {
const parsed = JSON.parse(data);
// 确保是数组
return Array.isArray(parsed) ? parsed : [];
}
} catch (error) {
logger.error('notificationHistoryService', 'loadHistory', error);
}
return [];
}
/**
* 保存历史记录到 localStorage
*/
saveHistory() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.history));
logger.debug('notificationHistoryService', 'History saved', {
count: this.history.length
});
} catch (error) {
logger.error('notificationHistoryService', 'saveHistory', error);
// localStorage 可能已满,尝试清理旧数据
if (error.name === 'QuotaExceededError') {
this.cleanup(100); // 清理 100 条
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.history));
} catch (retryError) {
logger.error('notificationHistoryService', 'saveHistory retry failed', retryError);
}
}
}
}
/**
* 保存通知到历史记录
* @param {object} notification - 通知对象
*/
saveNotification(notification) {
const record = {
id: notification.id || `notif_${Date.now()}`,
notification: { ...notification }, // 深拷贝
receivedAt: Date.now(),
readAt: null,
clickedAt: null,
};
// 检查是否已存在(去重)
const existingIndex = this.history.findIndex(r => r.id === record.id);
if (existingIndex !== -1) {
logger.debug('notificationHistoryService', 'Notification already exists', { id: record.id });
return;
}
// 添加到历史记录开头
this.history.unshift(record);
// 限制最大数量
if (this.history.length > MAX_HISTORY_SIZE) {
this.history = this.history.slice(0, MAX_HISTORY_SIZE);
}
this.saveHistory();
logger.info('notificationHistoryService', 'Notification saved', { id: record.id });
}
/**
* 标记通知为已读
* @param {string} id - 通知 ID
*/
markAsRead(id) {
const record = this.history.find(r => r.id === id);
if (record && !record.readAt) {
record.readAt = Date.now();
this.saveHistory();
logger.debug('notificationHistoryService', 'Marked as read', { id });
}
}
/**
* 标记通知为已点击
* @param {string} id - 通知 ID
*/
markAsClicked(id) {
const record = this.history.find(r => r.id === id);
if (record && !record.clickedAt) {
record.clickedAt = Date.now();
// 点击也意味着已读
if (!record.readAt) {
record.readAt = Date.now();
}
this.saveHistory();
logger.debug('notificationHistoryService', 'Marked as clicked', { id });
}
}
/**
* 获取历史记录
* @param {object} filters - 筛选条件
* @param {string} filters.type - 通知类型
* @param {string} filters.priority - 优先级
* @param {string} filters.readStatus - 阅读状态 ('read' | 'unread' | 'all')
* @param {number} filters.startDate - 开始日期(时间戳)
* @param {number} filters.endDate - 结束日期(时间戳)
* @param {number} filters.page - 页码(从 1 开始)
* @param {number} filters.pageSize - 每页数量
* @returns {object} - { records, total, page, pageSize }
*/
getHistory(filters = {}) {
let filtered = [...this.history];
// 按类型筛选
if (filters.type && filters.type !== 'all') {
filtered = filtered.filter(r => r.notification.type === filters.type);
}
// 按优先级筛选
if (filters.priority && filters.priority !== 'all') {
filtered = filtered.filter(r => r.notification.priority === filters.priority);
}
// 按阅读状态筛选
if (filters.readStatus === 'read') {
filtered = filtered.filter(r => r.readAt !== null);
} else if (filters.readStatus === 'unread') {
filtered = filtered.filter(r => r.readAt === null);
}
// 按日期范围筛选
if (filters.startDate) {
filtered = filtered.filter(r => r.receivedAt >= filters.startDate);
}
if (filters.endDate) {
filtered = filtered.filter(r => r.receivedAt <= filters.endDate);
}
// 分页
const page = filters.page || 1;
const pageSize = filters.pageSize || 20;
const total = filtered.length;
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const records = filtered.slice(startIndex, endIndex);
return {
records,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
/**
* 搜索历史记录
* @param {string} keyword - 搜索关键词
* @returns {array} - 匹配的记录
*/
searchHistory(keyword) {
if (!keyword || keyword.trim() === '') {
return [];
}
const lowerKeyword = keyword.toLowerCase();
const results = this.history.filter(record => {
const { title, content, message } = record.notification;
const searchText = `${title || ''} ${content || ''} ${message || ''}`.toLowerCase();
return searchText.includes(lowerKeyword);
});
logger.info('notificationHistoryService', 'Search completed', {
keyword,
resultsCount: results.length
});
return results;
}
/**
* 删除单条历史记录
* @param {string} id - 通知 ID
*/
deleteRecord(id) {
const initialLength = this.history.length;
this.history = this.history.filter(r => r.id !== id);
if (this.history.length < initialLength) {
this.saveHistory();
logger.info('notificationHistoryService', 'Record deleted', { id });
return true;
}
return false;
}
/**
* 批量删除历史记录
* @param {array} ids - 通知 ID 数组
*/
deleteRecords(ids) {
const initialLength = this.history.length;
this.history = this.history.filter(r => !ids.includes(r.id));
const deletedCount = initialLength - this.history.length;
if (deletedCount > 0) {
this.saveHistory();
logger.info('notificationHistoryService', 'Batch delete completed', {
deletedCount
});
}
return deletedCount;
}
/**
* 清空所有历史记录
*/
clearHistory() {
this.history = [];
this.saveHistory();
logger.info('notificationHistoryService', 'History cleared');
}
/**
* 清理旧数据
* @param {number} count - 要清理的数量
*/
cleanup(count) {
if (this.history.length > count) {
this.history = this.history.slice(0, -count);
this.saveHistory();
logger.info('notificationHistoryService', 'Cleanup completed', { count });
}
}
/**
* 获取统计数据
* @returns {object} - 统计信息
*/
getStats() {
const total = this.history.length;
const read = this.history.filter(r => r.readAt !== null).length;
const unread = total - read;
const clicked = this.history.filter(r => r.clickedAt !== null).length;
// 按类型统计
const byType = {};
const byPriority = {};
this.history.forEach(record => {
const { type, priority } = record.notification;
// 类型统计
if (type) {
byType[type] = (byType[type] || 0) + 1;
}
// 优先级统计
if (priority) {
byPriority[priority] = (byPriority[priority] || 0) + 1;
}
});
// 计算点击率
const clickRate = total > 0 ? ((clicked / total) * 100).toFixed(2) : '0.00';
return {
total,
read,
unread,
clicked,
clickRate,
byType,
byPriority,
};
}
/**
* 导出历史记录为 JSON
* @param {object} filters - 筛选条件(可选)
*/
exportToJSON(filters = {}) {
const { records } = this.getHistory(filters);
const exportData = {
exportedAt: new Date().toISOString(),
total: records.length,
records: records.map(r => ({
id: r.id,
notification: r.notification,
receivedAt: new Date(r.receivedAt).toISOString(),
readAt: r.readAt ? new Date(r.readAt).toISOString() : null,
clickedAt: r.clickedAt ? new Date(r.clickedAt).toISOString() : null,
})),
};
return JSON.stringify(exportData, null, 2);
}
/**
* 导出历史记录为 CSV
* @param {object} filters - 筛选条件(可选)
*/
exportToCSV(filters = {}) {
const { records } = this.getHistory(filters);
let csv = 'ID,Type,Priority,Title,Content,Received At,Read At,Clicked At\n';
records.forEach(record => {
const { id, notification, receivedAt, readAt, clickedAt } = record;
const { type, priority, title, content, message } = notification;
const escapeCsv = (str) => {
if (!str) return '';
return `"${String(str).replace(/"/g, '""')}"`;
};
csv += [
escapeCsv(id),
escapeCsv(type),
escapeCsv(priority),
escapeCsv(title),
escapeCsv(content || message),
new Date(receivedAt).toISOString(),
readAt ? new Date(readAt).toISOString() : '',
clickedAt ? new Date(clickedAt).toISOString() : '',
].join(',') + '\n';
});
return csv;
}
/**
* 触发下载文件
* @param {string} content - 文件内容
* @param {string} filename - 文件名
* @param {string} mimeType - MIME 类型
*/
downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
logger.info('notificationHistoryService', 'File downloaded', { filename });
}
/**
* 导出并下载 JSON 文件
* @param {object} filters - 筛选条件(可选)
*/
downloadJSON(filters = {}) {
const json = this.exportToJSON(filters);
const filename = `notifications_${new Date().toISOString().split('T')[0]}.json`;
this.downloadFile(json, filename, 'application/json');
}
/**
* 导出并下载 CSV 文件
* @param {object} filters - 筛选条件(可选)
*/
downloadCSV(filters = {}) {
const csv = this.exportToCSV(filters);
const filename = `notifications_${new Date().toISOString().split('T')[0]}.csv`;
this.downloadFile(csv, filename, 'text/csv');
}
}
// 导出单例
export const notificationHistoryService = new NotificationHistoryService();
export default notificationHistoryService;