feat(cache): 通用缓存按区统计与清空接口
GET /cache/regions 列出 kv_cache 各区的未过期条数、已过期条数、占用字节、最近写入;
POST /cache/regions/{region}/clear 清空一个区(admin)。contracts 加区名中文映射
cacheRegionLabel,web 加对应 hooks。
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
ad3bf8926b
commit
b7460d5d0a
5 files changed
+190
-6
No files matched your search
+33
-2
@@ -8,6 +8,7 @@ import {
|
||||
type TmdbCacheItem,
|
||||
type TmdbCacheList,
|
||||
} from "@umt/contracts";
|
||||
import { sql } from "@umt/db";
|
||||
import type { z } from "zod";
|
||||
import { dataPaths } from "../../config.ts";
|
||||
import { externalFetch } from "../../egress.ts";
|
||||
@@ -40,10 +41,11 @@ type Infer<T> = T extends z.ZodType ? z.infer<T> : Record<string, never>;
|
||||
function route<D extends EndpointDef>(
|
||||
app: ModuleApp,
|
||||
def: D,
|
||||
handler: (i: { query: Infer<D["query"]> }) => Promise<unknown>,
|
||||
handler: (i: { query: Infer<D["query"]>; params: Infer<D["params"]> }) => Promise<unknown>,
|
||||
) {
|
||||
const request: RouteConfig["request"] = {};
|
||||
if (def.query) request.query = def.query as never;
|
||||
if (def.params) request.params = def.params as never;
|
||||
const r = createRoute({
|
||||
method: def.method,
|
||||
path: def.path,
|
||||
@@ -57,7 +59,12 @@ function route<D extends EndpointDef>(
|
||||
});
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 路由泛型在封装里擦除,入参已由 zod-openapi 校验
|
||||
app.openapi(r, (async (c: any) => {
|
||||
const data = def.response.parse(await handler({ query: def.query ? c.req.valid("query") : {} } as never));
|
||||
const data = def.response.parse(
|
||||
await handler({
|
||||
query: def.query ? c.req.valid("query") : {},
|
||||
params: def.params ? c.req.valid("param") : {},
|
||||
} as never),
|
||||
);
|
||||
return c.json({ ok: true, data }, 200);
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
}) as any);
|
||||
@@ -178,6 +185,30 @@ export function createCacheModule(opts: CacheModuleOptions = {}): ServerModule &
|
||||
};
|
||||
});
|
||||
route(app, Api.cache.imagesClear, async () => ({ deleted: await images(ctx).clear() }));
|
||||
// 通用缓存按区统计与清空(设置 → 缓存);图片缓存在磁盘上,走上面两个接口
|
||||
route(app, Api.cache.regions, async () => {
|
||||
const r = await sql<{ region: string; entries: number; expired: number; bytes: string; last: Date | null }>`
|
||||
select region,
|
||||
count(*) filter (where expires_at > now())::int as entries,
|
||||
count(*) filter (where expires_at <= now())::int as expired,
|
||||
coalesce(sum(pg_column_size(value) + pg_column_size(key)), 0)::text as bytes,
|
||||
max(updated_at) as last
|
||||
from kv_cache group by region order by region`.execute(ctx.db);
|
||||
return {
|
||||
items: r.rows.map((x) => ({
|
||||
region: x.region,
|
||||
entries: Number(x.entries),
|
||||
expired: Number(x.expired),
|
||||
bytes: Number(x.bytes),
|
||||
lastWriteAt: x.last ? new Date(x.last).toISOString() : null,
|
||||
})),
|
||||
};
|
||||
});
|
||||
route(app, Api.cache.regionClear, async ({ params }) => {
|
||||
const deleted = await sharedCache(ctx).clear(params.region);
|
||||
ctx.logger.info({ region: params.region, deleted }, "缓存区已清空");
|
||||
return { deleted };
|
||||
});
|
||||
},
|
||||
registerTasks() {},
|
||||
start(ctx: ServerContext) {
|
||||
|
||||
Reference in new issue
Block a user