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