From f25d5f0b17788fbfa04a77c049f50c4704ddf8fa Mon Sep 17 00:00:00 2001 From: linkeyu <798648091@qq.com> Date: Mon, 20 Jul 2026 20:01:38 +0800 Subject: [PATCH] fix(dashboard): consume backend comparison aggregates --- src/app/(main)/dashboard/page.tsx | 122 +++++------------------------- src/lib/types.ts | 7 +- 2 files changed, 24 insertions(+), 105 deletions(-) diff --git a/src/app/(main)/dashboard/page.tsx b/src/app/(main)/dashboard/page.tsx index 3a7dbb0..82b1add 100644 --- a/src/app/(main)/dashboard/page.tsx +++ b/src/app/(main)/dashboard/page.tsx @@ -5,13 +5,7 @@ import { Alert, App, DatePicker, Select, Spin, Tag, Tooltip } from 'antd'; import { CalendarOutlined, InfoCircleOutlined } from '@ant-design/icons'; import dayjs, { type Dayjs } from 'dayjs'; import { api } from '@/lib/api'; -import { percentile } from '@/lib/format'; -import type { - AdRevenueReport, - ComparisonRecordListItem, - CursorPage, - DashboardOverview, -} from '@/lib/types'; +import type { AdRevenueReport, DashboardOverview } from '@/lib/types'; type PeriodKey = 'yesterday' | '7d' | '30d'; type TrendSeriesKey = 'dau' | 'newUsers' | 'comparisons'; @@ -43,10 +37,6 @@ const fmtYuan = (yuan: number | null | undefined) => (yuan == null ? '--' : `¥$ const fmtMsAsSeconds = (ms: number | null | undefined) => (ms == null ? '--' : (ms / 1000).toFixed(1)); const pct = (v: number | null | undefined) => (v == null ? '--' : `${(v * 100).toFixed(1)}%`); const compact = (v: number) => (v >= 1000 ? `${(v / 1000).toFixed(v >= 10000 ? 0 : 1)}k` : String(v)); -const inDateRange = (value: string, start: Dayjs, end: Dayjs) => { - const date = dayjs(value).format('YYYY-MM-DD'); - return date >= start.format('YYYY-MM-DD') && date <= end.format('YYYY-MM-DD'); -}; const fmtCoinAmount = (v: number | null | undefined) => { if (v == null) return '--'; if (Math.abs(v) >= 10000) return `${(v / 10000).toFixed(2).replace(/\.?0+$/, '')} 万`; @@ -472,8 +462,6 @@ export default function DashboardPage() { const [updatedAt, setUpdatedAt] = useState(null); const [refreshKey, setRefreshKey] = useState(0); const [cpsSyncing, setCpsSyncing] = useState(false); - const [comparisonRecords, setComparisonRecords] = useState([]); - const [comparisonRecordsComplete, setComparisonRecordsComplete] = useState(false); const [couponP95Ms, setCouponP95Ms] = useState(null); const range = useMemo(() => { @@ -498,54 +486,17 @@ export default function DashboardPage() { rewardVideoAdSummary && rewardVideoAdSummary.impressions > 0 ? fmtYuan((rewardVideoAdSummary.revenue_yuan / rewardVideoAdSummary.impressions) * 1000) : '--'; - const comparisonMetrics = useMemo(() => { - const current = comparisonRecords.filter((r) => inDateRange(r.created_at, range.start, range.end)); - const previous = comparisonRecords.filter((r) => - inDateRange(r.created_at, previousRange.start, previousRange.end), - ); - const summarize = (rows: ComparisonRecordListItem[]) => { - const cancelled = rows.filter((r) => r.status === 'cancelled').length; - const completedRows = rows.filter((r) => r.status === 'success' || r.status === 'failed'); - const completed = completedRows.length; - const success = rows.filter((r) => r.status === 'success').length; - const successRateDenominator = rows.length - cancelled; - const durations = completedRows - .filter((r) => r.total_ms != null) - .map((r) => r.total_ms as number); - const tokenCosts = rows - .map((r) => r.llm_cost_yuan) - .filter((v): v is number => v != null); - return { - total: rows.length, - completed, - cancelled, - success, - successRate: successRateDenominator > 0 ? success / successRateDenominator : null, - medianMs: percentile(durations, 0.5), - p95Ms: percentile(durations, 0.95), - tokenCostTotal: tokenCosts.reduce((sum, value) => sum + value, 0), - }; - }; - return { current: summarize(current), previous: summarize(previous) }; - }, [comparisonRecords, range.start, range.end, previousRange.start, previousRange.end]); - const compareSuccessRate = comparisonRecordsComplete - ? comparisonMetrics.current.successRate - : null; - // 新口径依赖完整明细(尤其 cancelled 分母)。拉取不完整时整组统一显示空值, - // 不与服务端旧口径的 total/success 混搭,避免同一组卡片来自两套数据源。 - const comparisonTotal = comparisonRecordsComplete ? comparisonMetrics.current.total : null; - const comparisonCompleted = comparisonRecordsComplete ? comparisonMetrics.current.completed : null; - const comparisonSuccess = comparisonRecordsComplete - ? comparisonMetrics.current.success - : null; - const comparisonSuccessRateDenominator = comparisonRecordsComplete - ? comparisonMetrics.current.total - comparisonMetrics.current.cancelled - : null; - const comparisonMedianMs = comparisonRecordsComplete - ? comparisonMetrics.current.medianMs - : null; - const comparisonP95Ms = comparisonRecordsComplete ? comparisonMetrics.current.p95Ms : null; - const tokenCostYuan = comparisonRecordsComplete ? comparisonMetrics.current.tokenCostTotal : null; + const compareSuccessRate = periodData?.comparison.success_rate ?? null; + const comparisonTotal = periodData?.comparison.total ?? null; + const comparisonCompleted = periodData?.comparison.completed ?? null; + const comparisonSuccess = periodData?.comparison.success ?? null; + const comparisonSuccessRateDenominator = + periodData == null || periodData.comparison.cancelled == null + ? null + : periodData.comparison.total - periodData.comparison.cancelled; + const comparisonMedianMs = periodData?.comparison.median_duration_ms ?? null; + const comparisonP95Ms = periodData?.comparison.p95_duration_ms ?? null; + const tokenCostYuan = periodData?.comparison.token_cost_total_yuan ?? null; const couponRewardCoinTotal = periodData?.coins.coupon_reward_coin_total; const comparisonRewardCoinTotal = periodData?.coins.comparison_reward_coin_total; const signinCoinTotal = periodData?.coins.signin_coin_total; @@ -588,17 +539,17 @@ export default function DashboardPage() { const newUserDelta = percentDelta(periodData?.users.new, previousPeriodData?.users.new); const retentionDelta = pointDelta(periodData?.users.retention_rate, previousPeriodData?.users.retention_rate); const comparisonTotalDelta = percentDelta( - comparisonRecordsComplete ? comparisonMetrics.current.total : null, - comparisonRecordsComplete ? comparisonMetrics.previous.total : null, + periodData?.comparison.total, + previousPeriodData?.comparison.total, ); const comparisonSuccessDelta = percentDelta( - comparisonRecordsComplete ? comparisonMetrics.current.success : null, - comparisonRecordsComplete ? comparisonMetrics.previous.success : null, + periodData?.comparison.success, + previousPeriodData?.comparison.success, ); const comparisonOrderedDelta = percentDelta(periodData?.comparison.ordered, previousPeriodData?.comparison.ordered); const durationDeltaInfo = durationDelta( - comparisonRecordsComplete ? comparisonMetrics.current.medianMs : null, - comparisonRecordsComplete ? comparisonMetrics.previous.medianMs : null, + periodData?.comparison.median_duration_ms, + previousPeriodData?.comparison.median_duration_ms, ); // 领券核心数据(period.coupon;旧后端无此字段时整节显示 '--') const couponPeriod = periodData?.coupon ?? null; @@ -628,43 +579,6 @@ export default function DashboardPage() { const rewardVideoCoinRatio = ratioBadge(periodData?.coins.reward_video_coin_total, periodData?.coins.granted_total); const regularTaskCoinRatio = ratioBadge(regularTaskCoinTotal, periodData?.coins.granted_total); - useEffect(() => { - let alive = true; - const loadComparisonRecords = async () => { - const collected: ComparisonRecordListItem[] = []; - let cursor = 0; - let complete = false; - for (let pageIndex = 0; pageIndex < 100; pageIndex += 1) { - const { data: pageData } = await api.get>( - '/admin/api/comparison-records', - { params: { limit: 100, cursor } }, - ); - collected.push(...pageData.items); - const oldest = pageData.items.at(-1); - if ( - pageData.next_cursor == null || - pageData.items.length === 0 || - (oldest && dayjs(oldest.created_at).isBefore(previousRange.start.startOf('day'))) - ) { - complete = true; - break; - } - cursor = pageData.next_cursor; - } - if (!alive) return; - setComparisonRecords(collected); - setComparisonRecordsComplete(complete); - }; - loadComparisonRecords().catch(() => { - if (!alive) return; - setComparisonRecords([]); - setComparisonRecordsComplete(false); - }); - return () => { - alive = false; - }; - }, [range.end, previousRange.start, refreshKey]); - useEffect(() => { let alive = true; api diff --git a/src/lib/types.ts b/src/lib/types.ts index cfd158d..936b3ab 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -556,11 +556,16 @@ export interface DashboardOverview { }; comparison: { total: number; + completed?: number; + cancelled?: number; success: number; - success_rate: number; + success_rate: number | null; ordered: number; average_duration_ms: number | null; + median_duration_ms?: number | null; + p95_duration_ms?: number | null; average_saved_cents: number | null; + token_cost_total_yuan?: number; }; // 领券核心数据(2026-07-05 新增;旧后端无此字段,前端 `?.` 探测)。 // 点位=一张券;成功口径 success+already_claimed;点位成功率分母=发起数×应领点位数(未跑到视为失败)。