Compare commits

..

3 Commits

Author SHA1 Message Date
guke 4c22f774d8 Merge branch 'main' into codex/ad-count-percentile-fix 2026-07-28 13:50:28 +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 c4b3fcfedb 修复:统一领券与比价广告数百分位口径 2026-07-28 11:42:13 +08:00
2 changed files with 22 additions and 10 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>
)}
+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)。 */