feat: add log cleanup controls

This commit is contained in:
JetSprow
2026-04-30 17:10:53 +10:00
parent 042c5b34ab
commit 1e194aabdb
18 changed files with 598 additions and 4 deletions

111
src/actions/admin/logs.ts Normal file
View File

@@ -0,0 +1,111 @@
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { prisma } from "@/lib/prisma";
import { requireAdmin } from "@/lib/require-auth";
import { actorFromSession, recordAuditLog } from "@/services/audit";
import {
cleanupExpiredLogs,
cutoffFromDays,
deleteLogEntry,
logCleanupTargetLabels,
logCleanupTargets,
summarizeLogCleanup,
type LogCleanupSummary,
} from "@/services/log-cleanup";
import { getErrorMessage } from "@/lib/errors";
const deleteTargets = logCleanupTargets.filter((target) => target !== "ALL") as [
Exclude<(typeof logCleanupTargets)[number], "ALL">,
...Exclude<(typeof logCleanupTargets)[number], "ALL">[],
];
const deleteLogSchema = z.object({
target: z.enum(deleteTargets),
id: z.string().trim().min(1),
});
const cleanupExpiredLogsSchema = z.object({
target: z.enum(logCleanupTargets),
cutoffDays: z.coerce.number().int().min(1).max(3650),
});
export type CleanupExpiredLogsResult =
| { ok: true; summary: LogCleanupSummary; message: string }
| { ok: false; error: string };
function revalidateLogViews() {
revalidatePath("/admin/audit-logs");
revalidatePath("/admin/tasks");
revalidatePath("/admin/traffic");
revalidatePath("/admin/subscription-risk");
revalidatePath("/admin/settings");
revalidatePath("/admin/subscriptions");
}
export async function deleteAdminLogEntry(input: {
target: Exclude<(typeof logCleanupTargets)[number], "ALL">;
id: string;
}) {
const session = await requireAdmin();
const actor = actorFromSession(session);
const { target, id } = deleteLogSchema.parse(input);
await deleteLogEntry({ target, id });
await recordAuditLog({
actor,
action: "logs.delete",
targetType: "LogEntry",
targetId: id,
targetLabel: logCleanupTargetLabels[target],
message: `删除${logCleanupTargetLabels[target]}记录`,
metadata: { target, deletedId: id },
});
revalidateLogViews();
}
export async function cleanupExpiredAdminLogs(input: {
target: (typeof logCleanupTargets)[number];
cutoffDays: number;
}): Promise<CleanupExpiredLogsResult> {
try {
const session = await requireAdmin();
const actor = actorFromSession(session);
const parsed = cleanupExpiredLogsSchema.parse(input);
const cutoff = cutoffFromDays(parsed.cutoffDays);
const summary = await cleanupExpiredLogs({
target: parsed.target,
cutoff,
keepActiveRiskRestrictions: true,
});
const message = summarizeLogCleanup(summary);
await prisma.appConfig.update({
where: { id: "default" },
data: { logCleanupLastRunAt: new Date() },
}).catch(() => null);
await recordAuditLog({
actor,
action: "logs.cleanup",
targetType: "LogCleanup",
targetLabel: logCleanupTargetLabels[parsed.target],
message: `手动清理 ${parsed.cutoffDays} 天前的${logCleanupTargetLabels[parsed.target]}${message}`,
metadata: {
target: parsed.target,
cutoffDays: parsed.cutoffDays,
cutoff: cutoff.toISOString(),
summary,
},
});
revalidateLogViews();
return { ok: true, summary, message };
} catch (error) {
return { ok: false, error: getErrorMessage(error, "清理日志失败") };
}
}