From 52c5830127fdfab674dd1de2c4146bdbb183791a Mon Sep 17 00:00:00 2001 From: zzhyyyyy <2685922758@qq.com> Date: Tue, 23 Jun 2026 22:07:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(ad-revenue):=20=E6=8A=A5=E8=A1=A8=E4=B8=BB?= =?UTF-8?q?=E8=A1=A8=E6=94=B9=E9=80=90=E6=9D=A1=E5=B9=BF=E5=91=8A=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=20+=20=E7=94=A8=E6=88=B7=E5=88=97=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E6=89=8B=E6=9C=BA=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对齐后端 /admin/api/ad-revenue-report 的逐条事件结构(每次广告一行,不再按用户折叠): - 主表列:时间/用户/类型/应用/广告位/eCPM/收益/发奖状态/应发/实发/一致/底层ADN; 点行展开看该条金币复算因子(因子1/因子2/份数/LT) - 「用户」列从 user_id 改为显示完整手机号(辅小字 #uid);查不到手机号显示 #uid(无手机号) - 纯展示行(信息流逐条展示 / 无对应发奖):金币列显「-」、状态显「仅展示」、不计对账 - 统计卡「聚合组数」→「广告事件数」,提示文案 组→条 - rowKey 用后端下发的 event_key tsc --noEmit 本次两文件零类型错误。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/(main)/ad-revenue/page.tsx | 185 ++++++++++++++--------------- src/lib/types.ts | 33 +++-- 2 files changed, 113 insertions(+), 105 deletions(-) diff --git a/src/app/(main)/ad-revenue/page.tsx b/src/app/(main)/ad-revenue/page.tsx index c6cb94e..77e17b2 100644 --- a/src/app/(main)/ad-revenue/page.tsx +++ b/src/app/(main)/ad-revenue/page.tsx @@ -35,7 +35,6 @@ import { api, errMsg } from '@/lib/api'; import { formatUtcTime } from '@/lib/format'; import type { AdRevenueDaily, - AdRevenueImpression, AdRevenueRecord, AdRevenueReport, AdRevenueRow, @@ -94,25 +93,7 @@ const LT_FACTOR_ROWS = [ { key: '5', lt: '第 11 条及以后', factor: 1.0 }, ]; -// 展开行 - 展示明细:每条广告展示一行(时间/eCPM/收益/adn/底层位) -const IMPRESSION_COLUMNS: ColumnsType = [ - { title: '时间', dataIndex: 'created_at', render: (v: string) => formatUtcTime(v), width: 165 }, - { title: 'eCPM(分)', dataIndex: 'ecpm', width: 100 }, - { title: '预估收益(元)', dataIndex: 'revenue_yuan', width: 120, render: (v: number) => v.toFixed(4) }, - { - title: 'adn', - dataIndex: 'adn', - width: 90, - render: (v: string | null) => v ?? '-', - }, - { - title: '底层代码位ID', - dataIndex: 'slot_id', - render: (v: string | null) => v ?? '-', - }, -]; - -// 展开行 - 发奖明细:该组的逐条发奖复算明细(还原原金币审计的 eCPM/因子1/份数/LT/因子2 等列) +// 展开行(逐条事件展开)- 该条的发奖复算明细(还原金币审计的 eCPM/因子1/份数/LT/因子2 等列) const DETAIL_COLUMNS: ColumnsType = [ { title: '时间', dataIndex: 'created_at', render: (v: string) => formatUtcTime(v), width: 165 }, { @@ -413,24 +394,29 @@ export default function AdRevenuePage() { const columns: ColumnsType = [ ...(queriedMultiDay - ? [{ title: '日期', dataIndex: 'report_date', width: 110, fixed: 'left' } as ColumnsType[number]] - : []), - { title: '用户', dataIndex: 'user_id', width: 80 }, - ...(queriedGranularity === 'hour' - ? [ - { - title: '小时', - dataIndex: 'hour', - width: 90, - render: (h: number | null) => - h == null ? '-' : `${String(h).padStart(2, '0')}:00`, - } as ColumnsType[number], - ] + ? [{ title: '日期', dataIndex: 'report_date', width: 105, fixed: 'left' } as ColumnsType[number]] : []), + { title: '时间', dataIndex: 'created_at', width: 165, render: (v: string) => formatUtcTime(v) }, + { + title: '用户', + dataIndex: 'user_phone', + width: 150, + render: (phone: string | null, r: AdRevenueRow) => + phone ? ( + + {phone} + + #{r.user_id} + + + ) : ( + #{r.user_id}(无手机号) + ), + }, { title: '广告类型', dataIndex: 'ad_type', - width: 120, + width: 110, render: (s: string) => { const t = TYPE_TAG[s] ?? { color: 'default', label: s }; return {t.label}; @@ -439,7 +425,7 @@ export default function AdRevenuePage() { { title: '来源应用', dataIndex: 'app_env', - width: 150, + width: 140, render: (v: string | null) => { if (!v) return 未知; const t = APP_TAG[v] ?? { color: 'default', label: v }; @@ -449,38 +435,68 @@ export default function AdRevenuePage() { { title: '广告位ID', dataIndex: 'our_code_id', - width: 120, + width: 110, render: (v: string | null) => v ? {v} : -, }, - { title: '展示条数', dataIndex: 'impressions', width: 90, align: 'right' }, + { + title: 'eCPM(分)', + dataIndex: 'ecpm', + width: 100, + align: 'right', + render: (v: string | null) => v ?? '-', + }, { title: '预估收益(元)', dataIndex: 'revenue_yuan', - width: 120, + width: 110, align: 'right', - render: (v: number) => v.toFixed(4), + render: (v: number, r: AdRevenueRow) => (r.has_impression ? v.toFixed(4) : '-'), + }, + { + title: '发奖状态', + dataIndex: 'status', + width: 100, + render: (s: string | null) => { + if (!s) return 仅展示; + const t = STATUS_TAG[s] ?? { color: 'default', label: s }; + return {t.label}; + }, }, { title: '应发金币', dataIndex: 'expected_coin', - width: 100, + width: 90, align: 'right', - render: (v: number) => {v}, + render: (v: number, r: AdRevenueRow) => + r.has_reward ? {v} : -, + }, + { + title: '实发金币', + dataIndex: 'actual_coin', + width: 90, + align: 'right', + render: (v: number, r: AdRevenueRow) => + r.has_reward ? v : -, }, - { title: '实发金币', dataIndex: 'actual_coin', width: 100, align: 'right' }, { title: '一致', dataIndex: 'matched', - width: 80, + width: 70, align: 'center', - render: (m: boolean) => (m ? : ✗ 不符), + render: (m: boolean, r: AdRevenueRow) => + r.has_reward ? ( + m ? : ✗ 不符 + ) : ( + - + ), }, { title: '底层 ADN', - dataIndex: 'adns', - render: (v: string[]) => - v.length ? v.map((a) => {a}) : -, + dataIndex: 'adn', + width: 100, + render: (v: string | null) => + v ? {v} : -, }, ]; @@ -591,7 +607,7 @@ export default function AdRevenuePage() { value={limit} onChange={setLimit} style={{ width: 110 }} - options={[100, 200, 500, 1000].map((n) => ({ value: n, label: `${n} 组` }))} + options={[100, 200, 500, 1000].map((n) => ({ value: n, label: `${n} 条` }))} />