Compare commits
2 Commits
ea6aca18fc
..
0.2.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 510ce349f7 | |||
| 03ef222b71 |
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ interface PreviewItem {
|
||||
|
||||
const yuan = (cents: number) => (cents / 100).toFixed(2);
|
||||
|
||||
/** 首页轮播「种子」管理(真实记录不足时的兜底假数据,现为「生成规则」)。「数据大盘」页的一个区块。 */
|
||||
/** 首页轮播「种子」管理(真实记录不足时的兜底假数据,现为「生成规则」)。「系统配置 / 首页」里的一个区块。 */
|
||||
export default function HomeMarqueeSeeds() {
|
||||
const { message } = App.useApp();
|
||||
const [seeds, setSeeds] = useState<Seed[]>([]);
|
||||
+1
-1
@@ -246,7 +246,7 @@ function buildBody(metric: string, e: Edit, immediate: boolean): Record<string,
|
||||
return body;
|
||||
}
|
||||
|
||||
/** 首页三门面数字(帮助用户/完成比价/累计节省)的展示模式配置。作为「数据大盘」页的一个区块。 */
|
||||
/** 首页三门面数字(帮助用户/完成比价/累计节省)的展示模式配置。「系统配置 / 首页」里的一个区块。 */
|
||||
export default function HomeStatsConfig() {
|
||||
const { message, modal } = App.useApp();
|
||||
const [items, setItems] = useState<StatItem[]>([]);
|
||||
@@ -1,8 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { App, Button, Card, Input, InputNumber, Space, Spin, Switch, Tag, Tooltip } from 'antd';
|
||||
import { App, Button, Card, Input, InputNumber, Space, Spin, Switch, Tabs, Tag, Tooltip } from 'antd';
|
||||
import { api, errMsg } from '@/lib/api';
|
||||
import HomeMarqueeSeeds from './HomeMarqueeSeeds';
|
||||
import HomeStatsConfig from './HomeStatsConfig';
|
||||
|
||||
interface ConfigItem {
|
||||
key: string;
|
||||
@@ -80,9 +82,66 @@ export default function ConfigPage() {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <Spin style={{ display: 'block', marginTop: 80 }} />;
|
||||
|
||||
const groups = [...new Set(items.map((i) => i.group))];
|
||||
const welfareConfig = loading ? (
|
||||
<Spin style={{ display: 'block', marginTop: 24 }} />
|
||||
) : (
|
||||
groups.map((g) => (
|
||||
<Card key={g} title={g} size="small" style={{ marginBottom: 16 }}>
|
||||
{items
|
||||
.filter((i) => i.group === g)
|
||||
.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
style={{ marginBottom: 12, paddingBottom: 12, borderBottom: '1px solid #f0f0f0' }}
|
||||
>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<b>{item.label}</b> {item.overridden ? <Tag color="blue">已改</Tag> : <Tag>默认</Tag>}
|
||||
{item.help && (
|
||||
<Tooltip title={item.help}>
|
||||
<span style={{ color: '#999', marginLeft: 6, fontSize: 12, cursor: 'help' }}>
|
||||
ⓘ
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<Space wrap>
|
||||
{item.type === 'bool' ? (
|
||||
<Switch
|
||||
checked={!!edits[item.key]}
|
||||
onChange={(v) => setEdits({ ...edits, [item.key]: v })}
|
||||
/>
|
||||
) : item.type === 'int' ? (
|
||||
<InputNumber
|
||||
value={edits[item.key]}
|
||||
onChange={(v) => setEdits({ ...edits, [item.key]: v })}
|
||||
style={{ width: 180 }}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
value={edits[item.key]}
|
||||
onChange={(e) => setEdits({ ...edits, [item.key]: e.target.value })}
|
||||
style={{ width: 380 }}
|
||||
placeholder={
|
||||
item.type === 'int_list' ? '逗号分隔,如 10, 20, 30' : 'JSON,如 {"key": 100}'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
loading={saving === item.key}
|
||||
onClick={() => save(item)}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<span style={{ color: '#bbb', fontSize: 12 }}>默认 {JSON.stringify(item.default)}</span>
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
))
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -90,64 +149,21 @@ export default function ConfigPage() {
|
||||
<p style={{ color: '#999' }}>
|
||||
改完即生效(业务下次读取用新值)。涉及金额 / 上限,改前请确认。每次改动都进审计日志。
|
||||
</p>
|
||||
{groups.map((g) => (
|
||||
<Card key={g} title={g} size="small" style={{ marginBottom: 16 }}>
|
||||
{items
|
||||
.filter((i) => i.group === g)
|
||||
.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
style={{ marginBottom: 12, paddingBottom: 12, borderBottom: '1px solid #f0f0f0' }}
|
||||
>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<b>{item.label}</b>{' '}
|
||||
{item.overridden ? <Tag color="blue">已改</Tag> : <Tag>默认</Tag>}
|
||||
{item.help && (
|
||||
<Tooltip title={item.help}>
|
||||
<span style={{ color: '#999', marginLeft: 6, fontSize: 12, cursor: 'help' }}>
|
||||
ⓘ
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<Space wrap>
|
||||
{item.type === 'bool' ? (
|
||||
<Switch
|
||||
checked={!!edits[item.key]}
|
||||
onChange={(v) => setEdits({ ...edits, [item.key]: v })}
|
||||
/>
|
||||
) : item.type === 'int' ? (
|
||||
<InputNumber
|
||||
value={edits[item.key]}
|
||||
onChange={(v) => setEdits({ ...edits, [item.key]: v })}
|
||||
style={{ width: 180 }}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
value={edits[item.key]}
|
||||
onChange={(e) => setEdits({ ...edits, [item.key]: e.target.value })}
|
||||
style={{ width: 380 }}
|
||||
placeholder={
|
||||
item.type === 'int_list' ? '逗号分隔,如 10, 20, 30' : 'JSON,如 {"key": 100}'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
loading={saving === item.key}
|
||||
onClick={() => save(item)}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
<span style={{ color: '#bbb', fontSize: 12 }}>
|
||||
默认 {JSON.stringify(item.default)}
|
||||
</span>
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
))}
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'home',
|
||||
label: '首页',
|
||||
children: (
|
||||
<>
|
||||
<HomeStatsConfig />
|
||||
<HomeMarqueeSeeds />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{ key: 'welfare', label: '福利页', children: welfareConfig },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+1265
-49
File diff suppressed because it is too large
Load Diff
+75
-2
@@ -383,6 +383,12 @@ export interface AdRevenueHourly {
|
||||
actual_coin: number;
|
||||
}
|
||||
|
||||
// 广告收益报表:按广告类型(ad_type)的小计(展示条数 + 预估收益;eCPM 前端用 收益÷展示×1000 算)
|
||||
export interface AdRevenueTypeStat {
|
||||
impressions: number; // 该类型展示条数合计
|
||||
revenue_yuan: number; // 该类型预估收益合计(元)
|
||||
}
|
||||
|
||||
// 广告收益报表:一次广告事件(逐条一行)。激励视频展示+发奖按 ad_session_id 合并;信息流展示/发奖各自成行。
|
||||
export interface AdRevenueRow {
|
||||
event_key: string; // 事件稳定唯一键(前端 rowKey)
|
||||
@@ -416,6 +422,8 @@ export interface AdRevenueReport {
|
||||
date_to: string; // 结束日 北京时间 YYYY-MM-DD(闭区间;单日时与 date_from 相同)
|
||||
daily: AdRevenueDaily[]; // 按日期汇总序列(全量,供按天趋势图)
|
||||
hourly: AdRevenueHourly[]; // 按小时汇总序列(全量,供按小时趋势图;按天查询时为空)
|
||||
type_stats: Record<string, AdRevenueTypeStat>; // 按广告类型小计;前端取 draw / reward_video
|
||||
dau: number | null; // 今日活跃(复用大盘 last_login_at);仅查询=今日时有值,否则 null
|
||||
total: number; // 当前筛选下的分页总条数(全量,不受分页影响)
|
||||
truncated: boolean; // 当前页之后是否还有更多事件(分页后前端不再据此报警)
|
||||
total_impressions: number;
|
||||
@@ -423,6 +431,13 @@ export interface AdRevenueReport {
|
||||
total_expected_coin: number;
|
||||
total_actual_coin: number;
|
||||
mismatch_count: number; // 应发≠实发的发奖条数
|
||||
by_ad_type?: Record<string, {
|
||||
ad_type: string;
|
||||
impressions: number;
|
||||
revenue_yuan: number;
|
||||
expected_coin: number;
|
||||
actual_coin: number;
|
||||
}>;
|
||||
items: AdRevenueRow[];
|
||||
}
|
||||
|
||||
@@ -435,7 +450,17 @@ export interface DashboardOverview {
|
||||
new_today: number;
|
||||
dau: number;
|
||||
};
|
||||
coins: { granted_total: number };
|
||||
coins: {
|
||||
granted_total: number;
|
||||
reward_video_coin_total?: number;
|
||||
reward_video_watch_count?: number;
|
||||
feed_ad_coin_total?: number;
|
||||
feed_ad_watch_count?: number;
|
||||
signin_coin_total?: number;
|
||||
signin_count?: number;
|
||||
signin_boost_coin_total?: number;
|
||||
signin_boost_watch_count?: number;
|
||||
};
|
||||
cash: {
|
||||
withdraw_success_cents: number;
|
||||
withdraw_pending_count: number;
|
||||
@@ -443,8 +468,56 @@ export interface DashboardOverview {
|
||||
withdraw_failed_count: number;
|
||||
};
|
||||
comparison: { total: number; success: number; success_rate: number };
|
||||
period: {
|
||||
date_from: string;
|
||||
date_to: string;
|
||||
users: {
|
||||
new: number;
|
||||
active: number;
|
||||
retained_new_users: number;
|
||||
retention_rate: number | null;
|
||||
retention_note: string;
|
||||
};
|
||||
comparison: {
|
||||
total: number;
|
||||
success: number;
|
||||
success_rate: number;
|
||||
ordered: number;
|
||||
average_duration_ms: number | null;
|
||||
average_saved_cents: number | null;
|
||||
};
|
||||
coins: {
|
||||
granted_total: number;
|
||||
reward_video_coin_total: number;
|
||||
feed_ad_coin_total: number;
|
||||
signin_coin_total: number;
|
||||
signin_boost_coin_total: number;
|
||||
task_coin_total: number;
|
||||
coupon_reward_coin_total: number;
|
||||
comparison_reward_coin_total: number;
|
||||
regular_task_coin_total: number;
|
||||
};
|
||||
cash: {
|
||||
withdraw_success_cents: number;
|
||||
};
|
||||
trend: Array<{
|
||||
date: string;
|
||||
active_users: number;
|
||||
new_users: number;
|
||||
comparisons: number;
|
||||
}>;
|
||||
};
|
||||
feedback: { new: number };
|
||||
cps: { available: boolean; note: string };
|
||||
cps: {
|
||||
available: boolean;
|
||||
note: string;
|
||||
meituan_order_count?: number;
|
||||
meituan_commission_cents?: number;
|
||||
meituan_hit_count?: number;
|
||||
meituan_miss_count?: number;
|
||||
meituan_unknown_rate_count?: number;
|
||||
meituan_hit_rate?: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PriceReport {
|
||||
|
||||
Reference in New Issue
Block a user