fix(dashboard): consume backend comparison aggregates

This commit is contained in:
linkeyu
2026-07-20 20:01:38 +08:00
parent 30dad5d4e0
commit f25d5f0b17
2 changed files with 24 additions and 105 deletions
+18 -104
View File
@@ -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<string | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
const [cpsSyncing, setCpsSyncing] = useState(false);
const [comparisonRecords, setComparisonRecords] = useState<ComparisonRecordListItem[]>([]);
const [comparisonRecordsComplete, setComparisonRecordsComplete] = useState(false);
const [couponP95Ms, setCouponP95Ms] = useState<number | null>(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<CursorPage<ComparisonRecordListItem>>(
'/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
+6 -1
View File
@@ -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;点位成功率分母=发起数×应领点位数(未跑到视为失败)。