feat: 访客列表站点归属显示与筛选
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
"""访客列表/详情/跨天时间线/单会话接口(管理台)。
|
||||
|
||||
M1 单组织多站点:访客 API 不做 site 过滤参数(后台看全站访客流,站点维度在 Dashboard)。
|
||||
M1 单组织多站点:访客列表支持 ?site= 可选过滤(缺省聚合全部站点),行内随带站点归属
|
||||
(site_id/site_name)——多站点下分辨访客来自哪个站;访客数据本身已按站点隔离。
|
||||
访客列表查询按 (site,-last_seen_at) 索引逐站扫描或全表 order_by——M1 规模(万级访客)可接受,
|
||||
量级上来后按站点分片或引入 site 过滤即可(索引已就位)。
|
||||
量级上来后按站点分片即可(索引已就位)。
|
||||
"""
|
||||
from datetime import datetime, time, timedelta
|
||||
|
||||
@@ -12,7 +13,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from tracker.api.envelope import EnvelopeMixin, ok
|
||||
from tracker.models import Event, VisitSession, Visitor
|
||||
from tracker.models import Event, Site, VisitSession, Visitor
|
||||
|
||||
PAGE_SIZE = 20
|
||||
TIMELINE_DEFAULT_DAYS = 30
|
||||
@@ -36,6 +37,8 @@ def _row(visitor, last_session, online_cutoff):
|
||||
"""列表/详情共用行字段。last_source 依赖最近会话(由调用方批量取好传入,防 N+1)。"""
|
||||
return {
|
||||
"visitor_id": visitor.visitor_id,
|
||||
# 站点归属:visitor.site 需随行 select_related("site"),否则逐行多一条查询
|
||||
"site_id": visitor.site.site_id, "site_name": visitor.site.name,
|
||||
"country": visitor.country, "province": visitor.province, "city": visitor.city,
|
||||
"device_type": visitor.device_type, "os": visitor.os,
|
||||
"browser": visitor.browser, "language": visitor.language,
|
||||
@@ -106,7 +109,7 @@ def _page_visitors(qs, request):
|
||||
page = 1
|
||||
page = max(1, page)
|
||||
|
||||
page_qs = _annotate(qs).order_by("-last_seen_at", "-pk")
|
||||
page_qs = _annotate(qs).select_related("site").order_by("-last_seen_at", "-pk")
|
||||
visitors = list(page_qs[(page - 1) * PAGE_SIZE: page * PAGE_SIZE])
|
||||
latest = {s.pk: s for s in VisitSession.objects.filter(
|
||||
pk__in=[v.latest_session_pk for v in visitors if v.latest_session_pk])}
|
||||
@@ -163,13 +166,21 @@ def _merge_page_stay(events):
|
||||
|
||||
class VisitorListView(EnvelopeMixin, APIView):
|
||||
def get(self, request):
|
||||
rows, total, page = _page_visitors(Visitor.objects.all(), request)
|
||||
qs = Visitor.objects.all()
|
||||
sid = request.query_params.get("site") # 可选站点过滤,缺省聚合全部(与 dashboard 同款)
|
||||
if sid:
|
||||
site = Site.objects.filter(site_id=sid).first()
|
||||
if not site:
|
||||
return _not_found("site_not_found")
|
||||
qs = qs.filter(site=site)
|
||||
rows, total, page = _page_visitors(qs, request)
|
||||
return ok({"list": rows, "total": total, "page": page})
|
||||
|
||||
|
||||
class VisitorDetailView(EnvelopeMixin, APIView):
|
||||
def get(self, request, visitor_id):
|
||||
visitor = _annotate(Visitor.objects.filter(visitor_id=visitor_id)) \
|
||||
visitor = _annotate(Visitor.objects.filter(visitor_id=visitor_id)
|
||||
.select_related("site")) \
|
||||
.order_by("-last_seen_at", "-pk").first()
|
||||
if not visitor:
|
||||
return _not_found("visitor_not_found")
|
||||
|
||||
Reference in New Issue
Block a user