fix(dashboard): consume backend comparison aggregates
This commit is contained in:
@@ -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
@@ -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;点位成功率分母=发起数×应领点位数(未跑到视为失败)。
|
||||
|
||||
Reference in New Issue
Block a user