diff --git a/src/modules/words/words.service.ts b/src/modules/words/words.service.ts index 425f1b2..19b3057 100644 --- a/src/modules/words/words.service.ts +++ b/src/modules/words/words.service.ts @@ -4,10 +4,15 @@ import * as fs from 'fs/promises'; import { PathService } from '../../core/path/path.service'; import { AutoUpdateService } from '../../core/auto-update/auto-update.service'; +interface WordCacheEntry { + data: string[]; + timer: NodeJS.Timeout; +} + @Injectable() export class WordsService { private readonly logger = new Logger(WordsService.name); - private wordCache: Record = {}; + private wordCache: Map = new Map(); private readonly clearIntervalMs = 240 * 60 * 1000; // 240min private readonly updateMs = 15 * 60 * 1000; // 15min @@ -18,21 +23,12 @@ export class WordsService { private readonly autoUpdateService: AutoUpdateService; constructor() { - this.startAutoClear(); this.startAutoUpdate(); } - private startAutoClear() { - setInterval(() => { - this.logger.log('清理文案缓存..'); - this.wordCache = {}; - }, this.clearIntervalMs); - } - private startAutoUpdate() { setInterval(async () => { const wordsPath = path.join(this.paths.get('words'), '..'); - //const wordsPath = this.paths.get('words'); this.logger.log('定时检查文案仓库更新..'); const updated = await this.autoUpdateService.checkRepoForUpdates( wordsPath, @@ -40,26 +36,51 @@ export class WordsService { ); if (updated) { this.logger.log('文案仓库已更新,清理缓存..'); - this.wordCache = {}; + this.clearAllCache(); } }, this.updateMs); } + private clearAllCache() { + for (const [key, entry] of this.wordCache.entries()) { + clearTimeout(entry.timer); + this.wordCache.delete(key); + } + } + + private scheduleCacheClear(key: string) { + const existing = this.wordCache.get(key); + if (existing) clearTimeout(existing.timer); + + return setTimeout(() => { + this.logger.log(`清理单项文案缓存: ${key}`); + this.wordCache.delete(key); + }, this.clearIntervalMs); + } + /** * 从本地加载文案到内存 */ public async loadWord(type: string, name: string): Promise { - const cacheKey = `${type}/${name}`; + const safeType = this.safePathSegment(type); + const safeName = this.safePathSegment(name); + const cacheKey = `${safeType}/${safeName}`; this.logger.log(`加载文案 ${cacheKey}..`); - if (this.wordCache[cacheKey]) return this.wordCache[cacheKey]; + const cache = this.wordCache.get(cacheKey); + if (cache) return cache.data; - const filePath = path.join(this.paths.get('words'), type, `${name}.json`); + const filePath = path.join( + this.paths.get('words'), + safeType, + `${safeName}.json`, + ); try { - const content = await fs.readFile(filePath, 'utf-8'); + const content = await fs.readFile(filePath, { encoding: 'utf-8' }); const parsed = JSON.parse(content); if (Array.isArray(parsed)) { const texts = parsed.filter((item) => typeof item === 'string'); - this.wordCache[cacheKey] = texts; + const timer = this.scheduleCacheClear(cacheKey); + this.wordCache.set(cacheKey, { data: texts, timer }); return texts; } return null; @@ -73,16 +94,22 @@ export class WordsService { * 重载文案 */ public async reloadWord(type: string, name: string): Promise { - const cacheKey = `${type}/${name}`; + const safeType = this.safePathSegment(type); + const safeName = this.safePathSegment(name); + const cacheKey = `${safeType}/${safeName}`; this.logger.log(`重载文案: ${cacheKey}..`); - const filePath = path.join(this.paths.get('words'), type, `${name}.json`); + const filePath = path.join( + this.paths.get('words'), + safeType, + `${safeName}.json`, + ); try { - const content = await fs.readFile(filePath, 'utf-8'); + const content = await fs.readFile(filePath, { encoding: 'utf-8' }); const parsed = JSON.parse(content); if (Array.isArray(parsed)) { - this.wordCache[cacheKey] = parsed.filter( - (item) => typeof item === 'string', - ); + const texts = parsed.filter((item) => typeof item === 'string'); + const timer = this.scheduleCacheClear(cacheKey); + this.wordCache.set(cacheKey, { data: texts, timer }); return true; } return false; @@ -91,4 +118,9 @@ export class WordsService { return false; } } + + private safePathSegment(segment: string): string { + // 将不安全字符转义为安全文件名形式 + return segment.replace(/[\\\/:*?"<>|]/g, '_'); + } }