Compare commits

..

5 Commits

Author SHA1 Message Date
linkeyu c4b3fcfedb 修复:统一领券与比价广告数百分位口径 2026-07-28 11:42:13 +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
linkeyu bea35fb707 在比价记录中展示是否下单 (#88)
## 改动说明
- 在“省”列后新增“是否下单”列
- 使用“已下单/未下单”标签展示
- 同步补充前端接口类型

## 验证
- Next.js 生产构建和类型检查通过

---------

Co-authored-by: linkeyu <798648091@qq.com>
Reviewed-on: #88
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
2026-07-27 17:33:23 +08:00
linkeyu 93c3daa784 优化比价记录设备信息展示 (#87)
## 改动说明
- 机型优先显示可读商品名,同时保留原始设备编码
- 列表和详情展示 ROM 厂商、名称与版本
- 同步补充前端接口类型

## 验证
- Next.js 生产构建通过
- 本地页面列表与详情抽屉已检查

---------

Co-authored-by: guke <guke@wonderable.ai>
Co-authored-by: linkeyu <798648091@qq.com>
Reviewed-on: #87
Co-authored-by: linkeyu <linkeyu@wonderable.ai>
Co-committed-by: linkeyu <linkeyu@wonderable.ai>
2026-07-27 16:38:35 +08:00
5 changed files with 37 additions and 17 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>
)}
@@ -333,6 +333,14 @@ export default function ComparisonRecordsPage() {
<b style={{ color: r.saved_amount_cents > 0 ? '#3f8600' : '#999' }}>{yuan(r.saved_amount_cents)}</b>
),
},
{
title: '是否下单',
dataIndex: 'ordered',
width: 84,
render: (ordered: boolean) => (
<Tag color={ordered ? 'green' : 'default'}>{ordered ? '已下单' : '未下单'}</Tag>
),
},
{ title: 'LLM', dataIndex: 'llm_call_count', width: 56, render: (v) => v ?? '-' },
{
title: 'TOKEN',
@@ -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)。 */
+1
View File
@@ -457,6 +457,7 @@ export interface ComparisonRecordListItem {
source_price_cents: number | null;
best_price_cents: number | null;
saved_amount_cents: number | null;
ordered: boolean;
total_ms: number | null;
step_count: number | null;
llm_call_count: number | null;