Compare commits

..

4 Commits

Author SHA1 Message Date
linkeyu 549c9bdfad 修复:统一领券与比价广告数百分位口径 (#92)
## 改动说明

- 将单次领券、比价广告数从线性插值百分位改为 nearest-rank 离散百分位
- 百分位结果始终取自真实观测样本,不再出现 13.5 条这类小数广告数
- 指标展示改为整数

## 线上数据复算

2026-07-24 正式业务口径:

- 领券 P5/P50/P95:`1/7/13.5` 修正为 `1/7/15`
- 比价 P5/P50/P95:`1/2/8.3` 修正为 `1/2/9`

## 验证

- 线上数据完整分组复算通过
- 空样本、单样本边界校验通过
- `npm run build` 通过(含 TypeScript 类型检查)

---------

Co-authored-by: guke <guke@wonderable.ai>
Co-authored-by: linkeyu <798648091@qq.com>
Reviewed-on: #92
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
2026-07-28 13:50:40 +08:00
linkeyu 26b1adb769 优化:数据大盘支持选择今日 (#93)
## 改动说明

- 数据大盘自定义日期范围放开今日选择
- 仍禁止选择明日及未来日期
- 同步修正页面日期口径文案
- 快捷窗口仍按原口径默认截至昨日

## 口径核对

后端 `stats/overview` 及广告收益查询已支持北京时间今日自然日区间,本次无需修改后端。

## 验证

- 昨日:可选
- 今日:可选
- 明日/未来日期:禁用
- `npm run build` 通过(含 TypeScript 类型检查)

---------

Co-authored-by: linkeyu <798648091@qq.com>
Reviewed-on: #93
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
2026-07-28 13:50:14 +08:00
linkeyu 0040c5795d 优化提现审核页原因列展示 (#90)
## 改动内容
- 已到账页签隐藏无关的失败/拒绝原因列
- 打款中页签隐藏无关的失败/拒绝原因列
- 待审核、已拒绝、打款失败及全部页签继续保留原因列
- 详情抽屉中的失败/拒绝原因保持不变

## 验证
- npm run build 通过
- git diff --check 通过

---------

Co-authored-by: guke <guke@wonderable.ai>
Co-authored-by: linkeyu <798648091@qq.com>
Reviewed-on: #90
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
2026-07-28 10:04:21 +08:00
linkeyu 5acfb05c57 优化提现审核侧页基础信息展示 (#91)
## 改动内容
- 删除提现详情侧页的提现类型
- 删除提现单区域重复展示的累计成功提现
- 用户详情和邀请详情中的累计成功提现继续保留
- 两类累计金额继续按 coin_cash / invite_cash 账户分别统计

## 验证
- 前端 npm run build 通过
- 后端账户隔离相关测试 2 passed
- 线上数据库抽查确认两类提现金额独立
- git diff --check 通过

---------

Co-authored-by: linkeyu <798648091@qq.com>
Reviewed-on: #91
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
2026-07-28 10:04:04 +08:00
4 changed files with 30 additions and 19 deletions
+10 -10
View File
@@ -30,7 +30,7 @@ import {
} from '@ant-design/icons';
import dayjs, { type Dayjs } from 'dayjs';
import { api, errMsg } from '@/lib/api';
import { formatUtcTime, percentile } from '@/lib/format';
import { formatUtcTime, nearestRankPercentile } from '@/lib/format';
import type {
AdRevenueDaily,
AdRevenueHourly,
@@ -758,9 +758,9 @@ export default function AdRevenueReportPage() {
.filter((item) => item.feed_scene === sceneName)
.map((item) => item.sub_count ?? 1);
return {
p5: percentile(counts, 0.05, false),
p50: percentile(counts, 0.5, false),
p95: percentile(counts, 0.95, false),
p5: nearestRankPercentile(counts, 0.05),
p50: nearestRankPercentile(counts, 0.5),
p95: nearestRankPercentile(counts, 0.95),
};
};
return { coupon: summarize('coupon'), comparison: summarize('comparison') };
@@ -1083,12 +1083,12 @@ export default function AdRevenueReportPage() {
</Typography.Text>
</Divider>
<Row gutter={[16, 12]}>
<Col span={4}><Statistic title="单次领券广告数 P5" value={sessionAdCounts.coupon.p5 ?? '-'} precision={1} /></Col>
<Col span={4}><Statistic title="单次领券广告数 P50" value={sessionAdCounts.coupon.p50 ?? '-'} precision={1} /></Col>
<Col span={4}><Statistic title="单次领券广告数 P95" value={sessionAdCounts.coupon.p95 ?? '-'} precision={1} /></Col>
<Col span={4}><Statistic title="单次比价广告数 P5" value={sessionAdCounts.comparison.p5 ?? '-'} precision={1} /></Col>
<Col span={4}><Statistic title="单次比价广告数 P50" value={sessionAdCounts.comparison.p50 ?? '-'} precision={1} /></Col>
<Col span={4}><Statistic title="单次比价广告数 P95" value={sessionAdCounts.comparison.p95 ?? '-'} precision={1} /></Col>
<Col span={4}><Statistic title="单次领券广告数 P5" value={sessionAdCounts.coupon.p5 ?? '-'} precision={0} /></Col>
<Col span={4}><Statistic title="单次领券广告数 P50" value={sessionAdCounts.coupon.p50 ?? '-'} precision={0} /></Col>
<Col span={4}><Statistic title="单次领券广告数 P95" value={sessionAdCounts.coupon.p95 ?? '-'} precision={0} /></Col>
<Col span={4}><Statistic title="单次比价广告数 P5" value={sessionAdCounts.comparison.p5 ?? '-'} precision={0} /></Col>
<Col span={4}><Statistic title="单次比价广告数 P50" value={sessionAdCounts.comparison.p50 ?? '-'} precision={0} /></Col>
<Col span={4}><Statistic title="单次比价广告数 P95" value={sessionAdCounts.comparison.p95 ?? '-'} precision={0} /></Col>
</Row>
</Card>
)}
+2 -2
View File
@@ -710,7 +710,7 @@ export default function DashboardPage() {
<div>
<h1></h1>
<p>
{updatedAt ?? '--'} · · 00:00
{updatedAt ?? '--'} · · 00:00
</p>
</div>
<div className="controls">
@@ -734,7 +734,7 @@ export default function DashboardPage() {
format="YYYY-MM-DD"
allowClear={false}
open={datePickerOpen}
disabledDate={(date) => date.isAfter(dayjs().subtract(1, 'day'), 'day')}
disabledDate={(date) => date.isAfter(dayjs(), 'day')}
suffixIcon={<CalendarOutlined />}
onClick={() => setDatePickerOpen(true)}
onOpenChange={setDatePickerOpen}
@@ -570,6 +570,7 @@ export function WithdrawReviewPage({ mode = 'other' }: { mode?: WithdrawReviewMo
},
{
title: '失败/拒绝原因',
key: 'fail_reason',
dataIndex: 'fail_reason',
ellipsis: true,
render: (v: string | null) => v || <Text type="secondary">-</Text>,
@@ -647,6 +648,10 @@ export function WithdrawReviewPage({ mode = 'other' }: { mode?: WithdrawReviewMo
},
},
];
const visibleColumns =
activeStatus === 'pending' || activeStatus === 'success'
? columns.filter((column) => column.key !== 'fail_reason')
: columns;
const inviteeColumns: ColumnsType<InviteeDetail> = [
{ title: '手机号', dataIndex: 'phone', width: 130 },
@@ -945,7 +950,7 @@ export function WithdrawReviewPage({ mode = 'other' }: { mode?: WithdrawReviewMo
}
: undefined
}
columns={columns}
columns={visibleColumns}
dataSource={items}
loading={loading}
pagination={{
@@ -989,12 +994,6 @@ export function WithdrawReviewPage({ mode = 'other' }: { mode?: WithdrawReviewMo
<Descriptions.Item label="金额">
<Text strong>{yuan(detail.order.amount_cents)}</Text>
</Descriptions.Item>
<Descriptions.Item label="提现类型">
{detail.order.source === 'invite_cash' ? '邀请奖励提现' : '福利页提现'}
</Descriptions.Item>
<Descriptions.Item label="累计成功提现">
{yuan(detail.cumulative_success_cents)}
</Descriptions.Item>
<Descriptions.Item label="提交时间">
{dt(detail.order.created_at)}
</Descriptions.Item>
+12
View File
@@ -37,6 +37,18 @@ export function percentile(values: readonly number[], q: number, round = true):
return round ? Math.round(result) : result;
}
/**
* 离散计数分位数(nearest-rank)。
* 广告条数等不可拆分的计数不做线性插值,结果始终取自真实样本。
*/
export function nearestRankPercentile(values: readonly number[], q: number): number | null {
if (!values.length) return null;
const sorted = [...values].sort((a, b) => a - b);
const quantile = Math.min(1, Math.max(0, q));
const rank = Math.max(1, Math.ceil(quantile * sorted.length));
return sorted[rank - 1];
}
const hasTz = (v: string) => /(?:Z|[+-]\d{2}:?\d{2})$/i.test(v);
/** 把 UTC 口径字符串解析为带时区的 dayjs(无时区后缀的补 Z 当 UTC)。 */