From fd913357457a0af8f90947c490ed7879ea4e0455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E4=B8=96=E7=9D=BF?= <2839904623@qq.com> Date: Sun, 5 Jul 2026 15:03:29 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E5=A4=A7=E7=9B=98=E9=A2=86=E5=88=B8?= =?UTF-8?q?=E5=92=8C=E6=AF=94=E4=BB=B7=E5=B9=BF=E5=91=8A=E6=94=B6=E7=9B=8A?= =?UTF-8?q?=E5=8D=A1=E6=94=B9=E8=AF=BB=E6=9C=8D=E5=8A=A1=E7=AB=AF=E5=85=A8?= =?UTF-8?q?=E9=87=8F=E5=9C=BA=E6=99=AF=E5=B0=8F=E8=AE=A1,=E6=BF=80?= =?UTF-8?q?=E5=8A=B1=E8=A7=86=E9=A2=91=E5=8D=A1=E6=94=B9=E8=AF=BB=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=B0=8F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/(main)/dashboard/page.tsx | 10 +++++++--- src/lib/types.ts | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app/(main)/dashboard/page.tsx b/src/app/(main)/dashboard/page.tsx index 725f4d5..f0ab98c 100644 --- a/src/app/(main)/dashboard/page.tsx +++ b/src/app/(main)/dashboard/page.tsx @@ -131,19 +131,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 { diff --git a/src/lib/types.ts b/src/lib/types.ts index 781b829..c0e6a80 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -474,6 +474,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< -- 2.52.0 From 2994b75dab60ded53b90c13c622f8af4fba70ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E4=B8=96=E7=9D=BF?= <2839904623@qq.com> Date: Sun, 5 Jul 2026 15:03:30 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E5=A4=A7=E7=9B=98=E7=95=99=E5=AD=98?= =?UTF-8?q?=E5=8D=A1=E6=A0=87=E9=A2=98=E4=B8=8E=E5=8F=A3=E5=BE=84=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E6=94=B9=E4=B8=BA=E5=89=8D=E6=97=A5=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=9A=84=E6=AC=A1=E6=97=A5=E7=95=99=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/(main)/dashboard/page.tsx | 7 ++++--- src/lib/types.ts | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/(main)/dashboard/page.tsx b/src/app/(main)/dashboard/page.tsx index f0ab98c..af8fc5f 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) { diff --git a/src/lib/types.ts b/src/lib/types.ts index c0e6a80..11b8806 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -533,6 +533,8 @@ export interface DashboardOverview { new: number; active: number; retained_new_users: number; + // 次日留存基数:窗口内逐日「前一日新增用户数」之和(retention_rate 的分母;旧后端无此字段) + retention_cohort?: number; retention_rate: number | null; retention_note: string; }; -- 2.52.0 From f9ed7d25588e64226afe43f9736eeb97221681ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E4=B8=96=E7=9D=BF?= <2839904623@qq.com> Date: Sun, 5 Jul 2026 15:03:30 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=A4=A7=E7=9B=98?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=A2=86=E5=88=B8=E6=A0=B8=E5=BF=83=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=8C=BA=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/(main)/dashboard/page.tsx | 62 +++++++++++++++++++++++++++++++ src/lib/types.ts | 11 ++++++ 2 files changed, 73 insertions(+) diff --git a/src/app/(main)/dashboard/page.tsx b/src/app/(main)/dashboard/page.tsx index af8fc5f..0046929 100644 --- a/src/app/(main)/dashboard/page.tsx +++ b/src/app/(main)/dashboard/page.tsx @@ -526,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, @@ -766,6 +783,51 @@ export default function DashboardPage() { +
+
+ +

领券核心数据

+ 发起、整场成功、点位成功与效率 +
+
+ + + + +
+
+
diff --git a/src/lib/types.ts b/src/lib/types.ts index 11b8806..209c482 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -546,6 +546,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; -- 2.52.0 From 089041abef30bebecc0e04f154a4cab9acc1688e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E4=B8=96=E7=9D=BF?= <2839904623@qq.com> Date: Sun, 5 Jul 2026 15:03:31 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=9C=80=E8=BF=91=E7=99=BB=E5=BD=95=E5=88=97=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E6=9C=80=E8=BF=91=E6=B4=BB=E8=B7=83,=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E3=80=81=E6=8E=92=E5=BA=8F=E3=80=81=E7=AD=9B=E9=80=89=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E6=8D=A2=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/(main)/users/page.tsx | 25 ++++++++++++++++--------- src/lib/types.ts | 2 ++ 2 files changed, 18 insertions(+), 9 deletions(-) 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 209c482..204b6aa 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)聚合,走过引导的账号数 + 最近完成时间。 -- 2.52.0