feat(web): 设置缓存分区显示命中率

缓存区表加「命中率」列(近 7 天、三个进程合计)与内存 / 数据库 / 旧值 / 未命中明细;
缓存有效期卡片补 TMDB 人物与图片、豆瓣 / Bangumi 搜索与条目。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-06 19:37:34 +08:00
1 parent 1535d8664e
commit 81dca5d664
2 files changed
+93 -4

No files matched your search

+48 -4
View File
@@ -1,4 +1,4 @@
import { cacheRegionLabel } from "@umt/contracts";
import { type CacheHitStats, cacheRegionLabel } from "@umt/contracts";
import { useState } from "react";
import { toast } from "sonner";
import {
@@ -32,6 +32,8 @@ export type CacheRow = {
expired: number;
bytes: number;
lastWriteAt: string | null;
/** 统一元数据缓存的区才有:最近 7 天命中统计 */
hits?: CacheHitStats | undefined;
};
/** 区名 → 行:有中文名的用中文名,没有的原样显示 region */
@@ -39,6 +41,23 @@ export function cacheRow(r: Omit<CacheRow, "label">): CacheRow {
return { ...r, label: cacheRegionLabel(r.region) ?? r.region };
}
/** 命中率文字:没有统计的区显示「—」,有统计没读取显示「暂无读取」 */
export function hitRateText(h: CacheHitStats | undefined): string {
if (!h) return "—";
if (h.hitRate === null) return "暂无读取";
return `${Math.round(h.hitRate * 1000) / 10}%`;
}
/** 命中明细:内存 / 数据库 / 旧值 / 未命中,有失败与后台刷新时附上 */
export function hitDetailText(h: CacheHitStats): string {
const parts = [`内存 ${h.l1Hits}`, `数据库 ${h.l2Hits}`];
if (h.staleHits) parts.push(`旧值 ${h.staleHits}`);
parts.push(`未命中 ${h.misses}`);
if (h.failures) parts.push(`回源失败 ${h.failures}`);
if (h.refreshes) parts.push(`后台刷新 ${h.refreshes}`);
return parts.join(" · ");
}
function CacheRegionsBlock() {
const regions = useCacheRegions();
const images = useImageCache();
@@ -85,6 +104,19 @@ function CacheRegionsBlock() {
</div>
),
},
{
key: "hitRate",
header: "命中率",
align: "right",
cell: (r) => (
<div className="tabular-nums">
{hitRateText(r.hits)}
{r.hits && r.hits.hitRate !== null ? (
<p className="text-meta text-muted-foreground">{hitDetailText(r.hits)}</p>
) : null}
</div>
),
},
{
key: "bytes",
header: "占用",
@@ -121,7 +153,7 @@ function CacheRegionsBlock() {
section="cache"
anchor="regions"
title="缓存区"
description="清空后下次用到时重新请求,不影响作品、资源和文件。已过期的条目由定时任务清理。"
description="清空后下次用到时重新请求,不影响作品、资源和文件。已过期的条目由定时任务清理。命中率按最近 7 天、三个进程合计。"
plain
>
<DataTable<CacheRow>
@@ -224,7 +256,16 @@ export const IMAGES_CARD: CardSpec = {
}),
};
const TTL_KEYS = ["tmdbSearchDays", "tmdbDetailDays", "tmdbMissDays", "siteDetailDays", "webSearchDays"] as const;
const TTL_KEYS = [
"tmdbSearchDays",
"tmdbDetailDays",
"tmdbMissDays",
"imageDays",
"metaSearchDays",
"metaDetailDays",
"siteDetailDays",
"webSearchDays",
] as const;
const ttlField = (k: string, label: string, hint?: string) => ({
name: `storage.cacheTtl.${k}`,
label,
@@ -234,11 +275,14 @@ const ttlField = (k: string, label: string, hint?: string) => ({
export const TTL_CARD: CardSpec = {
anchor: "ttl",
title: "缓存有效期",
description: "单位天。追踪中的作品命中 TMDB 详情缓存时自动续期。",
description: "单位天。过期后先用旧值、后台刷新;关注中的作品命中详情缓存时自动续期,不会因条数上限被清掉。",
fields: [
ttlField("tmdbSearchDays", "TMDB 搜索与识别结果"),
ttlField("tmdbDetailDays", "TMDB 详情"),
ttlField("tmdbMissDays", "TMDB 空结果", "搜索无结果、未识别时的负缓存"),
ttlField("imageDays", "TMDB 人物与图片"),
ttlField("metaSearchDays", "豆瓣 / Bangumi 搜索"),
ttlField("metaDetailDays", "豆瓣 / Bangumi 条目"),
ttlField("siteDetailDays", "站点详情页内容"),
ttlField("webSearchDays", "网页搜索结果"),
],