feat(server): 归因v2——paid_social渠道与fb+付费medium优先规则

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-18 22:41:58 +08:00
parent be0b53bce4
commit 52a8a72e3a
4 changed files with 66 additions and 10 deletions
+42 -3
View File
@@ -69,7 +69,7 @@ def test_paid_click_param_wins_over_natural_search():
assert r["channel"] == "paid_search" and r["platform"] == "Google Ads" # 付费优先于自然
assert r["detail"].get("click_param") == "gclid"
r2 = classify(landing_path="/?fbclid=IwAR0") # 仅落地页带点击参数
assert r2["channel"] == "paid_search" and r2["platform"] == "Meta Ads"
assert r2["channel"] == "paid_social" and r2["platform"] == "Meta Ads" # v2:fbclid → 付费社交
def test_paid_param_in_referrer_url():
@@ -148,13 +148,14 @@ def test_utm_chinese_alias_baidu_matches():
# ------------------------------------------- 广告家族归一(付费点击参数 ↔ UTM 同家族)
def test_fbclid_with_utm_fb_same_meta_family_combined():
r = classify(landing_path="/?fbclid=IwAR0", utm={"utm_source": "fb"})
assert r["channel"] == "paid_search" and r["platform"] == "Meta Ads"
assert r["channel"] == "paid_social" and r["platform"] == "Meta Ads" # v2:fbclid → 付费社交
assert r["resolution"] == "resolved" and r["evidence_kind"] == "combined"
assert r["detail"].get("utm_source") == "fb" # fb ↔ Meta Ads 同 meta 家族
def test_fbclid_with_utm_ig_combined():
r = classify(landing_path="/?fbclid=IwAR0", utm={"utm_source": "ig"})
assert r["channel"] == "paid_social"
assert r["platform"] == "Meta Ads" # ig 也在 meta 家族
assert r["resolution"] != "conflict" and r["evidence_kind"] == "combined"
@@ -202,6 +203,44 @@ def test_garbage_referrer_url_does_not_crash():
assert r["channel"] in ("referral", "direct") # 不崩,落到兜底分支即可
# ---------------------------------------- paid_social 渠道(归因 v2,spec §3)
def test_fb_paid_medium_priority():
# 实测主导模式:utm_medium=paid&utm_source=fb → paid_social(优先于强化集)
r = classify(referrer_url="", utm={"utm_source": "fb", "utm_medium": "paid"},
landing_path="/", ua="", internal_domains=[])
assert r["channel"] == "paid_social"
def test_fb_family_medium_union():
for m in ("cpc", "social", "paid_social", "social_ads"):
r = classify(referrer_url="", utm={"utm_source": "facebook.com", "utm_medium": m},
landing_path="/", ua="", internal_domains=[])
assert r["channel"] == "paid_social", m
def test_nonfb_paid_medium_stays_paid_search_label_only():
r = classify(referrer_url="", utm={"utm_source": "google", "utm_medium": "cpc"},
landing_path="/", ua="", internal_domains=[])
assert r["channel"] == "paid_search" and r["resolution"] == "label_only"
def test_fbclid_now_paid_social():
r = classify(referrer_url="", utm={}, landing_path="/?fbclid=IwARxyz", ua="", internal_domains=[])
assert r["channel"] == "paid_social"
def test_gclid_still_paid_search():
r = classify(referrer_url="", utm={}, landing_path="/?gclid=EAIa", ua="", internal_domains=[])
assert r["channel"] == "paid_search"
def test_nonfb_social_medium_not_paid():
# twitter&utm_medium=social → 不判付费(有意分歧:防误伤自然社交)
r = classify(referrer_url="", utm={"utm_source": "twitter", "utm_medium": "social"},
landing_path="/", ua="", internal_domains=[])
assert r["channel"] != "paid_search" and r["channel"] != "paid_social"
# ------------------------------------------------------- 入库 + 重算集成
@pytest.mark.django_db
def test_ingest_writes_first_touch_and_session_entry():
@@ -216,7 +255,7 @@ def test_ingest_writes_first_touch_and_session_entry():
ft = SourceClassification.objects.get(site=site, visitor__visitor_id="v1", scope="first_touch")
assert ft.session is None
assert ft.channel == "natural_search" and ft.platform == "Bing"
assert ft.search_term == "挖掘机" and ft.rule_version == "v1"
assert ft.search_term == "挖掘机" and ft.rule_version == "v2"
se = SourceClassification.objects.get(site=site, scope="session_entry")
assert se.session.session_id == "s1"
assert se.channel == "natural_search" and se.platform == "Bing"