Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27a96d4430 |
@@ -20,6 +20,7 @@ import {
|
|||||||
Statistic,
|
Statistic,
|
||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
@@ -37,6 +38,7 @@ import type {
|
|||||||
AdRevenueRecord,
|
AdRevenueRecord,
|
||||||
AdRevenueReport,
|
AdRevenueReport,
|
||||||
AdRevenueRow,
|
AdRevenueRow,
|
||||||
|
AdRevenueTypeStat,
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
@@ -268,6 +270,7 @@ export default function AdRevenueReportPage() {
|
|||||||
// 「场景」作为后端全局筛选(feed_scene):同时影响明细 / 合计 / 趋势,与「用户 / 类型」一致,点「查询」生效。
|
// 「场景」作为后端全局筛选(feed_scene):同时影响明细 / 合计 / 趋势,与「用户 / 类型」一致,点「查询」生效。
|
||||||
const [scene, setScene] = useState<string | undefined>();
|
const [scene, setScene] = useState<string | undefined>();
|
||||||
const [granularity, setGranularity] = useState<'day' | 'hour'>('day');
|
const [granularity, setGranularity] = useState<'day' | 'hour'>('day');
|
||||||
|
const [sortBy, setSortBy] = useState<'time' | 'ecpm'>('time'); // 明细排序:time=时间倒序 / ecpm=eCPM 倒序
|
||||||
const [limit, setLimit] = useState<number>(500); // 每页条数(分页大小)
|
const [limit, setLimit] = useState<number>(500); // 每页条数(分页大小)
|
||||||
const [page, setPage] = useState<number>(1); // 当前页码(后端分页;1 起)
|
const [page, setPage] = useState<number>(1); // 当前页码(后端分页;1 起)
|
||||||
const [queriedGranularity, setQueriedGranularity] = useState<'day' | 'hour'>('day'); // 本次结果对应的粒度,决定是否显示「小时」列
|
const [queriedGranularity, setQueriedGranularity] = useState<'day' | 'hour'>('day'); // 本次结果对应的粒度,决定是否显示「小时」列
|
||||||
@@ -282,7 +285,7 @@ export default function AdRevenueReportPage() {
|
|||||||
|
|
||||||
// load(目标页, 每页条数):分页与筛选都经它。offset 由 页码×每页 算;场景(feed_scene)随筛选下推后端。
|
// load(目标页, 每页条数):分页与筛选都经它。offset 由 页码×每页 算;场景(feed_scene)随筛选下推后端。
|
||||||
const load = useCallback(
|
const load = useCallback(
|
||||||
async (targetPage = 1, targetLimit = limit) => {
|
async (targetPage = 1, targetLimit = limit, targetSort = sortBy) => {
|
||||||
const from = range[0].format('YYYY-MM-DD');
|
const from = range[0].format('YYYY-MM-DD');
|
||||||
const to = range[1].format('YYYY-MM-DD');
|
const to = range[1].format('YYYY-MM-DD');
|
||||||
const multiDay = from !== to;
|
const multiDay = from !== to;
|
||||||
@@ -299,6 +302,7 @@ export default function AdRevenueReportPage() {
|
|||||||
granularity: gran,
|
granularity: gran,
|
||||||
limit: targetLimit,
|
limit: targetLimit,
|
||||||
offset: (targetPage - 1) * targetLimit,
|
offset: (targetPage - 1) * targetLimit,
|
||||||
|
sort: targetSort,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setData(res.data);
|
setData(res.data);
|
||||||
@@ -312,7 +316,7 @@ export default function AdRevenueReportPage() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[range, userId, adType, scene, granularity, limit],
|
[range, userId, adType, scene, granularity, limit, sortBy],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -450,14 +454,11 @@ export default function AdRevenueReportPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 派生指标(全部基于全量 total_* 字段,不受 limit 截断影响,准):
|
// 派生指标(全部基于全量 total_* 字段,不受分页影响,准):
|
||||||
// 平均 eCPM = 收益÷展示×1000;发奖成本(元)= 实发金币÷汇率;预估毛利 = 收益−发奖成本;
|
// 发奖成本(元)= 实发金币÷汇率;预估毛利 = 收益−发奖成本;发奖占收益比 = 发奖成本÷收益;
|
||||||
// 发奖占收益比 = 发奖成本÷收益;应发实发差额(金币)= 应发−实发(正=少发/负=多发)。
|
// 应发实发差额(金币)= 应发−实发(正=少发/负=多发);ARPU = 预估收益÷今日DAU(仅今日)。
|
||||||
const derived = data
|
const derived = data
|
||||||
? {
|
? {
|
||||||
avgEcpm: data.total_impressions
|
|
||||||
? (data.total_revenue_yuan / data.total_impressions) * 1000
|
|
||||||
: 0,
|
|
||||||
payoutYuan: data.total_actual_coin / COIN_PER_YUAN,
|
payoutYuan: data.total_actual_coin / COIN_PER_YUAN,
|
||||||
grossProfit: data.total_revenue_yuan - data.total_actual_coin / COIN_PER_YUAN,
|
grossProfit: data.total_revenue_yuan - data.total_actual_coin / COIN_PER_YUAN,
|
||||||
payoutRatioPct:
|
payoutRatioPct:
|
||||||
@@ -465,9 +466,17 @@ export default function AdRevenueReportPage() {
|
|||||||
? (data.total_actual_coin / COIN_PER_YUAN / data.total_revenue_yuan) * 100
|
? (data.total_actual_coin / COIN_PER_YUAN / data.total_revenue_yuan) * 100
|
||||||
: null,
|
: null,
|
||||||
coinGap: data.total_expected_coin - data.total_actual_coin,
|
coinGap: data.total_expected_coin - data.total_actual_coin,
|
||||||
|
// ARPU(今日)= 预估广告收益 ÷ 今日 DAU;dau 为 null(历史/多天)或 0 时不可算 → null,前端显示「-」。
|
||||||
|
arpu: data.dau && data.dau > 0 ? data.total_revenue_yuan / data.dau : null,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// 第二行大盘「分广告类型」:draw / 激励视频 各自 收益 / eCPM / 展示条数;eCPM = 收益÷展示×1000。
|
||||||
|
const drawStat = data?.type_stats?.draw;
|
||||||
|
const rvStat = data?.type_stats?.reward_video;
|
||||||
|
const ecpmOf = (s?: AdRevenueTypeStat) =>
|
||||||
|
s && s.impressions > 0 ? (s.revenue_yuan / s.impressions) * 1000 : 0;
|
||||||
|
|
||||||
// 明细直接用后端返回的当前页 items(「场景」已由后端 feed_scene 全局过滤,前端不再二次筛)。
|
// 明细直接用后端返回的当前页 items(「场景」已由后端 feed_scene 全局过滤,前端不再二次筛)。
|
||||||
const items = data?.items ?? [];
|
const items = data?.items ?? [];
|
||||||
|
|
||||||
@@ -569,6 +578,21 @@ export default function AdRevenueReportPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
|
<Space size={6}>
|
||||||
|
<Typography.Text type="secondary">排序</Typography.Text>
|
||||||
|
<Select
|
||||||
|
value={sortBy}
|
||||||
|
onChange={(v) => {
|
||||||
|
setSortBy(v);
|
||||||
|
load(1, limit, v); // 改排序:回第 1 页并重查
|
||||||
|
}}
|
||||||
|
style={{ width: 130 }}
|
||||||
|
options={[
|
||||||
|
{ value: 'time', label: '时间倒序' },
|
||||||
|
{ value: 'ecpm', label: 'eCPM 倒序' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
<Space size={6}>
|
<Space size={6}>
|
||||||
<Typography.Text type="secondary">每页</Typography.Text>
|
<Typography.Text type="secondary">每页</Typography.Text>
|
||||||
<Select
|
<Select
|
||||||
@@ -591,41 +615,46 @@ export default function AdRevenueReportPage() {
|
|||||||
<Card size="small" style={{ marginBottom: 16 }}>
|
<Card size="small" style={{ marginBottom: 16 }}>
|
||||||
<Divider orientation="left" plain style={{ marginTop: 0, marginBottom: 16 }}>
|
<Divider orientation="left" plain style={{ marginTop: 0, marginBottom: 16 }}>
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
广告展示与收益
|
核心指标
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Divider>
|
</Divider>
|
||||||
<Row gutter={[16, 12]}>
|
<Row gutter={[16, 12]}>
|
||||||
<Col span={6}>
|
<Col flex="1 1 0">
|
||||||
<Statistic title="展示条数合计" value={data.total_impressions} />
|
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
|
||||||
<Statistic title="平均 eCPM(元/千次)" value={derived.avgEcpm} precision={2} />
|
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
|
||||||
<Statistic title="预估收益合计(元)" value={data.total_revenue_yuan} precision={4} />
|
<Statistic title="预估收益合计(元)" value={data.total_revenue_yuan} precision={4} />
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={6}>
|
<Col flex="1 1 0">
|
||||||
<Statistic title="广告事件数" value={data.total} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Divider orientation="left" plain style={{ marginTop: 20, marginBottom: 16 }}>
|
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
||||||
发奖与对账
|
|
||||||
</Typography.Text>
|
|
||||||
</Divider>
|
|
||||||
<Row gutter={[16, 12]}>
|
|
||||||
<Col span={6}>
|
|
||||||
<Statistic title="应发金币合计" value={data.total_expected_coin} />
|
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
|
||||||
<Statistic title="实发金币合计" value={data.total_actual_coin} />
|
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
|
||||||
<Statistic title="发奖成本(元)" value={derived.payoutYuan} precision={4} />
|
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
|
||||||
<Statistic
|
<Statistic
|
||||||
title="预估毛利(元)"
|
title={
|
||||||
|
<Tooltip title="每活跃用户广告预估收入 = 预估收益 ÷ 今日 DAU;DAU 仅今日口径,历史/多天显示 -">
|
||||||
|
<span>
|
||||||
|
ARPU(今日)
|
||||||
|
<InfoCircleOutlined style={{ marginLeft: 4 }} />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
value={derived.arpu ?? '-'}
|
||||||
|
precision={4}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col flex="1 1 0">
|
||||||
|
<Statistic
|
||||||
|
title={
|
||||||
|
<Tooltip title="今日活跃用户(复用大盘口径 last_login_at = 今日登录过);仅查询=今日时有值,历史/多天显示 -">
|
||||||
|
<span>
|
||||||
|
今日活跃 DAU
|
||||||
|
<InfoCircleOutlined style={{ marginLeft: 4 }} />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
value={data.dau ?? '-'}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col flex="1 1 0">
|
||||||
|
<Statistic title="发放金币合计(元)" value={derived.payoutYuan} precision={4} />
|
||||||
|
</Col>
|
||||||
|
<Col flex="1 1 0">
|
||||||
|
<Statistic
|
||||||
|
title="预估广告毛利(元)"
|
||||||
value={derived.grossProfit}
|
value={derived.grossProfit}
|
||||||
precision={4}
|
precision={4}
|
||||||
prefix={derived.grossProfit >= 0 ? <RiseOutlined /> : <FallOutlined />}
|
prefix={derived.grossProfit >= 0 ? <RiseOutlined /> : <FallOutlined />}
|
||||||
@@ -637,6 +666,31 @@ export default function AdRevenueReportPage() {
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Divider orientation="left" plain style={{ marginTop: 20, marginBottom: 16 }}>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
分广告类型
|
||||||
|
</Typography.Text>
|
||||||
|
</Divider>
|
||||||
|
<Row gutter={[16, 12]}>
|
||||||
|
<Col span={4}>
|
||||||
|
<Statistic title="Draw 信息流收益(元)" value={drawStat?.revenue_yuan ?? 0} precision={4} />
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Statistic title="Draw 信息流 eCPM(元/千次)" value={ecpmOf(drawStat)} precision={2} />
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Statistic title="Draw 信息流条数" value={drawStat?.impressions ?? 0} />
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Statistic title="激励视频收益(元)" value={rvStat?.revenue_yuan ?? 0} precision={4} />
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Statistic title="激励视频 eCPM(元/千次)" value={ecpmOf(rvStat)} precision={2} />
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Statistic title="激励视频条数" value={rvStat?.impressions ?? 0} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -383,6 +383,12 @@ export interface AdRevenueHourly {
|
|||||||
actual_coin: number;
|
actual_coin: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 广告收益报表:按广告类型(ad_type)的小计(展示条数 + 预估收益;eCPM 前端用 收益÷展示×1000 算)
|
||||||
|
export interface AdRevenueTypeStat {
|
||||||
|
impressions: number; // 该类型展示条数合计
|
||||||
|
revenue_yuan: number; // 该类型预估收益合计(元)
|
||||||
|
}
|
||||||
|
|
||||||
// 广告收益报表:一次广告事件(逐条一行)。激励视频展示+发奖按 ad_session_id 合并;信息流展示/发奖各自成行。
|
// 广告收益报表:一次广告事件(逐条一行)。激励视频展示+发奖按 ad_session_id 合并;信息流展示/发奖各自成行。
|
||||||
export interface AdRevenueRow {
|
export interface AdRevenueRow {
|
||||||
event_key: string; // 事件稳定唯一键(前端 rowKey)
|
event_key: string; // 事件稳定唯一键(前端 rowKey)
|
||||||
@@ -416,6 +422,8 @@ export interface AdRevenueReport {
|
|||||||
date_to: string; // 结束日 北京时间 YYYY-MM-DD(闭区间;单日时与 date_from 相同)
|
date_to: string; // 结束日 北京时间 YYYY-MM-DD(闭区间;单日时与 date_from 相同)
|
||||||
daily: AdRevenueDaily[]; // 按日期汇总序列(全量,供按天趋势图)
|
daily: AdRevenueDaily[]; // 按日期汇总序列(全量,供按天趋势图)
|
||||||
hourly: AdRevenueHourly[]; // 按小时汇总序列(全量,供按小时趋势图;按天查询时为空)
|
hourly: AdRevenueHourly[]; // 按小时汇总序列(全量,供按小时趋势图;按天查询时为空)
|
||||||
|
type_stats: Record<string, AdRevenueTypeStat>; // 按广告类型小计;前端取 draw / reward_video
|
||||||
|
dau: number | null; // 今日活跃(复用大盘 last_login_at);仅查询=今日时有值,否则 null
|
||||||
total: number; // 当前筛选下的分页总条数(全量,不受分页影响)
|
total: number; // 当前筛选下的分页总条数(全量,不受分页影响)
|
||||||
truncated: boolean; // 当前页之后是否还有更多事件(分页后前端不再据此报警)
|
truncated: boolean; // 当前页之后是否还有更多事件(分页后前端不再据此报警)
|
||||||
total_impressions: number;
|
total_impressions: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user