/** * 通用缓存管理器 * 支持多种数据类型的本地缓存(JSON、文件、图片等) * 开发环境存储在项目目录,打包后存储在exe同级目录 */ import { app } from 'electron' import { promises as fs } from 'fs' import path from 'path' import crypto from 'crypto' /** * 缓存项元数据 */ interface CacheMetadata { key: string type: 'json' | 'text' | 'binary' | 'image' createdAt: number updatedAt: number size: number ttl?: number // Time to live in milliseconds (可选) } /** * 缓存配置 */ interface CacheConfig { maxAge?: number // 最大缓存时间(毫秒), 默认不过期 namespace?: string // 缓存命名空间,用于隔离不同模块的缓存 } /** * 缓存管理器类 */ class CacheManager { private cacheDir: string = '' private metadataMap: Map = new Map() private initialized: boolean = false /** * 初始化缓存管理器 * 必须在 app.whenReady() 之后调用 */ async init(): Promise { if (this.initialized) { console.warn('CacheManager 已经初始化过了') return } try { // 判断是否为开发环境 const isDev = !app.isPackaged if (isDev) { // 开发环境: 存储在项目根目录的 .cache 文件夹 this.cacheDir = path.join(process.cwd(), '.cache') } else { // 生产环境: 存储在 exe 同级目录的 cache 文件夹 const exePath = process.execPath const exeDir = path.dirname(exePath) this.cacheDir = path.join(exeDir, 'cache') } // 确保缓存目录存在 await fs.mkdir(this.cacheDir, { recursive: true }) // 加载元数据 await this.loadMetadata() this.initialized = true console.log('CacheManager 初始化完成') console.log('缓存目录:', this.cacheDir) console.log('当前环境:', isDev ? '开发环境' : '生产环境') } catch (error) { console.error('CacheManager 初始化失败:', error) throw error } } /** * 确保缓存管理器已初始化 */ private ensureInitialized(): void { if (!this.initialized) { throw new Error('CacheManager 未初始化,请先调用 init()') } } /** * 生成缓存键的哈希值(用于文件名) */ private hashKey(key: string): string { return crypto.createHash('md5').update(key).digest('hex') } /** * 获取缓存文件路径 */ private getCacheFilePath(key: string, namespace?: string): string { const hash = this.hashKey(key) if (namespace) { const namespaceDir = path.join(this.cacheDir, namespace) return path.join(namespaceDir, hash) } return path.join(this.cacheDir, hash) } /** * 获取元数据文件路径 */ private getMetadataPath(): string { return path.join(this.cacheDir, '_metadata.json') } /** * 加载元数据 */ private async loadMetadata(): Promise { try { const metadataPath = this.getMetadataPath() const exists = await fs.access(metadataPath).then(() => true).catch(() => false) if (exists) { const content = await fs.readFile(metadataPath, 'utf-8') const data = JSON.parse(content) this.metadataMap = new Map(Object.entries(data)) console.log(`加载了 ${this.metadataMap.size} 条缓存元数据`) } } catch (error) { console.warn('加载缓存元数据失败:', error) this.metadataMap = new Map() } } /** * 保存元数据 */ private async saveMetadata(): Promise { try { const metadataPath = this.getMetadataPath() const data = Object.fromEntries(this.metadataMap) await fs.writeFile(metadataPath, JSON.stringify(data, null, 2), 'utf-8') } catch (error) { console.error('保存缓存元数据失败:', error) } } /** * 检查缓存是否过期 */ private isExpired(metadata: CacheMetadata): boolean { if (!metadata.ttl) return false const now = Date.now() return now - metadata.updatedAt > metadata.ttl } /** * 设置缓存 (JSON数据) */ async setJSON(key: string, data: T, config?: CacheConfig): Promise { this.ensureInitialized() try { const namespace = config?.namespace const filePath = this.getCacheFilePath(key, namespace) // 确保命名空间目录存在 if (namespace) { const namespaceDir = path.join(this.cacheDir, namespace) await fs.mkdir(namespaceDir, { recursive: true }) } // 写入JSON数据 const jsonString = JSON.stringify(data, null, 2) await fs.writeFile(filePath, jsonString, 'utf-8') // 更新元数据 const stats = await fs.stat(filePath) const metadata: CacheMetadata = { key, type: 'json', createdAt: this.metadataMap.get(key)?.createdAt || Date.now(), updatedAt: Date.now(), size: stats.size, ttl: config?.maxAge } this.metadataMap.set(key, metadata) await this.saveMetadata() console.log(`缓存已设置: ${key} (${stats.size} bytes)`) } catch (error) { console.error(`设置缓存失败 [${key}]:`, error) throw error } } /** * 获取缓存 (JSON数据) */ async getJSON(key: string, config?: CacheConfig): Promise { this.ensureInitialized() try { const metadata = this.metadataMap.get(key) // 检查元数据是否存在 if (!metadata || metadata.type !== 'json') { console.log(`缓存不存在或类型不匹配: ${key}`) return null } // 检查是否过期 if (this.isExpired(metadata)) { console.log(`缓存已过期: ${key}`) await this.delete(key, config?.namespace) return null } // 读取缓存文件 const namespace = config?.namespace const filePath = this.getCacheFilePath(key, namespace) const content = await fs.readFile(filePath, 'utf-8') const data = JSON.parse(content) console.log(`读取缓存: ${key}`) return data } catch (error) { console.error(`读取缓存失败 [${key}]:`, error) return null } } /** * 设置缓存 (二进制数据 - 用于图片等) */ async setBinary(key: string, buffer: Buffer, type: 'binary' | 'image' = 'binary', config?: CacheConfig): Promise { this.ensureInitialized() try { const namespace = config?.namespace const filePath = this.getCacheFilePath(key, namespace) // 确保命名空间目录存在 if (namespace) { const namespaceDir = path.join(this.cacheDir, namespace) await fs.mkdir(namespaceDir, { recursive: true }) } // 写入二进制数据 await fs.writeFile(filePath, buffer) // 更新元数据 const stats = await fs.stat(filePath) const metadata: CacheMetadata = { key, type, createdAt: this.metadataMap.get(key)?.createdAt || Date.now(), updatedAt: Date.now(), size: stats.size, ttl: config?.maxAge } this.metadataMap.set(key, metadata) await this.saveMetadata() console.log(`二进制缓存已设置: ${key} (${stats.size} bytes)`) } catch (error) { console.error(`设置二进制缓存失败 [${key}]:`, error) throw error } } /** * 获取缓存 (二进制数据) */ async getBinary(key: string, config?: CacheConfig): Promise { this.ensureInitialized() try { const metadata = this.metadataMap.get(key) // 检查元数据是否存在 if (!metadata || (metadata.type !== 'binary' && metadata.type !== 'image')) { console.log(`二进制缓存不存在或类型不匹配: ${key}`) return null } // 检查是否过期 if (this.isExpired(metadata)) { console.log(`二进制缓存已过期: ${key}`) await this.delete(key, config?.namespace) return null } // 读取缓存文件 const namespace = config?.namespace const filePath = this.getCacheFilePath(key, namespace) const buffer = await fs.readFile(filePath) console.log(`读取二进制缓存: ${key}`) return buffer } catch (error) { console.error(`读取二进制缓存失败 [${key}]:`, error) return null } } /** * 检查缓存是否存在 */ async has(key: string, config?: CacheConfig): Promise { this.ensureInitialized() try { const metadata = this.metadataMap.get(key) if (!metadata) return false // 检查是否过期 if (this.isExpired(metadata)) { await this.delete(key, config?.namespace) return false } // 检查文件是否真实存在 const namespace = config?.namespace const filePath = this.getCacheFilePath(key, namespace) const exists = await fs.access(filePath).then(() => true).catch(() => false) return exists } catch (error) { console.error(`检查缓存失败 [${key}]:`, error) return false } } /** * 删除单个缓存 */ async delete(key: string, namespace?: string): Promise { this.ensureInitialized() try { const filePath = this.getCacheFilePath(key, namespace) // 删除缓存文件 await fs.unlink(filePath).catch(() => {}) // 删除元数据 this.metadataMap.delete(key) await this.saveMetadata() console.log(`缓存已删除: ${key}`) } catch (error) { console.error(`删除缓存失败 [${key}]:`, error) } } /** * 清空指定命名空间的所有缓存 */ async clearNamespace(namespace: string): Promise { this.ensureInitialized() try { const namespaceDir = path.join(this.cacheDir, namespace) // 删除命名空间目录 await fs.rm(namespaceDir, { recursive: true, force: true }) // 删除相关元数据 const keysToDelete: string[] = [] this.metadataMap.forEach((metadata, key) => { const filePath = this.getCacheFilePath(key, namespace) if (filePath.startsWith(namespaceDir)) { keysToDelete.push(key) } }) keysToDelete.forEach(key => this.metadataMap.delete(key)) await this.saveMetadata() console.log(`命名空间缓存已清空: ${namespace} (${keysToDelete.length} 项)`) } catch (error) { console.error(`清空命名空间缓存失败 [${namespace}]:`, error) } } /** * 清空所有缓存 */ async clearAll(): Promise { this.ensureInitialized() try { // 删除所有缓存文件(保留元数据文件) const files = await fs.readdir(this.cacheDir, { withFileTypes: true }) for (const file of files) { const filePath = path.join(this.cacheDir, file.name) // 跳过元数据文件 if (file.name === '_metadata.json') continue if (file.isDirectory()) { await fs.rm(filePath, { recursive: true, force: true }) } else { await fs.unlink(filePath) } } // 清空元数据 this.metadataMap.clear() await this.saveMetadata() console.log('所有缓存已清空') } catch (error) { console.error('清空所有缓存失败:', error) } } /** * 获取缓存统计信息 */ async getStats(): Promise<{ totalItems: number totalSize: number cacheDir: string }> { this.ensureInitialized() let totalSize = 0 this.metadataMap.forEach(metadata => { totalSize += metadata.size }) return { totalItems: this.metadataMap.size, totalSize, cacheDir: this.cacheDir } } /** * 获取所有缓存键 */ async getAllKeys(): Promise { this.ensureInitialized() return Array.from(this.metadataMap.keys()) } } // 导出单例 export const cacheManager = new CacheManager()