|
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
|
|
Statistic,
|
|
|
|
|
Table,
|
|
|
|
|
Tag,
|
|
|
|
|
Tooltip,
|
|
|
|
|
Typography,
|
|
|
|
|
} from 'antd';
|
|
|
|
|
import {
|
|
|
|
@@ -37,6 +38,7 @@ import type {
|
|
|
|
|
AdRevenueRecord,
|
|
|
|
|
AdRevenueReport,
|
|
|
|
|
AdRevenueRow,
|
|
|
|
|
AdRevenueTypeStat,
|
|
|
|
|
} from '@/lib/types';
|
|
|
|
|
|
|
|
|
|
const { RangePicker } = DatePicker;
|
|
|
|
@@ -268,6 +270,7 @@ export default function AdRevenueReportPage() {
|
|
|
|
|
// 「场景」作为后端全局筛选(feed_scene):同时影响明细 / 合计 / 趋势,与「用户 / 类型」一致,点「查询」生效。
|
|
|
|
|
const [scene, setScene] = useState<string | undefined>();
|
|
|
|
|
const [granularity, setGranularity] = useState<'day' | 'hour'>('day');
|
|
|
|
|
const [sortBy, setSortBy] = useState<'time' | 'ecpm'>('time'); // 明细排序:time=时间倒序 / ecpm=eCPM 倒序
|
|
|
|
|
const [limit, setLimit] = useState<number>(500); // 每页条数(分页大小)
|
|
|
|
|
const [page, setPage] = useState<number>(1); // 当前页码(后端分页;1 起)
|
|
|
|
|
const [queriedGranularity, setQueriedGranularity] = useState<'day' | 'hour'>('day'); // 本次结果对应的粒度,决定是否显示「小时」列
|
|
|
|
@@ -282,7 +285,7 @@ export default function AdRevenueReportPage() {
|
|
|
|
|
|
|
|
|
|
// load(目标页, 每页条数):分页与筛选都经它。offset 由 页码×每页 算;场景(feed_scene)随筛选下推后端。
|
|
|
|
|
const load = useCallback(
|
|
|
|
|
async (targetPage = 1, targetLimit = limit) => {
|
|
|
|
|
async (targetPage = 1, targetLimit = limit, targetSort = sortBy) => {
|
|
|
|
|
const from = range[0].format('YYYY-MM-DD');
|
|
|
|
|
const to = range[1].format('YYYY-MM-DD');
|
|
|
|
|
const multiDay = from !== to;
|
|
|
|
@@ -299,6 +302,7 @@ export default function AdRevenueReportPage() {
|
|
|
|
|
granularity: gran,
|
|
|
|
|
limit: targetLimit,
|
|
|
|
|
offset: (targetPage - 1) * targetLimit,
|
|
|
|
|
sort: targetSort,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
setData(res.data);
|
|
|
|
@@ -312,7 +316,7 @@ export default function AdRevenueReportPage() {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[range, userId, adType, scene, granularity, limit],
|
|
|
|
|
[range, userId, adType, scene, granularity, limit, sortBy],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
@@ -450,14 +454,11 @@ export default function AdRevenueReportPage() {
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 派生指标(全部基于全量 total_* 字段,不受 limit 截断影响,准):
|
|
|
|
|
// 平均 eCPM = 收益÷展示×1000;发奖成本(元)= 实发金币÷汇率;预估毛利 = 收益−发奖成本;
|
|
|
|
|
// 发奖占收益比 = 发奖成本÷收益;应发实发差额(金币)= 应发−实发(正=少发/负=多发)。
|
|
|
|
|
// 派生指标(全部基于全量 total_* 字段,不受分页影响,准):
|
|
|
|
|
// 发奖成本(元)= 实发金币÷汇率;预估毛利 = 收益−发奖成本;发奖占收益比 = 发奖成本÷收益;
|
|
|
|
|
// 应发实发差额(金币)= 应发−实发(正=少发/负=多发);ARPU = 预估收益÷今日DAU(仅今日)。
|
|
|
|
|
const derived = data
|
|
|
|
|
? {
|
|
|
|
|
avgEcpm: data.total_impressions
|
|
|
|
|
? (data.total_revenue_yuan / data.total_impressions) * 1000
|
|
|
|
|
: 0,
|
|
|
|
|
payoutYuan: data.total_actual_coin / COIN_PER_YUAN,
|
|
|
|
|
grossProfit: data.total_revenue_yuan - data.total_actual_coin / COIN_PER_YUAN,
|
|
|
|
|
payoutRatioPct:
|
|
|
|
@@ -465,9 +466,17 @@ export default function AdRevenueReportPage() {
|
|
|
|
|
? (data.total_actual_coin / COIN_PER_YUAN / data.total_revenue_yuan) * 100
|
|
|
|
|
: null,
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
// 第二行大盘「分广告类型」: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 全局过滤,前端不再二次筛)。
|
|
|
|
|
const items = data?.items ?? [];
|
|
|
|
|
|
|
|
|
@@ -569,6 +578,21 @@ export default function AdRevenueReportPage() {
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
</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}>
|
|
|
|
|
<Typography.Text type="secondary">每页</Typography.Text>
|
|
|
|
|
<Select
|
|
|
|
@@ -591,41 +615,46 @@ export default function AdRevenueReportPage() {
|
|
|
|
|
<Card size="small" style={{ marginBottom: 16 }}>
|
|
|
|
|
<Divider orientation="left" plain style={{ marginTop: 0, marginBottom: 16 }}>
|
|
|
|
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
|
|
|
广告展示与收益
|
|
|
|
|
核心指标
|
|
|
|
|
</Typography.Text>
|
|
|
|
|
</Divider>
|
|
|
|
|
<Row gutter={[16, 12]}>
|
|
|
|
|
<Col span={6}>
|
|
|
|
|
<Statistic title="展示条数合计" value={data.total_impressions} />
|
|
|
|
|
</Col>
|
|
|
|
|
<Col span={6}>
|
|
|
|
|
<Statistic title="平均 eCPM(元/千次)" value={derived.avgEcpm} precision={2} />
|
|
|
|
|
</Col>
|
|
|
|
|
<Col span={6}>
|
|
|
|
|
<Col flex="1 1 0">
|
|
|
|
|
<Statistic title="预估收益合计(元)" value={data.total_revenue_yuan} precision={4} />
|
|
|
|
|
</Col>
|
|
|
|
|
<Col span={6}>
|
|
|
|
|
<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}>
|
|
|
|
|
<Col flex="1 1 0">
|
|
|
|
|
<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}
|
|
|
|
|
precision={4}
|
|
|
|
|
prefix={derived.grossProfit >= 0 ? <RiseOutlined /> : <FallOutlined />}
|
|
|
|
@@ -637,6 +666,31 @@ export default function AdRevenueReportPage() {
|
|
|
|
|
</Typography.Text>
|
|
|
|
|
</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={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>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|