From a560ad1963f4480328709b372756e341336a9998 Mon Sep 17 00:00:00 2001 From: guke Date: Wed, 8 Jul 2026 15:27:44 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(coupon-data):=20=E9=A2=86=E5=88=B8?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9C=8B=E6=9D=BF=E5=8A=A0=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E7=8E=87=20+=20=E6=8C=89=E5=88=B8=E6=88=90=E5=8A=9F=E7=8E=87?= =?UTF-8?q?=E4=B8=8B=E9=92=BB=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 汇总卡:整单成功率、点位成功率(合计)+ 美团/淘宝/京东分平台(带口径 tooltip) - 按券成功率表默认隐藏,点平台统计卡下钻展开该平台各券 - 复用 fmtPct/平台映射;调 /admin/api/coupon-data/coupons Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/(main)/coupon-data/page.tsx | 164 +++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 6 deletions(-) diff --git a/src/app/(main)/coupon-data/page.tsx b/src/app/(main)/coupon-data/page.tsx index 01590dc..c58f01f 100644 --- a/src/app/(main)/coupon-data/page.tsx +++ b/src/app/(main)/coupon-data/page.tsx @@ -16,8 +16,10 @@ import { Statistic, Table, Tag, + Tooltip, Typography, } from 'antd'; +import { InfoCircleOutlined } from '@ant-design/icons'; import dayjs, { type Dayjs } from 'dayjs'; import { api, errMsg } from '@/lib/api'; import { formatUtcTime } from '@/lib/format'; @@ -34,6 +36,13 @@ interface CouponDataSummary { p50_ms: number | null; p95_ms: number | null; p99_ms: number | null; + // 平台粒度成功率(见 shaguabijia-app-server docs/guides/领券成功率指标-设计与埋点.md §3/§12) + full_success_count: number; + full_success_rate: number | null; + point_success_count: number; + point_total_count: number; + point_success_rate: number | null; + per_platform: Record; // {平台id: rate|null};恒含美团/淘宝/京东三档 } interface CouponDataDaily { date: string; @@ -74,6 +83,20 @@ interface CouponDataReport { total: number; items: CouponDataRow[]; } +// 按券成功率(§13):coupon_id 粒度,数据源 coupon_claim_record;成功率=成功/(成功+失败),skipped 排除。 +interface CouponSlotRow { + coupon_id: string; + coupon_name: string | null; + platform: string | null; + tried: number; + succeeded: number; + success_rate: number | null; +} +interface CouponSlotsOut { + date_from: string; + date_to: string; + items: CouponSlotRow[]; +} // 发起来源 App 包名 → 中文(「发起平台」列):空=傻瓜比价首页发起,外卖 App 包名→对应平台。 function originLabel(pkg: string | null): string { @@ -96,6 +119,24 @@ const STATUS_TAG: Record = { const fmtSec = (ms: number | null | undefined): string => ms == null ? '-' : `${(ms / 1000).toFixed(1)}s`; +// rate(0..1)→ "60.0%"(空值显示 -) +const fmtPct = (v: number | null | undefined): string => + v == null ? '-' : `${(v * 100).toFixed(1)}%`; + +// 汇总卡成功率口径 tooltip 文案 +const FULL_RATE_HINT = + '整单成功率:一次领券勾选的平台全部至少领到一张的场次 ÷ 领券发起数;基数含未完成/失败/中途退出。'; +const POINT_RATE_HINT = + '点位成功率(合计):平台粒度——各平台(美团/淘宝闪购/京东)成功点位数之和 ÷ 勾选平台点位数之和;空勾选按全领三档计。注意与「数据大盘」券粒度的点位成功率口径不同。'; + +// 按券表:coupon_id 平台 → 中文 +const SLOT_PLATFORM: Record = { + 'meituan-waimai': '美团', + 'taobao-shanguang': '淘宝', + 'jd-waimai': '京东', +}; +const slotPlatformLabel = (p: string | null): string => (p ? SLOT_PLATFORM[p] ?? p : '其他'); + // 点手机号弹出的「该用户全部领券」抽屉列(精简版:单用户,不含用户/手机号列)。 const RECORD_COLUMNS: ColumnsType = [ { title: '时间', dataIndex: 'started_at', width: 160, render: (v: string) => formatUtcTime(v) }, @@ -294,6 +335,9 @@ export default function CouponDataPage() { const [queriedMultiDay, setQueriedMultiDay] = useState(false); const [queriedLimit, setQueriedLimit] = useState(100); const [data, setData] = useState(null); + const [couponSlots, setCouponSlots] = useState([]); + // 按券表:默认隐藏,点某平台点位成功率卡 → 展开该平台的券表(null=隐藏)。 + const [selectedSlotPlatform, setSelectedSlotPlatform] = useState(null); const [loading, setLoading] = useState(false); // 点手机号:抽屉看该用户全部领券(总次数 + 记录) const [recordsUser, setRecordsUser] = useState<{ userId: number; phone: string | null } | null>(null); @@ -325,6 +369,13 @@ export default function CouponDataPage() { }, }); setData(res.data); + // 并行拉「按券成功率」(独立端点,失败不影响主报表) + api + .get('/admin/api/coupon-data/coupons', { + params: { date_from: from, date_to: to, app_env: appEnv }, + }) + .then((r) => setCouponSlots(r.data.items)) + .catch(() => setCouponSlots([])); setPage(targetPage); setQueriedLimit(targetLimit); setQueriedGranularity(gran); @@ -393,6 +444,12 @@ export default function CouponDataPage() { width: 110, render: (pkg: string | null) => originLabel(pkg), }, + { + title: '时间', + dataIndex: 'started_at', + width: 165, + render: (v: string) => formatUtcTime(v), + }, { title: '耗时', dataIndex: 'elapsed_ms', @@ -430,12 +487,6 @@ export default function CouponDataPage() { return parts.length ? parts.join(' / ') : -; }, }, - { - title: '时间', - dataIndex: 'started_at', - width: 165, - render: (v: string) => formatUtcTime(v), - }, { title: '领券 trace', dataIndex: 'trace_id', @@ -454,6 +505,31 @@ export default function CouponDataPage() { }, ]; + const slotColumns: ColumnsType = [ + { + title: '券名', + dataIndex: 'coupon_name', + render: (v: string | null, r: CouponSlotRow) => v || r.coupon_id, + }, + { + title: '尝试', + dataIndex: 'tried', + width: 90, + align: 'right', + sorter: (a: CouponSlotRow, b: CouponSlotRow) => a.tried - b.tried, + }, + { title: '成功', dataIndex: 'succeeded', width: 90, align: 'right' }, + { + title: '成功率', + dataIndex: 'success_rate', + width: 100, + align: 'right', + defaultSortOrder: 'descend', + render: (v: number | null) => fmtPct(v), + sorter: (a: CouponSlotRow, b: CouponSlotRow) => (a.success_rate ?? 0) - (b.success_rate ?? 0), + }, + ]; + const summary = data?.summary; const items = data?.items ?? []; @@ -580,6 +656,82 @@ export default function CouponDataPage() { + + + + + 整单成功率{' '} + + + + + } + value={fmtPct(summary.full_success_rate)} + /> + + + + 点位成功率(合计){' '} + + + + + } + value={fmtPct(summary.point_success_rate)} + /> + + + + + {( + [ + ['meituan-waimai', '美团点位成功率'], + ['taobao-shanguang', '淘宝点位成功率'], + ['jd-waimai', '京东点位成功率'], + ] as const + ).map(([pid, title]) => { + const active = selectedSlotPlatform === pid; + return ( + +
setSelectedSlotPlatform(active ? null : pid)} + title="点击查看该平台各券成功率" + style={{ + cursor: 'pointer', + padding: '4px 8px', + borderRadius: 6, + border: active ? '1px solid #1677ff' : '1px solid transparent', + background: active ? '#e6f4ff' : undefined, + transition: 'all .2s', + }} + > + +
+ + ); + })} +
+ + )} + + {selectedSlotPlatform && ( + setSelectedSlotPlatform(null)}>收起} + style={{ marginBottom: 16 }} + > + + rowKey="coupon_id" + size="small" + pagination={false} + dataSource={couponSlots.filter((r) => r.platform === selectedSlotPlatform)} + columns={slotColumns} + /> )} -- 2.52.0 From 25adbec48a7961dd5a5ac3395f605ce23b3a9bc8 Mon Sep 17 00:00:00 2001 From: guke Date: Wed, 8 Jul 2026 16:01:15 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(coupon-data):=20=E9=A2=86=E5=88=B8?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9C=8B=E6=9D=BF=E5=8A=A0=E9=A2=86=E5=88=B8?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=A4=9A=E9=80=89=E7=AD=9B=E9=80=89(?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=85=A8=E9=80=89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 筛选卡加状态多选(未完成/完成/失败/中途退出),默认全选=现状不改口径 - status 数组用 paramsSerializer indexes:null 序列化为 status=a&status=b(对齐 FastAPI list[str]) Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/(main)/coupon-data/page.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/app/(main)/coupon-data/page.tsx b/src/app/(main)/coupon-data/page.tsx index c58f01f..659b512 100644 --- a/src/app/(main)/coupon-data/page.tsx +++ b/src/app/(main)/coupon-data/page.tsx @@ -327,6 +327,8 @@ export default function CouponDataPage() { const [range, setRange] = useState<[Dayjs, Dayjs]>([dayjs().subtract(6, 'day'), dayjs()]); const [user, setUser] = useState(''); const [appEnv, setAppEnv] = useState('prod'); + // 领券状态多选(方案 A:整视图联动),默认全选四态(= 现状,不改口径)。 + const [statuses, setStatuses] = useState(['started', 'completed', 'failed', 'abandoned']); const [granularity, setGranularity] = useState<'day' | 'hour'>('day'); const [sortBy, setSortBy] = useState<'time' | 'elapsed'>('time'); const [limit, setLimit] = useState(100); @@ -362,11 +364,14 @@ export default function CouponDataPage() { date_to: to, user: user.trim() || undefined, app_env: appEnv, + status: statuses, granularity: gran, limit: targetLimit, offset: (targetPage - 1) * targetLimit, sort: targetSort, }, + // status 是数组:repeat 无方括号序列化(status=a&status=b),对齐 FastAPI list[str] Query + paramsSerializer: { indexes: null }, }); setData(res.data); // 并行拉「按券成功率」(独立端点,失败不影响主报表) @@ -386,7 +391,7 @@ export default function CouponDataPage() { setLoading(false); } }, - [range, user, appEnv, granularity, limit, sortBy, message], + [range, user, appEnv, statuses, granularity, limit, sortBy, message], ); useEffect(() => { @@ -581,6 +586,18 @@ export default function CouponDataPage() { ]} /> + + 状态 +