feat: 来源归因规则引擎(渠道/AI/搜索词/诚实状态/历史重算)
- constants 规则库 v1:AI/引擎/社交域名表、搜索词参数白名单、付费点击参数、 应用内 UA 标记、CHANNELS 枚举;page_view 白名单开 nav_type/prerendering - services/attribution.classify 纯函数:付费>AI>引擎>社交>referral 分层, UTM 证据叠加(combined/conflict/label_only),应用内 UA 叠加/兜底, 引擎 TLD 国家 variant,搜索词白名单提取(Google 恒空不虚构) - SourceClassification 派生表(迁移 0004):first_touch/session_entry 双 scope, rule_version 可追溯 - ingest 增量 best-effort:首访客/新会话时分类落库(savepoint+静默, 失败不影响采集);validation 对 page_view referrer 完整 URL 参数清洗+截500 - recompute_attribution 命令:全量/按站幂等重算 - API:访客列表归因五字段+channel 筛选、详情 attribution 块(含最近5会话入口)、 dashboard channels 聚合(sources 原口径保留);TS VisitorListParams 加 channel - 测试 97 → 124(引擎20 + 集成/接口7) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
from django.db import models
|
||||
import secrets
|
||||
|
||||
from tracker import constants as C
|
||||
|
||||
|
||||
def short_public_id() -> str:
|
||||
return "site_" + secrets.token_urlsafe(12)[:16]
|
||||
@@ -131,6 +133,35 @@ class RawBatch(models.Model):
|
||||
models.Index(fields=["site", "-received_at"])]
|
||||
|
||||
|
||||
class SourceClassification(models.Model):
|
||||
"""来源分类结果(attribution 规则引擎的派生存储)。
|
||||
|
||||
每访客两行 scope:first_touch(首访来源,session=None)/ session_entry(每次新会话入口)。
|
||||
写入两路:ingest 增量(新访客/新会话时 best-effort)+ recompute_attribution 全量重算
|
||||
(幂等 upsert)。resolution 表达证据可信度:resolved/combined/conflict/label_only/unknown——
|
||||
观察到的 referrer 与 UTM 自述矛盾时并列存 detail,不武断取一边。
|
||||
rule_version 关联 constants.ATTRIBUTION_RULE_VERSION:规则升级后可识别存量行需重算。
|
||||
"""
|
||||
site = models.ForeignKey(Site, on_delete=models.CASCADE, related_name="source_classifications")
|
||||
visitor = models.ForeignKey(Visitor, on_delete=models.CASCADE, related_name="source_classifications", null=True)
|
||||
session = models.ForeignKey(VisitSession, on_delete=models.CASCADE, related_name="source_classifications", null=True)
|
||||
scope = models.CharField(max_length=16) # first_touch / session_entry
|
||||
channel = models.CharField(max_length=32, blank=True, default="")
|
||||
platform = models.CharField(max_length=64, blank=True, default="")
|
||||
variant = models.CharField(max_length=64, blank=True, default="")
|
||||
search_term = models.CharField(max_length=200, blank=True, default="")
|
||||
evidence_kind = models.CharField(max_length=32, blank=True, default="")
|
||||
resolution = models.CharField(max_length=16, blank=True, default="")
|
||||
detail = models.JSONField(default=dict, blank=True)
|
||||
rule_version = models.CharField(max_length=16, default=C.ATTRIBUTION_RULE_VERSION)
|
||||
computed_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "source_classification"
|
||||
indexes = [models.Index(fields=["site", "visitor", "scope"]),
|
||||
models.Index(fields=["site", "channel"])]
|
||||
|
||||
|
||||
class DailyStat(models.Model):
|
||||
site = models.ForeignKey(Site, on_delete=models.CASCADE, related_name="daily_stats", db_index=False) # uq_site_date 前缀覆盖
|
||||
date = models.DateField()
|
||||
|
||||
Reference in New Issue
Block a user