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