diff --git a/src/app/(main)/dashboard/page.tsx b/src/app/(main)/dashboard/page.tsx index 725f4d5..0046929 100644 --- a/src/app/(main)/dashboard/page.tsx +++ b/src/app/(main)/dashboard/page.tsx @@ -118,9 +118,10 @@ function previousPeriodRange(range: ReturnType) { } function retentionTitle(period: PeriodKey) { - if (period === '7d') return '近 7 日新增留存'; - if (period === '30d') return '近 30 日新增留存'; - return '昨日新增留存'; + // 次日留存口径(2026-07-05):每天取前一日新增用户,统计其当日活跃比例;单日窗口即「前日新增的昨日留存」。 + if (period === '7d') return '近 7 日新增次日留存'; + if (period === '30d') return '近 30 日新增次日留存'; + return '前日用户新增留存'; } function avgEcpm(report: AdRevenueReport | null) { @@ -131,19 +132,23 @@ function avgEcpm(report: AdRevenueReport | null) { function adTypeSummary(report: AdRevenueReport | null, adType: string) { if (!report) return null; if (report.by_ad_type?.[adType]) return report.by_ad_type[adType]; + // 优先用后端全量小计 type_stats(items 受 limit 分页截断,>1000 行的区间按 items 现算会漏旧数据) + const stat = report.type_stats?.[adType]; + if (stat) return { impressions: stat.impressions, revenue_yuan: stat.revenue_yuan }; const matched = report.items.filter((it) => it.ad_type === adType); if (matched.length === 0) return null; return { - ad_type: adType, impressions: matched.reduce((sum, it) => sum + it.impressions, 0), revenue_yuan: matched.reduce((sum, it) => sum + it.revenue_yuan, 0), - expected_coin: matched.reduce((sum, it) => sum + it.expected_coin, 0), - actual_coin: matched.reduce((sum, it) => sum + it.actual_coin, 0), }; } function adSceneSummary(report: AdRevenueReport | null, feedScene: 'coupon' | 'comparison') { if (!report) return null; + // 优先用后端全量小计 scene_stats:2026-07-02 起 items 不再含信息流逐条展示行(唯一带收益+场景的行), + // 按 items 现算恒为 0;scene_stats 在全量 events 上聚合且不受分页截断。旧后端无此字段时回退 items 现算。 + const stat = report.scene_stats?.[feedScene]; + if (stat) return { impressions: stat.impressions, revenue_yuan: stat.revenue_yuan }; const matched = report.items.filter((it) => it.feed_scene === feedScene); if (matched.length === 0) return null; return { @@ -521,6 +526,23 @@ export default function DashboardPage() { periodData?.comparison.average_duration_ms, previousPeriodData?.comparison.average_duration_ms, ); + // 领券核心数据(period.coupon;旧后端无此字段时整节显示 '--') + const couponPeriod = periodData?.coupon ?? null; + const previousCouponPeriod = previousPeriodData?.coupon ?? null; + const couponStartedDelta = percentDelta(couponPeriod?.started, previousCouponPeriod?.started); + const couponSuccessRateValue = + couponPeriod?.success_rate == null ? '--' : (couponPeriod.success_rate * 100).toFixed(1); + const couponSuccessRateDelta = pointDelta(couponPeriod?.success_rate, previousCouponPeriod?.success_rate); + const couponPointRateValue = + couponPeriod?.point_success_rate == null ? '--' : (couponPeriod.point_success_rate * 100).toFixed(1); + const couponPointRateDelta = pointDelta( + couponPeriod?.point_success_rate, + previousCouponPeriod?.point_success_rate, + ); + const couponMedianDelta = durationDelta( + couponPeriod?.median_elapsed_ms, + previousCouponPeriod?.median_elapsed_ms, + ); const averageSavedDelta = moneyDeltaCents( periodData?.comparison.average_saved_cents, previousPeriodData?.comparison.average_saved_cents, @@ -761,6 +783,51 @@ export default function DashboardPage() { +
+
+ +

领券核心数据

+ 发起、整场成功、点位成功与效率 +
+
+ + + + +
+
+
diff --git a/src/app/(main)/users/page.tsx b/src/app/(main)/users/page.tsx index d53269b..01845e2 100644 --- a/src/app/(main)/users/page.tsx +++ b/src/app/(main)/users/page.tsx @@ -5,7 +5,7 @@ import type { Key } from 'react'; import { useRouter } from 'next/navigation'; import type { ColumnsType } from 'antd/es/table'; import type { SorterResult } from 'antd/es/table/interface'; -import { App, Button, DatePicker, Input, Select, Space, Table, Tag } from 'antd'; +import { App, Button, DatePicker, Input, Select, Space, Table, Tag, Tooltip } from 'antd'; import dayjs, { type Dayjs } from 'dayjs'; import { api, errMsg } from '@/lib/api'; import { canDo } from '@/lib/auth'; @@ -18,7 +18,7 @@ const { RangePicker } = DatePicker; const STATUS_COLOR: Record = { active: 'green', disabled: 'red', deleted: 'default' }; -type SortField = 'id' | 'created_at' | 'last_login_at'; +type SortField = 'id' | 'created_at' | 'last_active_at'; export default function UsersPage() { const router = useRouter(); @@ -61,8 +61,8 @@ export default function UsersPage() { register_channel: channel || undefined, created_from: regRange?.[0] ? regRange[0].startOf('day').toISOString() : undefined, created_to: regRange?.[1] ? regRange[1].endOf('day').toISOString() : undefined, - last_login_from: loginRange?.[0] ? loginRange[0].startOf('day').toISOString() : undefined, - last_login_to: loginRange?.[1] ? loginRange[1].endOf('day').toISOString() : undefined, + last_active_from: loginRange?.[0] ? loginRange[0].startOf('day').toISOString() : undefined, + last_active_to: loginRange?.[1] ? loginRange[1].endOf('day').toISOString() : undefined, }); const resetFilters = () => { @@ -235,12 +235,19 @@ export default function UsersPage() { render: (v: string) => formatUtcTime(v), }, { - title: '最近登录', - dataIndex: 'last_login_at', + // 最近活跃 = max(最近登录, 最近发起比价, 最近发起领券),与大盘 DAU/留存活跃口径一致(2026-07-05) + title: ( + + 最近活跃 + + ), + dataIndex: 'last_active_at', width: 160, sorter: true, - sortOrder: sortOrderOf('last_login_at'), - render: (v: string) => formatUtcTime(v), + // 关掉 antd 默认的「点击升序/降序」表头提示,否则与上面自定义口径 Tooltip 同时弹出互相遮挡 + showSorterTooltip: false, + sortOrder: sortOrderOf('last_active_at'), + render: (v: string | null, u: UserListItem) => formatUtcTime(v ?? u.last_login_at), }, { title: '操作', @@ -324,7 +331,7 @@ export default function UsersPage() { allowClear /> setLoginRange(v as [Dayjs, Dayjs] | null)} allowClear diff --git a/src/lib/types.ts b/src/lib/types.ts index 4a8e52a..7f5ec3b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -56,6 +56,8 @@ export interface UserListItem { wechat_openid: string | null; created_at: string; last_login_at: string; + // 最近活跃 = max(最近登录, 最近发起比价, 最近发起领券);与大盘 DAU/留存活跃口径一致 + last_active_at: string | null; } // 设备维度新手引导:一台设备(ANDROID_ID)聚合,走过引导的账号数 + 最近完成时间。 @@ -474,6 +476,10 @@ export interface AdRevenueReport { daily: AdRevenueDaily[]; // 按日期汇总序列(全量,供按天趋势图) hourly: AdRevenueHourly[]; // 按小时汇总序列(全量,供按小时趋势图;按天查询时为空) type_stats: Record; // 按广告类型小计;前端取 draw / reward_video + // 按信息流场景小计(comparison/coupon/welfare;全量,不受分页截断)。数据大盘「领券广告/比价广告」卡 + // 优先读它:2026-07-02 后端起 items 不再含信息流逐条展示行(唯一带收益+场景的行),按 items 现算恒 0。 + // 可选(`?.` 探测)以兼容未升级后端,缺失时回退 items 现算。 + scene_stats?: Record; // 可选:后端预聚合的「按广告类型」富小计(含发放金币明细)。当前后端未下发 → 数据大盘页 // (dashboard adTypeSummary)用 `?.` 探测、缺失时按 items 现算兜底;后端补上即自动启用。 by_ad_type?: Record< @@ -529,6 +535,8 @@ export interface DashboardOverview { new: number; active: number; retained_new_users: number; + // 次日留存基数:窗口内逐日「前一日新增用户数」之和(retention_rate 的分母;旧后端无此字段) + retention_cohort?: number; retention_rate: number | null; retention_note: string; }; @@ -540,6 +548,17 @@ export interface DashboardOverview { average_duration_ms: number | null; average_saved_cents: number | null; }; + // 领券核心数据(2026-07-05 新增;旧后端无此字段,前端 `?.` 探测)。 + // 点位=一张券;成功口径 success+already_claimed;点位成功率分母=发起数×应领点位数(未跑到视为失败)。 + coupon?: { + started: number; + all_success: number; // 全部点位领成功的完成场次数 + success_rate: number | null; + point_success: number; + points_per_session: number | null; // 应领点位数(本期完成场点位数众数) + point_success_rate: number | null; + median_elapsed_ms: number | null; // 仅 completed,同「领券数据」页口径 + }; coins: { granted_total: number; reward_video_coin_total: number;