码桶

发现社区成员的开源项目

geekssl /

wstats

公开
main
wstats/app/Support/Referrer.php
Referrer.php5.5 KB
<?php
/**
 * 流量来源分类。
 * 优先顺序:UTM 参数 > 广告平台 ClickID > 搜索引擎/社交/外链 > 直接访问。
 * 归属策略:会话首次事件(入口)判定一次,之后的站内跳转不覆盖来源。
 */
declare(strict_types=1);

namespace Wstat\Support;

class Referrer
{
    public const SEARCH = [
        'baidu.com', 'google.', 'bing.com', 'so.com', 'sogou.com', 'sm.cn', 'yahoo.',
        'yandex.', 'duckduckgo.com', 'ecosia.org', 'ask.com', 'yisou.com', 'toutiao.com/search',
    ];

    public const SOCIAL = [
        'weibo.com', 'weibo.cn', 'zhihu.com', 'xiaohongshu.com', 'xhslink.com', 'douyin.com',
        'iesdouyin.com', 'tiktok.com', 'wechat.com', 'weixin.qq.com', 'qq.com', 'qzone.qq.com',
        'facebook.com', 'twitter.com', 'x.com', 'instagram.com', 'youtube.com', 'linkedin.com',
        'telegram.org', 'discord.com', 'bilibili.com', 'kuaishou.com', 'feishu.cn', 'youtube.com',
    ];

    /** 各平台 ClickID 参数名 → 平台 */
    public const CLICKID = [
        'gclid' => 'Google Ads', 'msclkid' => 'Microsoft Ads', 'fbclid' => 'Meta Ads',
        'ttclid' => 'TikTok Ads', 'ytclid' => 'YouTube Ads', 'sccid' => 'Snapchat Ads',
        'dclid' => 'DoubleClick', 'wbraid' => 'Google Ads', 'gbraid' => 'Google Ads',
        'lgclickid' => 'LG U+', 'qqclid' => 'Tencent Ads', 'ogclid' => 'Omnicom',
    ];

    /**
     * @param string $refUrl 入口页面 referrer(可为空)
     * @param string $siteDomain 站点域名
     * @param array  $utm  ['source','medium','campaign','content','term']
     * @param string $clickId 已识别出的 ClickID 值(或 '')
     * @return array{type:string, source:string, medium:string, label:string, host:string}
     */
    public static function classify(string $refUrl, string $siteDomain, array $utm = [], string $clickId = ''): array
    {
        $u = $utm + ['source' => '', 'medium' => '', 'campaign' => '', 'content' => '', 'term' => ''];

        // 1) UTM 优先(只要带 source 或 medium 即视为营销来源)
        if ($u['source'] !== '' || $u['medium'] !== '') {
            $medium = $u['medium'] !== '' ? $u['medium'] : 'utm';
            return [
                'type' => 'utm',
                'source' => $u['source'] !== '' ? $u['source'] : 'utm',
                'medium' => $medium,
                'label' => 'UTM: ' . ($u['source'] ?: $u['medium']),
                'host' => '',
            ];
        }

        // 2) ClickID
        if ($clickId !== '') {
            $from = self::clickPlatform($refUrl) ?: 'Paid';
            return ['type' => 'paid', 'source' => $from, 'medium' => 'cpc', 'label' => 'Paid: ' . $from, 'host' => ''];
        }

        // 3) referrer 分析
        $refUrl = trim($refUrl);
        if ($refUrl === '') {
            return ['type' => 'direct', 'source' => 'direct', 'medium' => '', 'label' => 'Direct', 'host' => ''];
        }
        $host = self::hostOf($refUrl);
        if ($host === '') {
            return ['type' => 'direct', 'source' => 'direct', 'medium' => '', 'label' => 'Direct', 'host' => ''];
        }
        // 站内跳转(同域或子域)不算外链
        if ($siteDomain !== '' && (self::hostEquals($host, $siteDomain))) {
            return ['type' => 'internal', 'source' => 'internal', 'medium' => '', 'label' => 'Internal', 'host' => $host];
        }
        foreach (self::SEARCH as $k) {
            if (strpos($host, $k) !== false) {
                return ['type' => 'search', 'source' => self::brand($host), 'medium' => 'organic', 'label' => 'Search: ' . self::brand($host), 'host' => $host];
            }
        }
        foreach (self::SOCIAL as $k) {
            if (strpos($host, $k) !== false) {
                return ['type' => 'social', 'source' => self::brand($host), 'medium' => 'social', 'label' => 'Social: ' . self::brand($host), 'host' => $host];
            }
        }
        return ['type' => 'link', 'source' => self::brand($host), 'medium' => 'referral', 'label' => 'Link: ' . self::brand($host), 'host' => $host];
    }

    /** 从 ClickID 参数判断广告平台(无 referrer 时) */
    public static function clickPlatform(string $refUrl): string
    {
        return ''; // 平台已由 SDK 上报 click_id 参数名解析,见 classify() 分支1
    }

    /** referrer 中的 host 提取 */
    public static function hostOf(string $url): string
    {
        $parts = parse_url($url);
        return isset($parts['host']) ? mb_strtolower((string) $parts['host']) : '';
    }

    /** 域匹配:相等 或 host 以 .siteDomain 结尾(子域) */
    public static function hostEquals(string $host, string $siteDomain): bool
    {
        $h = mb_strtolower($host);
        $s = mb_strtolower($siteDomain);
        return $h === $s || substr($h, -strlen($s) - 1) === '.' . $s;
    }

    /** 品牌名(取主域二级名,如 www.baidu.com / m.weibo.cn → baidu / weibo) */
    public static function brand(string $host): string
    {
        $h = preg_replace('/^www\./', '', $host);
        if ($h === null || $h === '') {
            return $host;
        }
        $parts = explode('.', $h);
        $n = count($parts);
        if ($n <= 1) {
            return $h;
        }
        // 国家二级后缀组合(xxx.com.cn / xxx.co.uk ...)取倒数第三段
        $cc2 = ['com.cn', 'net.cn', 'org.cn', 'gov.cn', 'com.br', 'co.uk', 'com.au', 'com.mx', 'co.jp', 'com.hk', 'com.tw', 'com.sg'];
        if ($n >= 3 && in_array($parts[$n - 2] . '.' . $parts[$n - 1], $cc2, true)) {
            return $parts[$n - 3];
        }
        return $parts[$n - 2];
    }
}