Files
portal/app/app/Services/Tracking/GeoResolver.php
T
Дмитрий 42adebcc5b feat(visitors): определение города и оператора по офлайн-базе + признак VPN/хостинга
Гео читается из локального файла (DB-IP Lite) — IP посетителя наружу не уходит.
Нет файла базы → город не определяется, учёт продолжает работать (fail-open).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:32:34 +03:00

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;
}
}