增加微信用户每日领券明细 --------- Co-authored-by: guke <guke@autohome.com.cn> Reviewed-on: #19
9.4 KiB
CPS 群详情 · 每日明细「按天 · 按用户」领券下钻
日期:2026-06-26 状态:已与用户确认设计,待写实现计划
背景
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"(北京),下钻要完整日期 → 本次改为"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。
既有口径(沿用,不改):领券次数 = copy 事件数;点击次数 = visit 事件数。
目标
每日明细表每行末尾加「明细」按钮 → 弹窗展示该群、该天、以用户为单位的领券情况:
- 列:用户名、领券次数、点击次数。
- 鼠标悬停「点击次数」→ tooltip 展示该用户当天 visit 过的全部优惠券,每行
券名 ×次数,按次数倒序,合计 = 点击次数。
方案:A(一个新接口,券明细内联返回)
下钻数据量小(单群 / 单天 / 有 openid 的用户)。新接口一次返回每个用户的领券数、点击数及其 visit 过的券列表,前端一次请求渲染表格 + tooltip,无悬停二次加载、无闪烁。
(已否决方案 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
- 入参:
date(必填,YYYY-MM-DD)。 - 鉴权:
Depends(get_current_admin)(只读,登录即可;与/daily、/wx-users同口径,不加require_role)。 - 群不存在 → 404;
date非法 → 400(detail说明格式)。 - 时间窗:
date按北京时区(UTC+8)解释成[当天 00:00, 次日 00:00)半开区间,转 UTC 后查clicked_at >= start AND clicked_at < next_day_start(半开避免午夜双计)。北京日界 → UTC 用既有_as_utc约定,与group_click_timeseries分桶口径一致。 - 取数:
cps_clickwheregroup_id == {group_id}AND 时间窗内 ANDopenid IS NOT NULL。 - 解析券名:取该群相关
cps_link(或按出现的link_id批量取)建link_id → activity_id映射,再activity_id → cps_activity.name映射;Python 侧聚合(与本仓既有风格一致)。活动被硬删 → 券名兜底活动#{activity_id}。 - 按 openid 聚合:
copy_count= 该用户copy事件数(= 领券次数)visit_count= 该用户visit事件数(= 点击次数)coupons= 该用户 visit 事件按活动分组的{name, count}列表,按count倒序;copy事件不计入券列表(与用户选择"只算 visit"一致)
- 关联
cps_wx_user(openid IN (...))取nickname/headimgurl(未授权为 null)。 - 排序:
copy_countdesc,再visit_countdesc(沿用group_wx_users习惯)。 - 可选
limit(默认 200,与group_wx_users一致)。
响应:
{
"group_id": 1,
"group_name": "宝妈薅羊毛1群",
"date": "2026-06-25",
"users": [
{
"openid": "oABC...",
"nickname": "张三",
"headimgurl": "https://thirdwx.qlogo.cn/...",
"copy_count": 2,
"visit_count": 5,
"coupons": [
{ "name": "618神券", "count": 3 },
{ "name": "买一送一", "count": 2 }
]
}
]
}
实现建议:在 cps_repo 加 group_day_users(db, *, group_id, date_start, date_end, limit=200) -> list[dict],router 负责日期解析与窗口计算后调用之(与 group_daily/group_wx_users 的分工一致)。
微调既有 /daily:date 改全日期(不新增字段)
把每行 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)
DailyRow.date现为"YYYY-MM-DD"(类型仍string,无需加字段)。日期列{ title:'日期', dataIndex:'date' }宽度 64 → 调宽到约 96,完整显示YYYY-MM-DD(跨月/跨年看范围更直观)。表格scroll.x随新列 + 加宽日期列相应上调(约 980)。- 新增类型:
interface DayCoupon { name: string; count: number; } interface DayUser { openid: string; nickname: string | null; headimgurl: string | null; copy_count: number; visit_count: number; coupons: DayCoupon[]; } dailyColumns末尾加**「明细」列**(key:'drill',fixed:'right',width≈64),render 一个type="link"小按钮,onClick→openDayDetail(row.date)(全日期既用于查询也用于弹窗标题)。- 弹窗状态:
{ open, date, loading, users }。openDayDetail(date)置 open+loading,api.get('/admin/api/cps/groups/${id}/day-users?date=' + date)→ 填users;失败message.error(errMsg(e, ...))。 - 新增
<Modal>(声明式组件,不受 antd5 静态方法 context 限制,无需App.useApp())含<Table>,三列:- 用户:头像 + 昵称(空 →
(未授权昵称)),与既有「群内微信用户」表的用户单元一致(头像直接<img src={headimgurl}>,微信 URL 为绝对地址,非/media)。 - 领券次数:
copy_count,沿用既有红色加粗样式(0 显示 0)。 - 点击次数:
visit_count,整格用<Tooltip>包裹,title渲染coupons每行券名 ×次数;coupons为空时不挂 tooltip(直接显示数字)。
- 用户:头像 + 昵称(空 →
- Modal 标题:
群「{group_name}」{date} · 用户领券明细。空态文案:仅统计微信授权(openid)用户;美团/京东多为匿名跳转,可能为空。 - antd 引入新增
Modal, Tooltip。
默认与边界(已确认)
- 匿名点击(openid 为空)不计入(无法归属到人),镜像既有
/wx-users。后果:美团/京东群弹窗常为空 —— 空态文案已提示。 - 数据口径差异(需在代码注释点明,避免误读为 bug):每日明细行的
click_pv/copy_pv计全部点击(含匿名、UV 按(ip,ua));本弹窗只计openid用户。故弹窗各用户求和 ≤ 当天行的点击/复制总数,二者不必相等。 - 谁出现在弹窗:当天有任意事件(visit 或 copy)的授权用户都列(含领券数为 0 的纯点击用户)。
- 领券次数不做 tooltip:需求只要点击次数的 tooltip,保持最小。
- 权限:不加角色门槛(与本页其它只读统计一致)。
不做(YAGNI)
- 领券次数的「领了哪些券」tooltip。
- 匿名点击的聚合行 / 占位行。
- 用户级下单归因(需 user-level sid,另一期)。
验证(本仓无测试框架,手动验证)
- 后端:选一个有
cps_click数据的淘宝群,curl .../day-users?date=<某天>,核对:①只含 openid 非空用户;②某用户coupons的count之和 = 其visit_count;③date非法返回 400、群不存在返回 404;④跨年日期(如去年 12-31)能取到正确那天。 - 前端:
npm run build通过;日期列完整显示YYYY-MM-DD、列宽不挤;明细按钮 → 弹窗加载;悬停点击次数显示券×次数且合计对得上;美团/京东群弹窗走空态文案;昵称为空显示占位。
落点文件
- 后端:
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)。