// src/utils/CacheManager.js import { logger } from './logger'; /** * 缓存过期策略 */ export const CACHE_EXPIRY_STRATEGY = { MIDNIGHT: 'midnight', // 当天午夜过期 HOURS: 'hours', // 指定小时后过期 NEVER: 'never' // 永不过期 }; /** * 缓存管理器类 * 提供统一的缓存操作接口,支持多种过期策略 */ class CacheManager { constructor(storage = localStorage, logContext = 'CacheManager') { this.storage = storage; this.logContext = logContext; } /** * 计算过期时间 * @param {string} strategy - 过期策略 * @param {number} hours - 小时数(当策略为 HOURS 时使用) * @returns {string|null} ISO 格式的过期时间,或 null(永不过期) */ _calculateExpireTime(strategy = CACHE_EXPIRY_STRATEGY.MIDNIGHT, hours = 24) { if (strategy === CACHE_EXPIRY_STRATEGY.NEVER) { return null; } const expireDate = new Date(); if (strategy === CACHE_EXPIRY_STRATEGY.MIDNIGHT) { // 设置为明天凌晨 0 点 expireDate.setDate(expireDate.getDate() + 1); expireDate.setHours(0, 0, 0, 0); } else if (strategy === CACHE_EXPIRY_STRATEGY.HOURS) { // 设置为指定小时后 expireDate.setHours(expireDate.getHours() + hours); } return expireDate.toISOString(); } /** * 检查缓存是否过期 * @param {string|null} expireAt - 过期时间(ISO 格式) * @returns {boolean} 是否过期 */ _isExpired(expireAt) { if (!expireAt) return false; // null 表示永不过期 return new Date() > new Date(expireAt); } /** * 获取缓存数据 * @param {string} key - 缓存键名 * @returns {any|null} 缓存的数据,如果不存在或已过期返回 null */ get(key) { try { const cached = this.storage.getItem(key); if (!cached) return null; const { data, expireAt, cachedAt } = JSON.parse(cached); // 检查是否过期 if (this._isExpired(expireAt)) { this.remove(key); logger.debug(this.logContext, '缓存已过期', { key, expireAt }); return null; } logger.debug(this.logContext, '使用缓存数据', { key, dataLength: Array.isArray(data) ? data.length : typeof data, expireAt, cachedAt }); return data; } catch (error) { logger.error(this.logContext, 'get 缓存失败', error, { key }); // 清除损坏的缓存 this.remove(key); return null; } } /** * 设置缓存数据 * @param {string} key - 缓存键名 * @param {any} data - 要缓存的数据 * @param {string} strategy - 过期策略 * @param {number} hours - 小时数(当策略为 HOURS 时使用) * @returns {boolean} 是否设置成功 */ set(key, data, strategy = CACHE_EXPIRY_STRATEGY.MIDNIGHT, hours = 24) { try { const cacheData = { data, expireAt: this._calculateExpireTime(strategy, hours), cachedAt: new Date().toISOString(), strategy }; this.storage.setItem(key, JSON.stringify(cacheData)); logger.debug(this.logContext, '数据已缓存', { key, dataLength: Array.isArray(data) ? data.length : typeof data, expireAt: cacheData.expireAt, strategy }); return true; } catch (error) { logger.error(this.logContext, 'set 缓存失败', error, { key }); // 处理 localStorage 配额已满 if (error.name === 'QuotaExceededError') { logger.warn(this.logContext, 'Storage 配额已满,尝试清理部分缓存'); this._handleQuotaExceeded(); // 清理后重试一次 try { this.storage.setItem(key, JSON.stringify({ data, expireAt: this._calculateExpireTime(strategy, hours), cachedAt: new Date().toISOString(), strategy })); return true; } catch (retryError) { logger.error(this.logContext, '重试 set 缓存仍失败', retryError, { key }); return false; } } return false; } } /** * 删除指定缓存 * @param {string} key - 缓存键名 */ remove(key) { try { this.storage.removeItem(key); logger.debug(this.logContext, '缓存已删除', { key }); } catch (error) { logger.error(this.logContext, 'remove 缓存失败', error, { key }); } } /** * 批量删除缓存 * @param {string[]} keys - 缓存键名数组 */ removeMultiple(keys) { keys.forEach(key => this.remove(key)); logger.info(this.logContext, '批量删除缓存完成', { count: keys.length }); } /** * 清除所有缓存 */ clear() { try { this.storage.clear(); logger.info(this.logContext, '所有缓存已清除'); } catch (error) { logger.error(this.logContext, 'clear 缓存失败', error); } } /** * 检查缓存是否存在且有效 * @param {string} key - 缓存键名 * @returns {boolean} 是否存在有效缓存 */ has(key) { return this.get(key) !== null; } /** * 获取缓存元数据(不包含数据本身) * @param {string} key - 缓存键名 * @returns {Object|null} 元数据对象 { expireAt, cachedAt, strategy } */ getMetadata(key) { try { const cached = this.storage.getItem(key); if (!cached) return null; const { expireAt, cachedAt, strategy } = JSON.parse(cached); return { expireAt, cachedAt, strategy, isExpired: this._isExpired(expireAt) }; } catch (error) { logger.error(this.logContext, 'getMetadata 失败', error, { key }); return null; } } /** * 处理存储配额已满的情况 * 优先删除最旧的或已过期的缓存 * @private */ _handleQuotaExceeded() { try { const cacheItems = []; // 收集所有缓存项 for (let i = 0; i < this.storage.length; i++) { const key = this.storage.key(i); if (!key) continue; try { const cached = this.storage.getItem(key); const { cachedAt, expireAt } = JSON.parse(cached); cacheItems.push({ key, cachedAt: new Date(cachedAt), expireAt: expireAt ? new Date(expireAt) : null, isExpired: this._isExpired(expireAt) }); } catch (e) { // 解析失败的项直接删除 this.storage.removeItem(key); } } // 按优先级排序:已过期 > 最旧的 cacheItems.sort((a, b) => { if (a.isExpired && !b.isExpired) return -1; if (!a.isExpired && b.isExpired) return 1; return a.cachedAt - b.cachedAt; }); // 删除前 20% 的缓存 const deleteCount = Math.max(1, Math.floor(cacheItems.length * 0.2)); for (let i = 0; i < deleteCount; i++) { this.storage.removeItem(cacheItems[i].key); } logger.info(this.logContext, `已清理 ${deleteCount} 个缓存项`); } catch (error) { logger.error(this.logContext, '_handleQuotaExceeded 失败', error); // 最后手段:清除所有缓存 this.clear(); } } /** * 获取所有缓存键名 * @returns {string[]} 键名数组 */ keys() { const keys = []; try { for (let i = 0; i < this.storage.length; i++) { const key = this.storage.key(i); if (key) keys.push(key); } } catch (error) { logger.error(this.logContext, 'keys 获取失败', error); } return keys; } /** * 获取存储使用情况(仅支持 localStorage/sessionStorage) * @returns {Object} { used, total, percentage } */ getStorageInfo() { try { let used = 0; for (let i = 0; i < this.storage.length; i++) { const key = this.storage.key(i); if (key) { used += (key.length + (this.storage.getItem(key)?.length || 0)) * 2; // UTF-16 } } // 大多数浏览器 localStorage 限制为 5-10MB const total = 5 * 1024 * 1024; // 5MB const percentage = (used / total * 100).toFixed(2); return { used, total, percentage: parseFloat(percentage), usedMB: (used / 1024 / 1024).toFixed(2), totalMB: (total / 1024 / 1024).toFixed(2) }; } catch (error) { logger.error(this.logContext, 'getStorageInfo 失败', error); return null; } } } // 导出单例实例(使用 localStorage) export const localCacheManager = new CacheManager(localStorage, 'LocalCache'); // 导出单例实例(使用 sessionStorage) export const sessionCacheManager = new CacheManager(sessionStorage, 'SessionCache'); // 导出类本身,供自定义实例化 export default CacheManager;