fix: 总览筛选竞态守卫 + 时间戳统一上海时间序列化

- dashboard loadOverview 加请求令牌:快速切换站点时丢弃过期响应,
  防旧数据覆盖与旧 finally 提前清 loading
- 后端序列化统一 timezone.localtime:sites.created_at 与 visitors 的
  first/last_seen_at、会话 started/ended_at、时间线事件 time——
  USE_TZ 下 isoformat() 是 UTC,与上海自然日口径同屏矛盾(差 8 小时)
- 站点弹窗域名加轻量主机名正则校验与实时提示(拦含空格等垃圾输入)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-15 18:57:41 +08:00
parent 3eb703b4ac
commit 5b3297337e
4 changed files with 38 additions and 12 deletions
+12 -6
View File
@@ -26,6 +26,12 @@ def _source(referrer_domain, utm):
return referrer_domain or (utm or {}).get("utm_source") or "直接访问"
def _iso(dt):
"""输出给前端的时间统一按当前时区(上海)序列化:USE_TZ=True 下 isoformat() 是 UTC
("+00:00"),前端切串直显会与 Dashboard 的上海自然日口径同屏矛盾(差 8 小时)。"""
return timezone.localtime(dt).isoformat() if dt else None
def _row(visitor, last_session, online_cutoff):
"""列表/详情共用行字段。last_source 依赖最近会话(由调用方批量取好传入,防 N+1)。"""
return {
@@ -33,8 +39,8 @@ def _row(visitor, last_session, online_cutoff):
"country": visitor.country, "province": visitor.province, "city": visitor.city,
"device_type": visitor.device_type, "os": visitor.os,
"browser": visitor.browser, "language": visitor.language,
"first_seen_at": visitor.first_seen_at.isoformat(),
"last_seen_at": visitor.last_seen_at.isoformat(),
"first_seen_at": _iso(visitor.first_seen_at),
"last_seen_at": _iso(visitor.last_seen_at),
"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
@@ -48,7 +54,7 @@ def _row(visitor, last_session, online_cutoff):
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(),
"started_at": _iso(s.started_at), "ended_at": _iso(s.ended_at),
"referrer_domain": s.referrer_domain, "utm": s.utm,
"landing_path": s.landing_path, "event_count": s.event_count,
"stay_ms": s.stay_ms}
@@ -145,12 +151,12 @@ def _merge_page_stay(events):
it["active_ms"] += ms
break
else:
items.append({"time": e.occurred_at.isoformat(), "event_name": e.event_name,
items.append({"time": _iso(e.occurred_at), "event_name": e.event_name,
"path": e.path, "title": e.title, "active_ms": ms})
else:
# page_view 初始 active_ms=0;其余(sdk_loaded/自定义事件)无停留概念 → null
active = 0 if e.event_name == "page_view" else None
items.append({"time": e.occurred_at.isoformat(), "event_name": e.event_name,
items.append({"time": _iso(e.occurred_at), "event_name": e.event_name,
"path": e.path, "title": e.title, "active_ms": active})
return items
@@ -211,7 +217,7 @@ class VisitorTimelineView(EnvelopeMixin, APIView):
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()
"started_at": _iso(b["session"].started_at)
if b["session"] else None,
"events": _merge_page_stay(b["events"])}
for b in groups[d].values()]}