const crypto = require('crypto'); class SuperPageCache { constructor() { this.cache = new Map(); // path -> { body, contentType, etag, createdAt } this.ttlMs = 10 * 60 * 1000; // 10 minutes default memory cache } middleware() { return (req, res, next) => { // Only cache GET requests that are not admin, guanli, or API routes if (req.method !== 'GET' || req.path.startsWith('/guanli') || req.path.startsWith('/admin') || req.path.startsWith('/api')) { return next(); } const cacheKey = req.originalUrl || req.url; const cached = this.cache.get(cacheKey); if (cached && (Date.now() - cached.createdAt < this.ttlMs)) { // Check conditional ETag if (req.headers['if-none-match'] === cached.etag) { res.setHeader('X-Cache', 'HIT-304'); return res.status(304).end(); } res.setHeader('X-Cache', 'HIT'); res.setHeader('ETag', cached.etag); res.setHeader('Content-Type', cached.contentType || 'text/html; charset=utf-8'); res.setHeader('Cache-Control', 'no-cache, must-revalidate'); return res.send(cached.body); } // Intercept res.send to populate cache const originalSend = res.send.bind(res); res.send = (body) => { if (res.statusCode === 200 && typeof body === 'string') { const etag = `W/"${crypto.createHash('md5').update(body).digest('hex').slice(0, 16)}"`; this.cache.set(cacheKey, { body, contentType: res.getHeader('Content-Type') || 'text/html; charset=utf-8', etag, createdAt: Date.now() }); res.setHeader('X-Cache', 'MISS'); res.setHeader('ETag', etag); res.setHeader('Cache-Control', 'no-cache, must-revalidate'); } return originalSend(body); }; next(); }; } clear() { const size = this.cache.size; this.cache.clear(); console.log(`[SuperPageCache] Flushed ${size} cached pages.`); } invalidate(urlPattern) { for (const key of this.cache.keys()) { if (key.includes(urlPattern)) { this.cache.delete(key); } } } } module.exports = new SuperPageCache();