From ac2d21a5a02319af5ea357cace6b17d5607d9cae Mon Sep 17 00:00:00 2001 From: lizhilun Date: Tue, 31 Mar 2026 16:59:32 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=90=8E=E9=A6=96=E9=A1=B5=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=B8=8D=E5=88=B7=E6=96=B0=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将缓存清除逻辑从 stats.js 路由移至 app.js 全局中间件, 确保 records.js 路由的写操作也能触发缓存清除。 Co-Authored-By: Claude Opus 4.6 --- server/app.js | 11 +++++++ server/routes/stats.js | 43 ++------------------------ server/utils/cache.js | 69 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 41 deletions(-) create mode 100644 server/utils/cache.js diff --git a/server/app.js b/server/app.js index a07ab3e..1614d00 100644 --- a/server/app.js +++ b/server/app.js @@ -5,6 +5,7 @@ const compression = require('compression'); const config = require('./config'); const logger = require('./utils/logger'); +const { clearUserCache } = require('./utils/cache'); const db = require('./db'); const { router: authRouter, auth } = require('./routes/auth'); const statsRouter = require('./routes/stats'); @@ -30,6 +31,16 @@ app.use('/api', (req, res, next) => { next(); }); +// 全局缓存清除中间件:在写操作完成后清除用户缓存 +app.use('/api', (req, res, next) => { + res.once('finish', () => { + if (['POST', 'DELETE', 'PUT', 'PATCH'].includes(req.method) && req.user?.id) { + clearUserCache(req.user.id); + } + }); + next(); +}); + app.get('/api/contacts', auth, (req, res) => { const field = req.query.field; const allowed = ['boss', 'partner', 'source', 'destination']; diff --git a/server/routes/stats.js b/server/routes/stats.js index 9c8f14b..e9e0b9f 100644 --- a/server/routes/stats.js +++ b/server/routes/stats.js @@ -3,50 +3,11 @@ const db = require('../db'); const config = require('../config'); const logger = require('../utils/logger'); const { getMonthRange, AppError, validateRequired, validateNumber, validateInList } = require('../utils/helpers'); +const { getCached } = require('../utils/cache'); const router = express.Router(); -// 内存缓存(限制大小,基于LRU) -const statsCache = new Map(); -const CACHE_TTL = config.cache.ttl; -const MAX_CACHE_SIZE = config.cache.maxSize; - -function getCached(key, fetchFn) { - const cached = statsCache.get(key); - if (cached && Date.now() - cached.ts < CACHE_TTL) { - // LRU: move to end - statsCache.delete(key); - statsCache.set(key, cached); - return Promise.resolve(cached.data); - } - if (statsCache.size >= MAX_CACHE_SIZE) { - // Delete oldest entry - const firstKey = statsCache.keys().next().value; - statsCache.delete(firstKey); - } - return Promise.resolve(fetchFn()).then(data => { - statsCache.set(key, { data, ts: Date.now() }); - logger.debug(`缓存更新: ${key}`); - return data; - }).catch(err => { - logger.error('缓存获取失败:', err.message); - throw err; - }); -} - -// POST/DELETE/PUT/PATCH 后清除当前用户的缓存 -router.use((req, res, next) => { - res.once('finish', () => { - if (['POST', 'DELETE', 'PUT', 'PATCH'].includes(req.method) && req.user?.id) { - for (const key of statsCache.keys()) { - if (key.includes(`:${req.user.id}:`)) { - statsCache.delete(key); - } - } - } - }); - next(); -}); +// 缓存模块已移至 utils/cache.js // 格式化本地日期为 YYYY-MM-DD function formatLocalDate(date) { diff --git a/server/utils/cache.js b/server/utils/cache.js new file mode 100644 index 0000000..b88d023 --- /dev/null +++ b/server/utils/cache.js @@ -0,0 +1,69 @@ +/** + * 统计数据缓存模块 + * 基于 LRU 算法的内存缓存 + */ +const config = require('../config'); +const logger = require('./logger'); + +const statsCache = new Map(); +const CACHE_TTL = config.cache.ttl; +const MAX_CACHE_SIZE = config.cache.maxSize; + +/** + * 获取缓存数据,如果不存在则通过 fetchFn 获取并缓存 + * @param {string} key 缓存键 + * @param {Function} fetchFn 获取数据的函数 + * @returns {Promise} 缓存数据 + */ +function getCached(key, fetchFn) { + const cached = statsCache.get(key); + if (cached && Date.now() - cached.ts < CACHE_TTL) { + // LRU: move to end + statsCache.delete(key); + statsCache.set(key, cached); + return Promise.resolve(cached.data); + } + + // 超出最大缓存数量时,删除最旧的条目 + if (statsCache.size >= MAX_CACHE_SIZE) { + const firstKey = statsCache.keys().next().value; + statsCache.delete(firstKey); + } + + return Promise.resolve(fetchFn()).then(data => { + statsCache.set(key, { data, ts: Date.now() }); + logger.debug(`缓存更新: ${key}`); + return data; + }).catch(err => { + logger.error('缓存获取失败:', err.message); + throw err; + }); +} + +/** + * 清除指定用户的所有缓存 + * @param {number} userId 用户ID + */ +function clearUserCache(userId) { + const prefix = `:${userId}:`; + for (const key of statsCache.keys()) { + if (key.includes(prefix)) { + statsCache.delete(key); + logger.debug(`缓存清除: ${key}`); + } + } +} + +/** + * 清除所有缓存 + */ +function clearAllCache() { + statsCache.clear(); + logger.debug('所有缓存已清除'); +} + +module.exports = { + getCached, + clearUserCache, + clearAllCache +}; \ No newline at end of file