diff --git a/docs/superpowers/specs/2026-06-26-cps-day-user-drilldown-design.md b/docs/superpowers/specs/2026-06-26-cps-day-user-drilldown-design.md index 997a465..e146cd9 100644 --- a/docs/superpowers/specs/2026-06-26-cps-day-user-drilldown-design.md +++ b/docs/superpowers/specs/2026-06-26-cps-day-user-drilldown-design.md @@ -5,11 +5,11 @@ ## 背景 -`shaguabijia-admin-web`(纯前端)+ 姎仓后端 `shaguabijia-app-server`(Admin API,`/admin/api/*`,端口 8771)。 +`shaguabijia-admin-web`(纯前端)+ 后端 `shaguabijia-app-server`(Admin API,`/admin/api/*`,端口 8771)。 CPS 群对账详情页 `src/app/(main)/cps/groups/[id]/page.tsx` 现有两张表: -- **每日明细**(`GET /admin/api/cps/groups/{id}/daily`):按天一行,含点击/复制/订单佣金。行 `date` 仅 `"MM-DD"`(北京),**不含年份**。 +- **每日明细**(`GET /admin/api/cps/groups/{id}/daily`):按天一行,含点击/复制/订单佣金。行 `date` 原为 `"MM-DD"`(北京),下钻要完整日期 → **本次改为 `"YYYY-MM-DD"`**(见后端改动)。 - **群内微信用户**(`GET /admin/api/cps/groups/{id}/wx-users`):**累计**(非按天)每用户画像,列含 用户 / 领券次数(`copy_count`) / 点击次数(`visit_count`)。 底层数据 `cps_click`(一事件一行):`event_type` = `visit`(进落地页/被跳转) | `copy`(淘宝"复制口令")、`openid`(微信授权才有)、`clicked_at`、`link_id`。一条点击对应的「优惠券」= `link_id → cps_link.activity_id → cps_activity.name`(活动即优惠券)。`cps_wx_user` 按 `openid` 存 `nickname/headimgurl`。 @@ -29,6 +29,14 @@ CPS 群对账详情页 `src/app/(main)/cps/groups/[id]/page.tsx` 现有两张表 (已否决方案 B:用户列表与券明细拆两接口、悬停再拉 —— 多一接口、多一往返、有闪烁,对这数据量不值得。) +## 数据库:无需改动 + +纯读侧聚合,所需数据已全部在库,**不加表 / 不加字段 / 无 migration / 无需回填**: + +- `cps_click`:`group_id`(idx) / `clicked_at`(idx) / `openid`(idx) / `event_type` / `link_id` —— 筛群、筛天、分用户、分领券/点击、定位券所需字段齐全。 +- 券归属 `link_id → cps_link.activity_id → cps_activity.name` 在**写入时**就落了(`record_click` 一直写 `link_id`),历史点击可反查,**无需回填**。 +- 查询条件 `group_id + clicked_at 区间 + openid IS NOT NULL` 均命中现有单列索引;数据量小、Python 侧聚合,**无需新增复合索引**。 + ## 后端改动(`app/admin/routers/cps.py` + `app/admin/repositories/cps.py`) ### 新增接口 `GET /admin/api/cps/groups/{group_id}/day-users` @@ -72,13 +80,16 @@ CPS 群对账详情页 `src/app/(main)/cps/groups/[id]/page.tsx` 现有两张表 实现建议:在 `cps_repo` 加 `group_day_users(db, *, group_id, date_start, date_end, limit=200) -> list[dict]`,router 负责日期解析与窗口计算后调用之(与 `group_daily`/`group_wx_users` 的分工一致)。 -### 微调既有 `/daily` +### 微调既有 `/daily`:`date` 改全日期(不新增字段) -每行除现有 `"date": "MM-DD"` 外,**新增 `"date_iso": "YYYY-MM-DD"`**(北京)。下钻必须知道完整日期(`MM-DD` 丢年份,跨年会错)。`group_daily` 已在循环里持有北京日期 `cur`,顺手 `cur.strftime("%Y-%m-%d")` 即可。**纯新增字段、向后兼容**。 +把每行 `date` 从 `"MM-DD"` 改成 **`"YYYY-MM-DD"`**(北京),**不再新增 `date_iso`**。下钻必须知道完整日期(`MM-DD` 丢年份,跨年会错);`date` 直接给全日期,契约更简单、无冗余字段。`group_daily` 已在循环里持有北京日期 `cur`,把 `cur.strftime("%m-%d")` 改成 `cur.strftime("%Y-%m-%d")` 即可。 + +- 该 endpoint 唯一消费方是本前端(随本次一起改),故**非破坏性**。 +- 折线图/时序用的是 `/timeseries` 的 `label` 字段(另一套,仍 `MM-DD`),**不受影响**,本次不动。 ## 前端改动(`src/app/(main)/cps/groups/[id]/page.tsx`) -1. `DailyRow` 接口加 `date_iso: string`。 +1. `DailyRow.date` 现为 `"YYYY-MM-DD"`(类型仍 `string`,无需加字段)。日期列 `{ title:'日期', dataIndex:'date' }` 宽度 64 → **调宽到约 96,完整显示 `YYYY-MM-DD`**(跨月/跨年看范围更直观)。表格 `scroll.x` 随新列 + 加宽日期列相应上调(约 980)。 2. 新增类型: ```ts interface DayCoupon { name: string; count: number; } @@ -87,13 +98,13 @@ CPS 群对账详情页 `src/app/(main)/cps/groups/[id]/page.tsx` 现有两张表 copy_count: number; visit_count: number; coupons: DayCoupon[]; } ``` -3. `dailyColumns` 末尾加**「明细」列**(`key:'drill'`, `fixed:'right'`, `width≈64`),render 一个 `type="link"` 小按钮,`onClick` → `openDayDetail(row.date_iso, row.date)`。 -4. 弹窗状态:`{ open, date_iso, label, loading, users }`。`openDayDetail` 置 open+loading,`api.get('/admin/api/cps/groups/${id}/day-users?date=' + date_iso)` → 填 `users`;失败 `message.error(errMsg(e, ...))`。 +3. `dailyColumns` 末尾加**「明细」列**(`key:'drill'`, `fixed:'right'`, `width≈64`),render 一个 `type="link"` 小按钮,`onClick` → `openDayDetail(row.date)`(全日期既用于查询也用于弹窗标题)。 +4. 弹窗状态:`{ open, date, loading, users }`。`openDayDetail(date)` 置 open+loading,`api.get('/admin/api/cps/groups/${id}/day-users?date=' + date)` → 填 `users`;失败 `message.error(errMsg(e, ...))`。 5. 新增 ``(声明式组件,不受 antd5 静态方法 context 限制,无需 `App.useApp()`)含 ``,三列: - **用户**:头像 + 昵称(空 → `(未授权昵称)`),与既有「群内微信用户」表的用户单元一致(头像直接 ``,微信 URL 为绝对地址,非 `/media`)。 - **领券次数**:`copy_count`,沿用既有红色加粗样式(0 显示 0)。 - **点击次数**:`visit_count`,整格用 `` 包裹,`title` 渲染 `coupons` 每行 `券名 ×次数`;`coupons` 为空时不挂 tooltip(直接显示数字)。 -6. Modal 标题:`群「{group_name}」{label} · 用户领券明细`。空态文案:`仅统计微信授权(openid)用户;美团/京东多为匿名跳转,可能为空`。 +6. Modal 标题:`群「{group_name}」{date} · 用户领券明细`。空态文案:`仅统计微信授权(openid)用户;美团/京东多为匿名跳转,可能为空`。 7. antd 引入新增 `Modal, Tooltip`。 ## 默认与边界(已确认) @@ -113,9 +124,9 @@ CPS 群对账详情页 `src/app/(main)/cps/groups/[id]/page.tsx` 现有两张表 ## 验证(本仓无测试框架,手动验证) - 后端:选一个有 `cps_click` 数据的淘宝群,`curl .../day-users?date=<某天>`,核对:①只含 openid 非空用户;②某用户 `coupons` 的 `count` 之和 = 其 `visit_count`;③`date` 非法返回 400、群不存在返回 404;④跨年日期(如去年 12-31)能取到正确那天。 -- 前端:`npm run build` 通过;明细按钮 → 弹窗加载;悬停点击次数显示券×次数且合计对得上;美团/京东群弹窗走空态文案;昵称为空显示占位。 +- 前端:`npm run build` 通过;日期列完整显示 `YYYY-MM-DD`、列宽不挤;明细按钮 → 弹窗加载;悬停点击次数显示券×次数且合计对得上;美团/京东群弹窗走空态文案;昵称为空显示占位。 ## 落点文件 -- 后端:`app/admin/routers/cps.py`(新 endpoint + `/daily` 加 `date_iso`)、`app/admin/repositories/cps.py`(新 `group_day_users` + `group_daily` 补 `date_iso`)。 -- 前端:`src/app/(main)/cps/groups/[id]/page.tsx`(类型、明细列、Modal、Tooltip)。 +- 后端:`app/admin/routers/cps.py`(新 endpoint + `/daily` 的 `date` 改 `YYYY-MM-DD`)、`app/admin/repositories/cps.py`(新 `group_day_users` + `group_daily` 的 `date` 改全日期)。 +- 前端:`src/app/(main)/cps/groups/[id]/page.tsx`(类型、日期列加宽、明细列、Modal、Tooltip)。