feat: 日聚合服务与命令 + 索引迁移打包(Task3 审查结论)

- refresh_daily:按上海自然日聚合 pv/uv/sessions/new_visitors,update_or_create 幂等
- refresh_stats 管理命令:回刷近 N 天(生产由 1Panel 每日 00:10 调度,见 Task 22)
- 索引打包(0002):Event/VisitSession 增加 (site, 时间) 日聚合索引;
  删 5 个冗余单列 FK 索引(Event.site/session、VisitSession.site/visitor、DailyStat.site),
  保留 Event.visitor/Visitor.site;sqlite PRAGMA 实证级联索引完整、EXPLAIN 走新索引
- test_models:IntegrityError 断言收紧 + Site 级联删除测试

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-15 13:38:01 +08:00
parent f69822859e
commit cc1ea6805b
8 changed files with 153 additions and 7 deletions
+26
View File
@@ -0,0 +1,26 @@
from datetime import datetime, time, timedelta
from django.utils import timezone
from tracker.models import DailyStat, Event, VisitSession, Visitor
def refresh_daily(site, date) -> DailyStat:
"""重算某站点某自然日的聚合(幂等,update_or_create)。
日界取 TIME_ZONE(Asia/Shanghai)自然日:make_aware(datetime.combine(date, time.min))。
口径:pv/uv 均从事件表统计(排除 is_bot),uv=distinct visitor,与 pv 同源;
sessions 按会话 started_at 落入当日且访客非爬虫;new_visitors 按 Visitor.first_seen_at 落入当日。
"""
day_start = timezone.make_aware(datetime.combine(date, time.min))
day_end = day_start + timedelta(days=1)
pv = Event.objects.filter(site=site, is_bot=False,
occurred_at__gte=day_start, occurred_at__lt=day_end).count()
uv = Event.objects.filter(site=site, is_bot=False, occurred_at__gte=day_start,
occurred_at__lt=day_end).values("visitor").distinct().count()
sessions = VisitSession.objects.filter(site=site, visitor__is_bot=False,
started_at__gte=day_start,
started_at__lt=day_end).count()
new_v = Visitor.objects.filter(site=site, first_seen_at__gte=day_start,
first_seen_at__lt=day_end).count()
obj, _ = DailyStat.objects.update_or_create(site=site, date=date,
defaults={"pv": pv, "uv": uv, "sessions": sessions, "new_visitors": new_v})
return obj