from datetime import datetime, time, timedelta from django.db.models import Count, Sum from django.utils import timezone from rest_framework.views import APIView from rest_framework.response import Response from tracker.api.envelope import EnvelopeMixin, ok from tracker.models import DailyStat, Event, Site, VisitSession, Visitor from tracker.services.stats import daily_counts METRICS = ("pv", "uv", "sessions", "new_visitors") def _day_bounds(date): """上海自然日窗口,与 daily_counts 同款 make_aware。""" start = timezone.make_aware(datetime.combine(date, time.min)) return start, start + timedelta(days=1) class OverviewView(EnvelopeMixin, APIView): def get(self, request): site = None sid = request.query_params.get("site") if sid: site = Site.objects.filter(site_id=sid).first() if not site: return Response({"code": 404, "data": None, "message": "site_not_found"}, status=404) today = timezone.localdate() counts = daily_counts(site, today) # 与 refresh_daily 共享口径(Task8 审查结论) counts["active_now"] = self._active_now(site) return ok({"today": counts, "trend": self._trend(site, today, counts), "sources": self._today_sources(site, today), "top_pages": self._today_pages(site, today)}) @staticmethod def _active_now(site): # 5 分钟内有行为的访客(last_seen_at 有 site,-last_seen_at 索引;不用无索引的 received_at) q = Visitor.objects.filter(is_bot=False, last_seen_at__gt=timezone.now() - timedelta(minutes=5)) if site is not None: q = q.filter(site=site) return q.count() def _trend(self, site, today, today_counts): """DailyStat 近 30 天(缺日补零;今天一位用 daily_counts 实时值覆盖——聚合作业未跑也不缺当日)。""" days = 30 start = today - timedelta(days=days - 1) qs = DailyStat.objects.filter(date__gte=start, date__lte=today) if site is not None: qs = qs.filter(site=site) rows = {r["date"]: r for r in qs.values("date").annotate( pv=Sum("pv"), uv=Sum("uv"), sessions=Sum("sessions"), new_visitors=Sum("new_visitors"))} out = [] for i in range(days): date = start + timedelta(days=i) if date == today: out.append({"date": date.isoformat(), **{k: today_counts[k] for k in METRICS}}) else: r = rows.get(date, {}) out.append({"date": date.isoformat(), **{k: r.get(k, 0) for k in METRICS}}) return out def _today_sources(self, site, today): start, end = _day_bounds(today) q = VisitSession.objects.filter(visitor__is_bot=False, started_at__gte=start, started_at__lt=end) if site is not None: q = q.filter(site=site) return [{"source": r["referrer_domain"] or "直接访问", "sessions": r["n"]} for r in q.values("referrer_domain") .annotate(n=Count("id")) .order_by("-n", "referrer_domain")[:8]] # 平局时次序跨请求稳定 def _today_pages(self, site, today): start, end = _day_bounds(today) q = Event.objects.filter(is_bot=False, event_name="page_view", occurred_at__gte=start, occurred_at__lt=end) if site is not None: q = q.filter(site=site) return [{"path": r["path"], "pv": r["n"]} for r in q.values("path") .annotate(n=Count("id")) .order_by("-n", "path")[:8]] # 平局时次序跨请求稳定