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
+23 -2
View File
@@ -78,8 +78,8 @@ def test_list_fields_and_default_bot_excluded(client, member_user):
"device_type", "os", "browser", "language", "first_seen_at",
"last_seen_at", "visit_count", "active_days", "session_count",
"total_stay_ms", "first_source", "last_source", "first_landing_path",
"online", "channel", "platform", "variant", "search_term", "resolution",
"device_model"}
"last_path", "online", "channel", "platform", "variant", "search_term",
"resolution", "device_model"}
assert row["country"] == "中国" and row["os"] == "Windows" and row["browser"] == "Chrome"
assert row["session_count"] == 2
assert row["total_stay_ms"] == 150000 # 120000+30000,有效停留口径
@@ -146,6 +146,26 @@ def test_list_site_fields_and_site_filter(client, member_user):
assert d["total"] == 1 and [x["visitor_id"] for x in d["list"]] == ["vb"] # 只剩本站访客
def test_list_last_path(client, member_user):
"""列表行 last_path:该访客最近一条事件的 path(Subquery 注解,无 N+1);
无事件访客为空串。"""
_auth(client, member_user)
site = Site.objects.create(name="s", domains=["a.com"])
v1 = _visitor(site, "v1", last_seen=timezone.now())
v2 = _visitor(site, "v2", last_seen=timezone.now() - timedelta(hours=1))
s1 = _session(site, v1, "s1", at=NOW - timedelta(hours=2))
_event(site, v1, s1, "e1", "page_view", NOW - timedelta(hours=2), path="/old")
_event(site, v1, s1, "e2", "page_view", NOW - timedelta(minutes=90), path="/stay")
_event(site, v1, s1, "e3", "page_stay", NOW - timedelta(minutes=30), path="/stay",
active_ms=1000) # occurred_at 早于 e2 → 不改判"最近"
_event(site, v1, s1, "e4", "page_view", NOW, path="/newest")
d = client.get("/api/admin/visitors").json()["data"]
by_id = {x["visitor_id"]: x for x in d["list"]}
assert by_id["v1"]["last_path"] == "/newest" # 最近一条事件的 path
assert by_id["v2"]["last_path"] == "" # 无事件 → 空串(前端显示 —)
def test_list_site_not_found(client, member_user):
_auth(client, member_user)
site = Site.objects.create(name="s", domains=["a.com"])
@@ -198,6 +218,7 @@ def test_detail_shape(client, member_user):
assert d["session_count"] == 2 and d["total_stay_ms"] == 90000
assert d["first_source"] == "google.com"
assert d["last_source"] == "wechat" # 最近会话 s-new:referrer 空 → utm 兜底
assert d["last_path"] == "/b" # 最近一条事件 e3 的 path
assert [s["session_id"] for s in d["sessions"]] == ["s-new", "s-old"] # 最近在前
srow = d["sessions"][0]
assert set(srow) == {"session_id", "started_at", "ended_at", "referrer_domain", "utm",