feat(analytics): 新增「埋点日志」管理页 (#22)

Co-authored-by: zzhyyyyy <2685922758@qq.com>
Reviewed-on: #22
Co-authored-by: zhuzihao <zhuzihao@wonderable.ai>
Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
This commit was merged in pull request #22.
This commit is contained in:
2026-06-26 23:56:25 +08:00
committed by marco
parent d3efb3c487
commit 6ece274af6
3 changed files with 79 additions and 9 deletions
+72 -3
View File
@@ -47,6 +47,13 @@ const TYPE_TAG: Record<string, { color: string; label: string }> = {
withdrawal_video: { color: 'gold', label: '提现激励视频' }, withdrawal_video: { color: 'gold', label: '提现激励视频' },
}; };
// Draw 信息流投放场景标签(后端 AdRevenueRow.feed_scene:comparison/coupon/welfare)
const SCENE_TAG: Record<string, { text: string; color: string }> = {
comparison: { text: '比价', color: 'blue' },
coupon: { text: '领券', color: 'gold' },
welfare: { text: '福利', color: 'green' },
};
// 我们的应用环境标签 // 我们的应用环境标签
const APP_TAG: Record<string, { color: string; label: string }> = { const APP_TAG: Record<string, { color: string; label: string }> = {
prod: { color: 'green', label: '傻瓜比价(正式)' }, prod: { color: 'green', label: '傻瓜比价(正式)' },
@@ -256,6 +263,8 @@ export default function AdRevenueReportPage() {
const [range, setRange] = useState<[Dayjs, Dayjs]>([dayjs(), dayjs()]); const [range, setRange] = useState<[Dayjs, Dayjs]>([dayjs(), dayjs()]);
const [userId, setUserId] = useState<number | null>(null); const [userId, setUserId] = useState<number | null>(null);
const [adType, setAdType] = useState<string | undefined>(); const [adType, setAdType] = useState<string | undefined>();
// 「场景」为纯前端过滤(后端 query 暂不支持 feed_scene),只作用于下方明细表,不重新请求、不影响趋势/日汇总。
const [scene, setScene] = useState<string | undefined>();
const [granularity, setGranularity] = useState<'day' | 'hour'>('day'); const [granularity, setGranularity] = useState<'day' | 'hour'>('day');
const [limit, setLimit] = useState<number>(500); const [limit, setLimit] = useState<number>(500);
const [queriedGranularity, setQueriedGranularity] = useState<'day' | 'hour'>('day'); // 本次结果对应的粒度,决定是否显示「小时」列 const [queriedGranularity, setQueriedGranularity] = useState<'day' | 'hour'>('day'); // 本次结果对应的粒度,决定是否显示「小时」列
@@ -332,6 +341,16 @@ export default function AdRevenueReportPage() {
return <Tag color={t.color}>{t.label}</Tag>; return <Tag color={t.color}>{t.label}</Tag>;
}, },
}, },
{
title: '场景',
dataIndex: 'feed_scene',
width: 80,
render: (v: string | null | undefined) => {
if (!v) return <Typography.Text type="secondary">-</Typography.Text>;
const t = SCENE_TAG[v];
return t ? <Tag color={t.color}>{t.text}</Tag> : <Tag>{v}</Tag>;
},
},
{ {
title: '来源应用', title: '来源应用',
dataIndex: 'app_env', dataIndex: 'app_env',
@@ -428,6 +447,16 @@ export default function AdRevenueReportPage() {
} }
: null; : null;
// 「场景」纯前端过滤:只对已加载的明细 items 生效(后端 query 暂不支持 feed_scene);趋势图/日汇总仍用全量 daily/total_*。
const allItems = data?.items ?? [];
const filteredItems = scene ? allItems.filter((r) => r.feed_scene === scene) : allItems;
// 选中比价/领券且类型=draw 时,给出该场景在已加载明细内的实发金币小计(仅明细口径,可能受 limit 截断)。
const sceneCoinSubtotal =
adType === 'draw' && (scene === 'comparison' || scene === 'coupon')
? filteredItems.reduce((s, r) => s + (r.has_reward ? r.actual_coin : 0), 0)
: null;
return ( return (
<div> <div>
<Space align="center" style={{ display: 'flex', marginBottom: 16 }}> <Space align="center" style={{ display: 'flex', marginBottom: 16 }}>
@@ -497,6 +526,22 @@ export default function AdRevenueReportPage() {
]} ]}
/> />
</Space> </Space>
<Space size={6}>
<Typography.Text type="secondary"></Typography.Text>
<Select
placeholder="全部"
value={scene}
onChange={setScene}
allowClear
style={{ width: 130 }}
title="仅前端过滤下方明细,不影响趋势图/汇总"
options={[
{ value: 'comparison', label: '比价' },
{ value: 'coupon', label: '领券' },
{ value: 'welfare', label: '福利' },
]}
/>
</Space>
<Space size={6}> <Space size={6}>
<Typography.Text type="secondary"></Typography.Text> <Typography.Text type="secondary"></Typography.Text>
<Select <Select
@@ -599,6 +644,22 @@ export default function AdRevenueReportPage() {
message={`明细已截断到 ${queriedLimit} 条,还有更多未显示——请按用户/类型/日期缩小范围,或调大「条数」。合计数字不受影响。`} message={`明细已截断到 ${queriedLimit} 条,还有更多未显示——请按用户/类型/日期缩小范围,或调大「条数」。合计数字不受影响。`}
/> />
)} )}
{sceneCoinSubtotal != null && (
<Alert
type="info"
showIcon
message={
<span>
,
<Tag color={SCENE_TAG[scene!].color} style={{ marginInline: 4 }}>
{SCENE_TAG[scene!].text}
</Tag>
Draw <b>{sceneCoinSubtotal}</b>( {filteredItems.length} ;
,;/)
</span>
}
/>
)}
</Space> </Space>
)} )}
@@ -640,7 +701,7 @@ export default function AdRevenueReportPage() {
</Space> </Space>
} }
> >
{/* 按小时图由 items 聚合,会被 limit 截断;按天图用 daily(全量)不受影响,故只在按小时时警告 */} {/* 趋势图与日汇总恒为全量:按天图用 daily(全量),按小时图用全量 items;「场景」前端过滤只作用于下方明细表,不影响此图。 */}
{!queriedMultiDay && data.truncated && ( {!queriedMultiDay && data.truncated && (
<Alert <Alert
type="warning" type="warning"
@@ -649,6 +710,14 @@ export default function AdRevenueReportPage() {
message={`图表按小时聚合的是已截断到 ${queriedLimit} 组的明细,可能少算——调大「条数」或缩小筛选可还原真实趋势。`} message={`图表按小时聚合的是已截断到 ${queriedLimit} 组的明细,可能少算——调大「条数」或缩小筛选可还原真实趋势。`}
/> />
)} )}
{scene && (
<Alert
type="info"
showIcon
style={{ marginBottom: 12 }}
message="趋势图为全量数据,不随「场景」筛选变化;场景过滤仅作用于下方明细表与其金币小计。"
/>
)}
{queriedMultiDay ? ( {queriedMultiDay ? (
<TrendChart points={aggregateDaily(data.date_from, data.date_to, data.daily)} /> <TrendChart points={aggregateDaily(data.date_from, data.date_to, data.daily)} />
) : ( ) : (
@@ -660,11 +729,11 @@ export default function AdRevenueReportPage() {
<Table <Table
rowKey="event_key" rowKey="event_key"
columns={columns} columns={columns}
dataSource={data?.items ?? []} dataSource={filteredItems}
loading={loading} loading={loading}
pagination={false} pagination={false}
size="small" size="small"
scroll={{ x: 1200 }} scroll={{ x: 1280 }}
rowClassName={(r) => (r.has_reward && !r.matched ? 'row-mismatch' : '')} rowClassName={(r) => (r.has_reward && !r.matched ? 'row-mismatch' : '')}
expandable={{ expandable={{
// 只有发了奖的事件可展开,看「这条广告金币怎么算的」;纯展示行无复算、不可展开。 // 只有发了奖的事件可展开,看「这条广告金币怎么算的」;纯展示行无复算、不可展开。
+6 -6
View File
@@ -10,8 +10,8 @@ import { api, errMsg } from '@/lib/api';
interface AdCfg { interface AdCfg {
app_id: string; app_id: string;
reward_code_id: string; reward_code_id: string;
compare_feed_code_id: string; compare_draw_code_id: string;
coupon_feed_code_id: string; coupon_draw_code_id: string;
reward_mkey: string; reward_mkey: string;
reward_enabled: boolean; reward_enabled: boolean;
compare_ad_enabled: boolean; compare_ad_enabled: boolean;
@@ -74,11 +74,11 @@ export default function AdConfigPage() {
> >
<Input.Password placeholder="GroMore S2S 回调验签密钥" autoComplete="off" /> <Input.Password placeholder="GroMore S2S 回调验签密钥" autoComplete="off" />
</Form.Item> </Form.Item>
<Form.Item name="compare_feed_code_id" label="比价 · 信息流 广告位ID" rules={[{ required: true }]}> <Form.Item name="compare_draw_code_id" label="比价Draw信息流代码位" rules={[{ required: true }]}>
<Input placeholder="如 104090333" /> <Input placeholder="比价Draw信息流代码位,如 104098712" />
</Form.Item> </Form.Item>
<Form.Item name="coupon_feed_code_id" label="领券 · 信息流 广告位ID" rules={[{ required: true }]}> <Form.Item name="coupon_draw_code_id" label="领券Draw信息流代码位" rules={[{ required: true }]}>
<Input placeholder="如 104090333" /> <Input placeholder="领券Draw信息流代码位,如 104098712" />
</Form.Item> </Form.Item>
<Form.Item name="reward_enabled" label="福利激励视频开关" valuePropName="checked"> <Form.Item name="reward_enabled" label="福利激励视频开关" valuePropName="checked">
<Switch /> <Switch />
+1
View File
@@ -381,6 +381,7 @@ export interface AdRevenueRow {
user_id: number; user_id: number;
user_phone: string | null; // 用户手机号(admin 展示;查不到为空) user_phone: string | null; // 用户手机号(admin 展示;查不到为空)
ad_type: string; // reward_video / feed / draw ad_type: string; // reward_video / feed / draw
feed_scene?: string | null; // Draw 信息流投放场景 comparison(比价) / coupon(领券) / welfare(福利);非信息流或旧数据为 null
app_env: string | null; // prod(傻瓜比价) / test(测试);旧数据为空 app_env: string | null; // prod(傻瓜比价) / test(测试);旧数据为空
our_code_id: string | null; // 我们配置的代码位 104xxx;旧数据为空 our_code_id: string | null; // 我们配置的代码位 104xxx;旧数据为空
hour: number | null; // 北京时间小时 0–23(按小时粒度时有值;按天为 null) hour: number | null; // 北京时间小时 0–23(按小时粒度时有值;按天为 null)