shouldCache($request)) { return $next($request); } $key = $this->key($request); $ttl = (int) config('blog.page_cache_ttl', 300); if ($cached = Cache::get($key)) { self::increment('hits'); return response($cached['body'], 200, [ 'Content-Type' => $cached['type'], 'Cache-Control' => 'public, max-age='.$ttl, ]); } self::increment('misses'); $response = $next($request); if ($response->getStatusCode() === 200) { $body = $response->getContent(); Cache::put($key, [ 'body' => $body, 'type' => $response->headers->get('Content-Type') ?: 'text/html; charset=utf-8', ], $ttl); self::rememberKey($key, $request->fullUrl(), strlen($body), $ttl); } return $response; } public static function flush(): void { foreach (array_keys((array) Cache::get(self::TAG.'_keys', [])) as $key) { Cache::forget((string) $key); } Cache::forget(self::TAG.'_keys'); } public static function count(): int { return count((array) Cache::get(self::TAG.'_keys', [])); } /** * 命中/未命中统计(按日滚动),用于判断前台缓存效果。 */ public static function stats(): array { $hits = (int) Cache::get(self::TAG.'_hits', 0); $misses = (int) Cache::get(self::TAG.'_misses', 0); $total = $hits + $misses; return [ 'hits' => $hits, 'misses' => $misses, 'count' => self::count(), 'rate' => $total > 0 ? round($hits / $total * 100, 1) : 0.0, ]; } public static function resetStats(): void { Cache::forget(self::TAG.'_hits'); Cache::forget(self::TAG.'_misses'); } /** * @return list */ public static function entries(): array { $entries = []; foreach ((array) Cache::get(self::TAG.'_keys', []) as $key => $meta) { $entries[] = [ 'key' => (string) $key, 'url' => is_array($meta) ? (string) ($meta['url'] ?? $key) : (string) $key, 'cached_at' => is_array($meta) ? (string) ($meta['cached_at'] ?? '') : '', 'size' => is_array($meta) ? (int) ($meta['size'] ?? 0) : 0, 'expires_at' => is_array($meta) ? (string) ($meta['expires_at'] ?? '') : '', ]; } usort($entries, fn (array $left, array $right): int => strcmp($right['cached_at'], $left['cached_at'])); return $entries; } public static function forget(string $key): void { Cache::forget($key); $keys = (array) Cache::get(self::TAG.'_keys', []); unset($keys[$key]); Cache::put(self::TAG.'_keys', $keys, now()->addDay()); } private static function increment(string $metric): void { $key = self::TAG.'_'.$metric; Cache::put($key, (int) Cache::get($key, 0) + 1, now()->addDay()); } private static function rememberKey(string $key, ?string $url = null, int $size = 0, int $ttl = 0): void { $keys = (array) Cache::get(self::TAG.'_keys', []); $previous = $keys[$key] ?? []; $keys[$key] = [ 'url' => $url ?: (is_array($previous) ? (string) ($previous['url'] ?? '') : ''), 'cached_at' => now()->toDateTimeString(), 'size' => $size, 'expires_at' => $ttl > 0 ? now()->addSeconds($ttl)->toDateTimeString() : '', ]; Cache::put(self::TAG.'_keys', $keys, now()->addDay()); } private function shouldCache(Request $request): bool { if (! $request->isMethod('GET')) { return false; } if (auth()->check()) { return false; } $path = $request->path(); if (str_starts_with($path, 'admin') || str_starts_with($path, 'api') || str_starts_with($path, 'themes')) { return false; } // 动态端点、订阅和 agent 索引不进整页缓存,新文章要马上能看到 if (str_starts_with($path, 'track-view')) { return false; } if (in_array($path, ['rss.xml', 'rss.php', 'sitemap.xml', 'robots.txt', 'llms.txt'], true)) { return false; } if (str_ends_with($path, '.md')) { return false; } return true; } private function key(Request $request): string { return self::TAG.':'.md5($request->fullUrl().'|view:'.HomeViewMode::resolve($request)); } }