From 7269e27e242b44ce75559ab6cc38007675c951a1 Mon Sep 17 00:00:00 2001 From: guke Date: Sat, 11 Jul 2026 18:48:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(coupon-data):=20=E7=82=B9=E4=BD=8D?= =?UTF-8?q?=E6=88=90=E5=8A=9F=E7=8E=87=E6=94=B9=E4=B8=BA=E6=8C=89=E5=88=B8?= =?UTF-8?q?=E6=88=90=E5=8A=9F=E7=8E=87=E7=9A=84=E7=AE=97=E6=9C=AF=E5=B9=B3?= =?UTF-8?q?=E5=9D=87(=E5=89=8D=E7=AB=AF=E4=BB=8E=E6=8C=89=E5=88=B8?= =?UTF-8?q?=E6=98=8E=E7=BB=86=E4=B8=8A=E5=8D=B7)=20(#46)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 点位成功率改为按券成功率的算术平均(前端从按券明细上卷) --------- Co-authored-by: guke Reviewed-on: https://gitea.shaguabijia.com/WonderableAI/shaguabijia-admin-web/pulls/46 --- ...coupon-point-rate-as-coupon-mean-design.md | 101 ++++++++++++++++++ src/app/(main)/coupon-data/page.tsx | 17 ++- 2 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 docs/superpowers/specs/2026-07-11-coupon-point-rate-as-coupon-mean-design.md diff --git a/docs/superpowers/specs/2026-07-11-coupon-point-rate-as-coupon-mean-design.md b/docs/superpowers/specs/2026-07-11-coupon-point-rate-as-coupon-mean-design.md new file mode 100644 index 0000000..3448258 --- /dev/null +++ b/docs/superpowers/specs/2026-07-11-coupon-point-rate-as-coupon-mean-design.md @@ -0,0 +1,101 @@ +# 点位成功率改为「按券成功率的算术平均」设计 + +- **日期**:2026-07-11 +- **范围**:纯前端 `shaguabijia-admin-web`,**后端零改动、零 schema 改动、零迁移** +- **涉及文件**:`src/app/(main)/coupon-data/page.tsx`(单文件) + +## 目标 + +「领券数据」看板汇总卡里的 **分平台点位成功率**(美团/淘宝/京东)与 **点位成功率(合计)**,改为 +**该平台(/全部)几张券成功率的算术平均**,让平台数字与下方「按券明细」自洽,便于快速揪出坏券。 + +## 背景 + +现状:分平台 / 合计点位成功率来自后端 `_success_rates`(**会话口径**,数据源 `coupon_session.platform_success`), +与页面下方「按券成功率」(数据源 `coupon_claim_record`)口径不同,导致同一平台的平台数(如淘宝 71.4%) +与其券明细(100%/80%/80%)对不上,甚至出现「合计低于每个平台」的反直觉现象。 + +关键点:**本页已经把全量按券数据拉到前端**——`couponSlots`(来自 `/admin/api/coupon-data/coupons`, +后端 `coupon_slot_report` 不分页、返回全量),每行含 `platform` 与 `success_rate`。因此直接在前端 +对其求算术平均即可,**无需后端改动**。 + +## 口径(改动后) + +- **分平台点位成功率** = 该平台所有券 `success_rate` 的算术平均(每张券等权;`success_rate` 为空的券跳过;无券 → 显示 `-`)。 +- **点位成功率(合计)** = `couponSlots` 全部券 `success_rate` 的算术平均。 +- **每张券成功率**(按券明细,不变):成功(success + already_claimed) ÷ 尝试(+ failed),skipped 排除(后端 `coupon_slot_report`)。 + +例:淘宝三张券 100% / 80% / 80% → 淘宝点位 = (100+80+80)/3 = **86.7%**。 + +## 不改(其他保持不变) + +- **后端**:`coupon_slot_report` / `_success_rates` / `CouponDataSummary` schema 全不动。 + `summary.per_platform`、`summary.point_success_rate` 仍会返回(会话口径),只是前端这两处不再引用它们(留着不删,接口稳定)。 +- **整单成功率**:会话口径不变。 +- **按券明细表**:排序、列等不动。 +- **筛选联动**:整单 / 发起 / 完成 / 耗时仍随「用户 / 状态 / 日期 / 环境」全部筛选; + 点位随 `couponSlots` 走(= 日期 + 环境,不随「用户 / 状态」搜索变——与按券表现状一致)。 + +## 实现(单文件 `page.tsx`) + +### 1. 加一个求均值 helper(放 `fmtPct` 附近) + +```ts +// 一组按券行的 success_rate 算术平均;无有效券 → null(显示 -) +const slotRateMean = (rows: CouponSlotRow[]): number | null => { + const rates = rows + .map((r) => r.success_rate) + .filter((v): v is number => v != null); + return rates.length ? rates.reduce((a, b) => a + b, 0) / rates.length : null; +}; +``` + +### 2. 合计:改 value + +把 +```tsx +value={fmtPct(summary.point_success_rate)} +``` +改成 +```tsx +value={fmtPct(slotRateMean(couponSlots))} +``` + +### 3. 分平台卡:改 rate 来源 + +把每个平台卡里的 +```tsx +const rate = summary.per_platform?.[pid]; +``` +改成 +```tsx +const rate = slotRateMean(couponSlots.filter((r) => r.platform === pid)); +``` + +### 4. 更新口径 tooltip 文案 + +`POINT_RATE_HINT` 改成(去掉旧的会话口径描述): +```ts +const POINT_RATE_HINT = + '点位成功率(合计):当前区间内全部券成功率的算术平均(每张券等权);' + + '单券成功率=成功(含已领)÷尝试(不含跳过),源自领券每券记录。'; +``` +分平台卡标题(`${SLOT_PLATFORM[pid]}点位成功率`)如需补口径,注明「该平台各券成功率的算术平均」。 + +## 边角 / 注意 + +- **精度**:`success_rate` 后端已 round 到 4 位,前端再平均,展示只到 1 位小数(`fmtPct`),误差可忽略。 +- **依赖按券请求**:点位现在依赖 `couponSlots`(独立请求,失败时 `setCouponSlots([])`)。若该请求失败,点位卡显示 `-`(优雅降级)。此前它来自主 `summary` 请求。 +- **platform=null 的券**:coupon_id 前缀无法识别的券(实践中 pricebot 都是 `mt_`/`tb_`/`ele_`/`elm_`/`jd_`,基本不出现)会计入 **合计**、但不在任何分平台卡里。可接受。 + +## 验证 + +- **手动**:选一天,展开某平台「查看明细」,核对该平台卡数字 = 明细表各券成功率的算术平均;合计 = 全部券的算术平均。 +- `npx tsc --noEmit` 通过;`npm run lint` 无新增 error。 + +## 不做(YAGNI) + +- 最小尝试数阈值 / 低量券滤噪 / 坏券标红。 +- 按券表默认升序(坏券置顶)。 +- 「数据大盘」overview 的领券点位成功率(另一页、另一口径)。 +- 删除后端已不再被前端使用的 `per_platform` / `point_success_rate` 字段。 diff --git a/src/app/(main)/coupon-data/page.tsx b/src/app/(main)/coupon-data/page.tsx index ffb8c8f..f49aaac 100644 --- a/src/app/(main)/coupon-data/page.tsx +++ b/src/app/(main)/coupon-data/page.tsx @@ -128,11 +128,21 @@ const fmtPct = (v: number | null | undefined): string => const fmtYuan = (v: number | null | undefined): string => v == null || v <= 0 ? '-' : `¥${v.toFixed(4)}`; +// 一组按券行的 success_rate 算术平均(每张券等权;空值券跳过);无有效券 → null(显示 -)。 +// 汇总卡「分平台/合计点位成功率」据此由按券明细上卷,口径与下方按券表一致。 +const slotRateMean = (rows: CouponSlotRow[]): number | null => { + const rates = rows + .map((r) => r.success_rate) + .filter((v): v is number => v != null); + return rates.length ? rates.reduce((a, b) => a + b, 0) / rates.length : null; +}; + // 汇总卡成功率口径 tooltip 文案 const FULL_RATE_HINT = '整单成功率:一次领券勾选的平台全部至少领到一张的场次 ÷ 领券发起数;基数含未完成/失败/中途退出。'; const POINT_RATE_HINT = - '点位成功率(合计):平台粒度——各平台(美团/淘宝闪购/京东)成功点位数之和 ÷ 勾选平台点位数之和;空勾选按全领三档计。注意与「数据大盘」券粒度的点位成功率口径不同。'; + '点位成功率(合计):当前区间内全部券成功率的算术平均(每张券等权);' + + '单券成功率=成功(含已领)÷尝试(不含跳过),源自领券每券记录。'; // 按券表:coupon_id 平台 → 中文 const SLOT_PLATFORM: Record = { @@ -717,7 +727,7 @@ export default function CouponDataPage() { } - value={fmtPct(summary.point_success_rate)} + value={fmtPct(slotRateMean(couponSlots))} /> @@ -725,7 +735,8 @@ export default function CouponDataPage() { {(['meituan-waimai', 'taobao-shanguang', 'jd-waimai'] as const).map((pid) => { const active = selectedSlotPlatform === pid; - const rate = summary.per_platform?.[pid]; + // 点位成功率 = 该平台各券成功率的算术平均(源自按券明细 couponSlots,与下方展开的按券表一致) + const rate = slotRateMean(couponSlots.filter((r) => r.platform === pid)); // 平台名(前置品牌色圆点) | 大数字成功率 | 「查看明细」按钮;仅按钮可展开表格。 return (