42adebcc5b
Гео читается из локального файла (DB-IP Lite) — IP посетителя наружу не уходит. Нет файла базы → город не определяется, учёт продолжает работать (fail-open). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
71 lines
2.4 KiB
PHP
71 lines
2.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Tracking;
|
|
|
|
use GeoIp2\Database\Reader;
|
|
|
|
/**
|
|
* Город/регион/оператор по IP из ОФЛАЙН-базы (DB-IP Lite). Наружу ничего не шлём —
|
|
* IP посетителя не покидает сервер. Нет файла базы → пустой результат (fail-open).
|
|
* Хостинг/VPN определяем по названию организации: у таких гостей город недостоверен.
|
|
* Spec: docs/superpowers/specs/2026-07-13-visitors-analytics-design.md §9
|
|
*/
|
|
class GeoResolver
|
|
{
|
|
private const DATACENTER_MARKERS = [
|
|
'hosting', 'cloud', 'server', 'data center', 'datacenter', 'vpn', 'vps', 'proxy',
|
|
'digitalocean', 'tencent', 'amazon', 'aws', 'google llc', 'microsoft', 'ovh',
|
|
'hetzner', 'vultr', 'linode', 'scaleway', 'alibaba', 'oracle', 'colocation', 'm247',
|
|
];
|
|
|
|
/** @return array{city:?string,region:?string,asn_org:?string,is_datacenter:bool} */
|
|
public function lookup(string $ip): array
|
|
{
|
|
$empty = ['city' => null, 'region' => null, 'asn_org' => null, 'is_datacenter' => false];
|
|
|
|
$cityDb = (string) config('services.geoip.city_db');
|
|
$asnDb = (string) config('services.geoip.asn_db');
|
|
|
|
$city = null;
|
|
$region = null;
|
|
$org = null;
|
|
|
|
try {
|
|
if ($cityDb !== '' && is_readable($cityDb)) {
|
|
$rec = (new Reader($cityDb))->city($ip);
|
|
$city = $rec->city->name;
|
|
$region = $rec->mostSpecificSubdivision->name;
|
|
}
|
|
if ($asnDb !== '' && is_readable($asnDb)) {
|
|
$org = (new Reader($asnDb))->asn($ip)->autonomousSystemOrganization;
|
|
}
|
|
} catch (\Throwable) {
|
|
return $empty; // приватный IP, не найден, битая база — учёт не ломаем
|
|
}
|
|
|
|
return [
|
|
'city' => $city,
|
|
'region' => $region,
|
|
'asn_org' => $org,
|
|
'is_datacenter' => self::looksLikeDatacenter($org),
|
|
];
|
|
}
|
|
|
|
public static function looksLikeDatacenter(?string $org): bool
|
|
{
|
|
$o = mb_strtolower((string) $org);
|
|
if ($o === '') {
|
|
return false;
|
|
}
|
|
foreach (self::DATACENTER_MARKERS as $m) {
|
|
if (str_contains($o, $m)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|