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:
@@ -53,8 +53,9 @@ class Visitor(models.Model):
|
||||
|
||||
|
||||
class VisitSession(models.Model):
|
||||
site = models.ForeignKey(Site, on_delete=models.CASCADE, related_name="sessions")
|
||||
visitor = models.ForeignKey(Visitor, on_delete=models.CASCADE, related_name="sessions")
|
||||
# db_index=False:单列 FK 索引冗余——site 由 (site, started_at)、visitor 由 (visitor, -started_at) 复合索引前缀覆盖
|
||||
site = models.ForeignKey(Site, on_delete=models.CASCADE, related_name="sessions", db_index=False)
|
||||
visitor = models.ForeignKey(Visitor, on_delete=models.CASCADE, related_name="sessions", db_index=False)
|
||||
session_id = models.CharField(max_length=64)
|
||||
started_at = models.DateTimeField()
|
||||
ended_at = models.DateTimeField()
|
||||
@@ -68,13 +69,19 @@ class VisitSession(models.Model):
|
||||
class Meta:
|
||||
db_table = "visit_session"
|
||||
constraints = [models.UniqueConstraint(fields=["site", "session_id"], name="uq_site_session")]
|
||||
indexes = [models.Index(fields=["visitor", "-started_at"])]
|
||||
indexes = [
|
||||
models.Index(fields=["visitor", "-started_at"]),
|
||||
models.Index(fields=["site", "started_at"]), # 按日聚合/时间线查询
|
||||
]
|
||||
|
||||
|
||||
class Event(models.Model):
|
||||
site = models.ForeignKey(Site, on_delete=models.CASCADE, related_name="events")
|
||||
# site/session 的单列 FK 索引冗余:site 由 (site, visitor/-occurred_at)、(site, event_name, -occurred_at)、
|
||||
# (site, occurred_at) 前缀覆盖,session 由 (session, occurred_at) 前缀覆盖,故 db_index=False。
|
||||
# visitor 保留默认索引:无其他以 visitor 打头的索引,级联删除(visitor 反向删 events)需要它。
|
||||
site = models.ForeignKey(Site, on_delete=models.CASCADE, related_name="events", db_index=False)
|
||||
visitor = models.ForeignKey(Visitor, on_delete=models.CASCADE, related_name="events")
|
||||
session = models.ForeignKey(VisitSession, on_delete=models.CASCADE, related_name="events", null=True)
|
||||
session = models.ForeignKey(VisitSession, on_delete=models.CASCADE, related_name="events", null=True, db_index=False)
|
||||
event_id = models.CharField(max_length=64)
|
||||
event_name = models.CharField(max_length=50)
|
||||
occurred_at = models.DateTimeField()
|
||||
@@ -96,11 +103,12 @@ class Event(models.Model):
|
||||
models.Index(fields=["site", "visitor", "-occurred_at"]),
|
||||
models.Index(fields=["session", "occurred_at"]),
|
||||
models.Index(fields=["site", "event_name", "-occurred_at"]),
|
||||
models.Index(fields=["site", "occurred_at"]), # refresh_daily 按日聚合扫描
|
||||
]
|
||||
|
||||
|
||||
class DailyStat(models.Model):
|
||||
site = models.ForeignKey(Site, on_delete=models.CASCADE, related_name="daily_stats")
|
||||
site = models.ForeignKey(Site, on_delete=models.CASCADE, related_name="daily_stats", db_index=False) # uq_site_date 前缀覆盖
|
||||
date = models.DateField()
|
||||
pv = models.PositiveIntegerField(default=0)
|
||||
uv = models.PositiveIntegerField(default=0)
|
||||
|
||||
Reference in New Issue
Block a user