feat(admin): 用户排行榜每日快照——手动刷新当天 + 凌晨自动定稿前一天
- 新增 POST /api/admin/users/leaderboard/refresh-today:同步强制重建当天快照 并返回合并区间;前端 useRefreshTodayLeaderboard + AdminUserLeaderboardPage 「刷新当天」按钮 - Gateway 调度循环在每日 ADMIN_LEADERBOARD_FINALIZE_HOUR(默认凌晨1点)对前一天 强制重算一次,避免临近午夜落库的 runs/tool-calls 漏统计;判定逻辑下沉为纯函数 deerflow.persistence.admin_stats.finalize.should_finalize_previous_day - 新增 scripts/sql/column_additions_mysql.sql:既有 MySQL 库手动补列的幂等脚本 (Alembic 自动迁移已禁用,create_all 不改既有表) - CLAUDE.md 补充排行榜快照、Schema 迁移约定,并补齐技能/知识库选模型的文档 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
96e0d695c0
commit
d61aa853f1
8 files changed
+356
-3
No files matched your search
@@ -247,6 +247,32 @@ export async function getUserLeaderboard(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force a synchronous rebuild of *today's* leaderboard snapshot so the latest
|
||||
* real-time records are included, then return the leaderboard for `range`.
|
||||
*/
|
||||
export async function refreshTodayLeaderboard(
|
||||
range: {
|
||||
since: string;
|
||||
until: string;
|
||||
days?: number;
|
||||
},
|
||||
limit = 20,
|
||||
): Promise<AdminLeaderboardResponse> {
|
||||
const params = new URLSearchParams({
|
||||
days: String(range.days ?? 30),
|
||||
since: range.since,
|
||||
until: range.until,
|
||||
limit: String(limit),
|
||||
});
|
||||
return parse(
|
||||
await apiFetch(`${base()}/leaderboard/refresh-today?${params}`, {
|
||||
method: "POST",
|
||||
}),
|
||||
"Failed to refresh today's leaderboard",
|
||||
);
|
||||
}
|
||||
|
||||
export async function getUserAnalytics(
|
||||
userId: string,
|
||||
range: { since: string; until: string; days?: number },
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getUserMemory,
|
||||
getUserAnalytics,
|
||||
getUserLeaderboard,
|
||||
refreshTodayLeaderboard,
|
||||
listUserQa,
|
||||
listUserThreadMessages,
|
||||
listUserThreads,
|
||||
@@ -41,6 +42,36 @@ export function useUserLeaderboard(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-rebuild today's snapshot, then prime + invalidate the matching
|
||||
* leaderboard query so the page shows the freshly recomputed numbers.
|
||||
*/
|
||||
export function useRefreshTodayLeaderboard() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
range,
|
||||
limit = 20,
|
||||
}: {
|
||||
range: { since: string; until: string; days?: number };
|
||||
limit?: number;
|
||||
}) => refreshTodayLeaderboard(range, limit),
|
||||
onSuccess: (data, variables) => {
|
||||
qc.setQueryData(
|
||||
[
|
||||
"admin-activity",
|
||||
"leaderboard",
|
||||
variables.range.since,
|
||||
variables.range.until,
|
||||
variables.limit ?? 20,
|
||||
],
|
||||
data,
|
||||
);
|
||||
qc.invalidateQueries({ queryKey: ["admin-activity", "leaderboard"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUserAnalytics(
|
||||
userId: string | null,
|
||||
range: { since: string; until: string; days?: number },
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Medal,
|
||||
MessageSquareText,
|
||||
MousePointerClick,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Settings2,
|
||||
Sparkles,
|
||||
@@ -55,7 +56,7 @@ import type {
|
||||
AdminLeaderboardUser,
|
||||
} from "@/core/admin/api";
|
||||
import { streamUserLeaderboardReport } from "@/core/admin/api";
|
||||
import { useUserAnalytics, useUserLeaderboard } from "@/core/admin/hooks";
|
||||
import { useRefreshTodayLeaderboard, useUserAnalytics, useUserLeaderboard } from "@/core/admin/hooks";
|
||||
import { useModels } from "@/core/models/hooks";
|
||||
import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -1052,6 +1053,7 @@ export default function AdminUserLeaderboardPage() {
|
||||
[range],
|
||||
);
|
||||
const { data, isLoading, error } = useUserLeaderboard(apiRange, 100);
|
||||
const refreshToday = useRefreshTodayLeaderboard();
|
||||
const [reportOpen, setReportOpen] = useState(false);
|
||||
const { models: reportModels, isLoading: reportModelsLoading } = useModels({ enabled: reportOpen });
|
||||
const [reportModelName, setReportModelName] = useState(DEFAULT_REPORT_MODEL);
|
||||
@@ -1269,6 +1271,30 @@ export default function AdminUserLeaderboardPage() {
|
||||
>
|
||||
<Settings2 className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={refreshToday.isPending}
|
||||
onClick={() =>
|
||||
refreshToday.mutate(
|
||||
{ range: apiRange, limit: 100 },
|
||||
{
|
||||
onSuccess: () => toast.success("已刷新当天最新统计数据"),
|
||||
onError: (err) =>
|
||||
toast.error(err instanceof Error ? err.message : "刷新当天数据失败"),
|
||||
},
|
||||
)
|
||||
}
|
||||
className="h-9"
|
||||
title="强制重算今天的统计,把最新的实时记录纳入榜单"
|
||||
>
|
||||
{refreshToday.isPending ? (
|
||||
<Loader2 className="mr-1 size-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="mr-1 size-4" />
|
||||
)}
|
||||
{refreshToday.isPending ? "刷新中" : "刷新当天"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
||||
Reference in new issue
Block a user