feat(admin): 数据总览按客迹视觉V1重做(周期卡/趋势双线/渠道条形/最近访问)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-17 12:18:37 +08:00
parent 132e2feb75
commit 8f20d8d9a2
8 changed files with 955 additions and 300 deletions
+9 -2
View File
@@ -37,7 +37,8 @@ def _iso(dt):
def _row(visitor, last_session, online_cutoff, ft=None):
"""列表/详情共用行字段。last_source 依赖最近会话(由调用方批量取好传入,防 N+1)。
channel 等归因五字段来自该访客 first_touch 分类(无分类行 → 空串,前端回落 first_source 展示)。"""
channel 等归因五字段来自该访客 first_touch 分类(无分类行 → 空串,前端回落 first_source 展示)。
last_path 是最近一条事件的 path(_annotate 的 Subquery 注解,同样无 N+1)。"""
return {
"visitor_id": visitor.visitor_id,
# 站点归属:visitor.site 需随行 select_related("site"),否则逐行多一条查询
@@ -55,6 +56,7 @@ def _row(visitor, last_session, online_cutoff, ft=None):
"last_source": _source(last_session.referrer_domain, last_session.utm)
if last_session else "直接访问", # 无会话访客(仅采集异常场景)
"first_landing_path": visitor.first_landing_path, # 列表首触来源 tooltip 用(详情本就输出)
"last_path": visitor.last_event_path or "", # 最近一条事件 path(总览"浏览页面"列)
"channel": ft.channel if ft else "",
"platform": ft.platform if ft else "",
"variant": ft.variant if ft else "",
@@ -87,18 +89,23 @@ def _session_summary(s):
def _annotate(qs):
"""会话数/有效停留/最近会话标记一次注解到位。
"""会话数/有效停留/最近会话/最近事件路径一次注解到位。
last_source 不逐行 sessions.order_by("-started_at").first()(页大小 20 即 N+1):
先用相关子查询把每个访客最近会话的 pk 标到行上(走 (visitor,-started_at) 索引),
分页后一次 IN 查询取回整页会话——全程只多 2 条 SQL。
last_path 同理用相关子查询取最近一条事件的 path(走 (site,visitor,-occurred_at) 索引),
-pk 平局次序跨请求稳定;无事件访客注解为 NULL,_row 收敛为空串。
"""
latest = (VisitSession.objects.filter(visitor=OuterRef("pk"))
.order_by("-started_at", "-pk")) # -pk 平局次序跨请求稳定
last_event = (Event.objects.filter(visitor=OuterRef("pk"))
.order_by("-occurred_at", "-pk"))
return qs.annotate(
session_count=Count("sessions"),
total_stay_ms=Sum("sessions__stay_ms"), # 有效停留口径(非 duration_ms)
latest_session_pk=Subquery(latest.values("pk")[:1]),
last_event_path=Subquery(last_event.values("path")[:1]),
)