Реализация автоопределения валюты по GeoIP для сайта
GeoIP-определение валюты — дополнение к системе мультивалютности, которое снимает с пользователя необходимость самостоятельно выбирать валюту при первом визите. Покупатель из Беларуси видит цены в BYN, из Германии — в EUR, из США — в USD. Реализация проще, чем кажется, но есть нюансы с точностью баз, кешированием и уважением к выбору пользователя.
Как работает GeoIP
IP-адрес → страна → валюта страны. Цепочка из двух шагов.
Шаг 1: IP → страна. Используются базы данных геолокации. Основные варианты:
| База | Тип | Точность по стране | Стоимость |
|---|---|---|---|
| MaxMind GeoLite2 | Локальная MMDB | ~95–99% | Бесплатно (регистрация) |
| MaxMind GeoIP2 | Локальная + API | ~99%+ | Платно |
| ip-api.com | HTTP API | ~98% | Бесплатно (1000/мин) |
| ipinfo.io | HTTP API | ~99% | Freemium |
| DB-IP | Локальная | ~95% | Freemium |
Для большинства проектов MaxMind GeoLite2 — оптимальный выбор: локальная база не зависит от внешних сервисов и не тормозит запросы.
Шаг 2: страна → валюта. Статическая таблица соответствий ISO 3166-1 → ISO 4217.
Установка MaxMind GeoLite2
composer require geoip2/geoip2
База обновляется MaxMind каждые вторник и пятницу. Для автоматического обновления используется утилита geoipupdate:
# /etc/GeoIP.conf
AccountID 123456
LicenseKey your_license_key
EditionIDs GeoLite2-Country
DatabaseDirectory /var/lib/GeoIP
# cron каждую среду и субботу
0 3 * * 3,6 /usr/local/bin/geoipupdate
Сервис определения валюты
class GeoIpCurrencyDetector
{
private Reader $geoIpReader;
public function __construct()
{
$this->geoIpReader = new Reader(
storage_path('app/geoip/GeoLite2-Country.mmdb')
);
}
public function detect(string $ip): ?string
{
// Приватные и зарезервированные диапазоны — пропускаем
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return null;
}
try {
$record = $this->geoIpReader->country($ip);
$countryCode = $record->country->isoCode; // 'BY', 'RU', 'DE' и т.д.
return $this->mapCountryToCurrency($countryCode);
} catch (AddressNotFoundException) {
return null;
}
}
private function mapCountryToCurrency(string $countryCode): ?string
{
$map = config('geoip.country_currency_map');
return $map[$countryCode] ?? null;
}
}
Конфиг country_currency_map — это массив из ~250 стран. Ключевые записи:
// config/geoip.php
return [
'country_currency_map' => [
'BY' => 'BYN',
'RU' => 'RUB',
'UA' => 'UAH',
'KZ' => 'KZT',
'US' => 'USD',
'CA' => 'CAD',
'GB' => 'GBP',
'DE' => 'EUR', 'FR' => 'EUR', 'IT' => 'EUR', 'ES' => 'EUR',
'PL' => 'PLN',
'CZ' => 'CZK',
'CN' => 'CNY',
'JP' => 'JPY',
// ...и ещё ~200 стран
],
'fallback_currency' => 'USD',
];
Кеширование результата
Определение по IP — быстрая операция (локальная база, ~0.5 мс), но кешировать всё равно стоит: защита от повторных операций чтения файла при каждом запросе.
public function detectCached(string $ip): ?string
{
$cacheKey = 'geoip:' . md5($ip);
return Cache::remember($cacheKey, now()->addDay(), function () use ($ip) {
return $this->detect($ip);
});
}
TTL 24 часа — баланс между актуальностью (пользователь не меняет страну каждый час) и точностью (VPN-переключение подхватится на следующий день).
Интеграция в middleware
class ResolveCurrencyFromGeoIp
{
public function handle(Request $request, Closure $next): Response
{
// Если пользователь уже сделал явный выбор — не перебиваем
if ($this->hasExplicitChoice($request)) {
return $next($request);
}
$ip = $request->ip();
// Учитываем прокси и балансировщики
if ($request->header('CF-Connecting-IP')) {
$ip = $request->header('CF-Connecting-IP'); // Cloudflare
} elseif ($request->header('X-Real-IP')) {
$ip = $request->header('X-Real-IP'); // nginx proxy_pass
}
$currency = $this->detector->detectCached($ip);
if ($currency && $this->isSupportedCurrency($currency)) {
session(['auto_currency' => $currency]);
Cookie::queue('preferred_currency', $currency, 60 * 24 * 90);
}
return $next($request);
}
private function hasExplicitChoice(Request $request): bool
{
// Пользователь явно переключал валюту
return session()->has('explicit_currency_choice')
|| $request->user()?->preferred_currency;
}
}
Обработка IP за прокси
Проблемы с IP возникают при:
- Cloudflare — реальный IP в
CF-Connecting-IP - nginx reverse proxy — реальный IP в
X-Real-IPилиX-Forwarded-For - AWS ELB —
X-Forwarded-For(первый в списке)
Для Laravel правильная конфигурация через TrustProxies middleware:
// app/Http/Middleware/TrustProxies.php
protected $proxies = '*'; // или конкретные IP балансировщиков
protected $headers = Request::HEADER_X_FORWARDED_FOR
| Request::HEADER_X_FORWARDED_HOST
| Request::HEADER_X_FORWARDED_PORT
| Request::HEADER_X_FORWARDED_PROTO;
После этого $request->ip() вернёт корректный клиентский IP.
UX: уведомление об автоопределении
Хорошая практика — показать пользователю тост/баннер при первом автоопределении:
Мы определили, что вы из Беларуси. Цены показаны в BYN. Изменить валюту →
Баннер показывается один раз (флаг в sessionStorage) и содержит быструю ссылку для смены валюты. Это уважение к пользователю: автоматика помогает, а не навязывает.
Когда GeoIP не работает
- VPN/прокси: пользователь из RU выглядит как US → показывается USD. Решение: явная ссылка «Изменить валюту» всегда доступна.
- Корпоративные сети: IP зарегистрирован в другой стране. Аналогично.
- Tor: exit-нода в случайной стране. Fallback на валюту по умолчанию.
- IPv6: база GeoLite2-Country поддерживает IPv6 начиная с версии 2020+. Убедитесь, что используется актуальная версия.
Тестирование
Для локального тестирования с фиктивными IP:
// В тестах или локальном окружении
if (app()->environment('local')) {
$ip = config('geoip.test_ip', '178.124.0.1'); // белорусский IP
}
Для автотестов — mock сервиса:
$this->mock(GeoIpCurrencyDetector::class, function ($mock) {
$mock->shouldReceive('detectCached')->andReturn('BYN');
});
Сроки реализации
- Установка GeoLite2 + базовое определение + маппинг стран: 1 день
- Middleware + кеширование + интеграция с системой мультивалютности: 1 день
- UX-уведомление + обработка явного выбора: 0,5 дня
- Автообновление базы (cron + geoipupdate): 0,5 дня
Итого: 2–3 дня при условии, что система мультивалютности уже реализована.







