fix: 访客API 微收口(duration_ms 移除/truncated 标记/锁定测试/source 收敛)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,11 @@ TIMELINE_MAX_EVENTS = 1000
|
||||
ONLINE_WINDOW = timedelta(minutes=5) # 与 dashboard active_now 同口径
|
||||
|
||||
|
||||
def _source(referrer_domain, utm):
|
||||
"""来源三级兜底:referrer_domain → utm.utm_source → 直接访问(访客档案/会话共用)。"""
|
||||
return referrer_domain or (utm or {}).get("utm_source") or "直接访问"
|
||||
|
||||
|
||||
def _row(visitor, last_session, online_cutoff):
|
||||
"""列表/详情共用行字段。last_source 依赖最近会话(由调用方批量取好传入,防 N+1)。"""
|
||||
return {
|
||||
@@ -33,25 +38,20 @@ def _row(visitor, last_session, online_cutoff):
|
||||
"visit_count": visitor.visit_count, "active_days": visitor.active_days,
|
||||
"session_count": visitor.session_count,
|
||||
"total_stay_ms": visitor.total_stay_ms or 0, # 无会话时 Sum 为 None → 0
|
||||
"first_source": visitor.first_referrer_domain
|
||||
or (visitor.first_utm or {}).get("utm_source") or "直接访问",
|
||||
"last_source": _session_source(last_session),
|
||||
"first_source": _source(visitor.first_referrer_domain, visitor.first_utm),
|
||||
"last_source": _source(last_session.referrer_domain, last_session.utm)
|
||||
if last_session else "直接访问", # 无会话访客(仅采集异常场景)
|
||||
"online": visitor.last_seen_at > online_cutoff, # Python 侧比对(last_seen_at 已随行取出)
|
||||
}
|
||||
|
||||
|
||||
def _session_source(session):
|
||||
if not session:
|
||||
return "直接访问" # 无会话访客(仅可能出现在采集异常场景)
|
||||
return session.referrer_domain or (session.utm or {}).get("utm_source") or "直接访问"
|
||||
|
||||
|
||||
def _session_summary(s):
|
||||
# duration_ms 仅内部字段不展示——前端时长一律用 stay_ms(有效停留口径)
|
||||
return {"session_id": s.session_id,
|
||||
"started_at": s.started_at.isoformat(), "ended_at": s.ended_at.isoformat(),
|
||||
"referrer_domain": s.referrer_domain, "utm": s.utm,
|
||||
"landing_path": s.landing_path, "event_count": s.event_count,
|
||||
"duration_ms": s.duration_ms, "stay_ms": s.stay_ms}
|
||||
"stay_ms": s.stay_ms}
|
||||
|
||||
|
||||
def _annotate(qs):
|
||||
@@ -199,6 +199,7 @@ class VisitorTimelineView(EnvelopeMixin, APIView):
|
||||
events = list(Event.objects.filter(visitor=visitor, occurred_at__gte=start)
|
||||
.select_related("session") # 会话信息随行带出,免逐事件查询
|
||||
.order_by("-occurred_at", "-pk")[:TIMELINE_MAX_EVENTS])
|
||||
truncated = len(events) >= TIMELINE_MAX_EVENTS # 切片取满即窗口内可能还有更多
|
||||
events.reverse()
|
||||
|
||||
groups = {} # date -> {session.pk/None: {"session":..., "events":[...]}},插入序即正序
|
||||
@@ -208,13 +209,14 @@ class VisitorTimelineView(EnvelopeMixin, APIView):
|
||||
bucket = day.setdefault(key, {"session": e.session, "events": []})
|
||||
bucket["events"].append(e)
|
||||
|
||||
return ok([{"date": d.isoformat(),
|
||||
"sessions": [{"session_id": b["session"].session_id if b["session"] else None,
|
||||
"started_at": b["session"].started_at.isoformat()
|
||||
if b["session"] else None,
|
||||
"events": _merge_page_stay(b["events"])}
|
||||
for b in groups[d].values()]}
|
||||
for d in sorted(groups, reverse=True)]) # 日期组倒序,日内会话/事件正序
|
||||
return ok({"days": [{"date": d.isoformat(),
|
||||
"sessions": [{"session_id": b["session"].session_id if b["session"] else None,
|
||||
"started_at": b["session"].started_at.isoformat()
|
||||
if b["session"] else None,
|
||||
"events": _merge_page_stay(b["events"])}
|
||||
for b in groups[d].values()]}
|
||||
for d in sorted(groups, reverse=True)], # 日期组倒序,日内会话/事件正序
|
||||
"truncated": truncated}) # 前端区分"没动静"和"被截断"
|
||||
|
||||
|
||||
class VisitorSessionView(EnvelopeMixin, APIView):
|
||||
|
||||
Reference in New Issue
Block a user