From 87da6efbfbb583a6532da595c32d0e4df865efa4b90bcebe32adde2b313080d6 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 10 Dec 2025 13:54:15 +0300 Subject: [PATCH] 10 12 25 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавил таблицы для геолокации --- .../java/shine/db/DatabaseInitializer.java | 19 +++- .../main/java/shine/db/dao/IpGeoCacheDAO.java | 99 +++++++++++++++++++ .../shine/db/entities/IpGeoCacheEntry.java | 49 +++++++++ .../main/java/shine.geo/GeoLookupService.java | 44 +++++---- 4 files changed, 190 insertions(+), 21 deletions(-) create mode 100644 shine-server-db/src/main/java/shine/db/dao/IpGeoCacheDAO.java create mode 100644 shine-server-db/src/main/java/shine/db/entities/IpGeoCacheEntry.java diff --git a/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java b/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java index bb19704..38d9606 100644 --- a/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java +++ b/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java @@ -19,6 +19,7 @@ import java.sql.Statement; * - solana_users * - active_sessions * - users_params + * - ip_geo_cache */ public class DatabaseInitializer { @@ -95,7 +96,7 @@ public class DatabaseInitializer { """); // 2. Таблица active_sessions - // sessionId теперь TEXT (base64 от 32 байт), а не INTEGER. + // sessionId TEXT (base64 от 32 байт). st.executeUpdate(""" CREATE TABLE IF NOT EXISTS active_sessions ( sessionId TEXT NOT NULL PRIMARY KEY, @@ -136,6 +137,20 @@ public class DatabaseInitializer { CREATE INDEX IF NOT EXISTS idx_users_params_loginId ON users_params (loginId); """); + + // 4. Таблица ip_geo_cache — персистентный кэш геолокации по IP + st.executeUpdate(""" + CREATE TABLE IF NOT EXISTS ip_geo_cache ( + ip TEXT NOT NULL PRIMARY KEY, + geo TEXT, + updated_at_ms INTEGER NOT NULL + ); + """); + + st.executeUpdate(""" + CREATE INDEX IF NOT EXISTS idx_ip_geo_cache_updated_at + ON ip_geo_cache (updated_at_ms); + """); } } -} +} \ No newline at end of file diff --git a/shine-server-db/src/main/java/shine/db/dao/IpGeoCacheDAO.java b/shine-server-db/src/main/java/shine/db/dao/IpGeoCacheDAO.java new file mode 100644 index 0000000..ab20831 --- /dev/null +++ b/shine-server-db/src/main/java/shine/db/dao/IpGeoCacheDAO.java @@ -0,0 +1,99 @@ +package shine.db.dao; + +import shine.db.SqliteDbController; +import shine.db.entities.IpGeoCacheEntry; + +import java.sql.*; + +/** + * DAO для таблицы ip_geo_cache. + * + * Таблица: + * - ip TEXT PRIMARY KEY + * - geo TEXT + * - updated_at_ms INTEGER NOT NULL + */ +public final class IpGeoCacheDAO { + + private static volatile IpGeoCacheDAO instance; + private final SqliteDbController db = SqliteDbController.getInstance(); + + private IpGeoCacheDAO() { + } + + public static IpGeoCacheDAO getInstance() { + if (instance == null) { + synchronized (IpGeoCacheDAO.class) { + if (instance == null) { + instance = new IpGeoCacheDAO(); + } + } + } + return instance; + } + + /** + * UPSERT по ip. + * Если записи нет — вставляем. + * Если есть — обновляем geo и updated_at_ms. + */ + public void upsert(IpGeoCacheEntry entry) throws SQLException { + String sql = """ + INSERT INTO ip_geo_cache (ip, geo, updated_at_ms) + VALUES (?, ?, ?) + ON CONFLICT(ip) + DO UPDATE SET + geo = excluded.geo, + updated_at_ms = excluded.updated_at_ms + """; + + try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { + ps.setString(1, entry.getIp()); + ps.setString(2, entry.getGeo()); + ps.setLong(3, entry.getUpdatedAtMs()); + ps.executeUpdate(); + } + } + + /** + * Получить запись по IP. + * Если нет — возвращает null. + */ + public IpGeoCacheEntry getByIp(String ip) throws SQLException { + String sql = """ + SELECT ip, geo, updated_at_ms + FROM ip_geo_cache + WHERE ip = ? + """; + + try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { + ps.setString(1, ip); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) { + return null; + } + return mapRow(rs); + } + } + } + + /** + * Опционально — очистка старых записей. + * Можно вызывать по расписанию, если нужно. + */ + public int deleteOlderThan(long thresholdMs) throws SQLException { + String sql = "DELETE FROM ip_geo_cache WHERE updated_at_ms < ?"; + + try (PreparedStatement ps = db.getConnection().prepareStatement(sql)) { + ps.setLong(1, thresholdMs); + return ps.executeUpdate(); + } + } + + private IpGeoCacheEntry mapRow(ResultSet rs) throws SQLException { + String ip = rs.getString("ip"); + String geo = rs.getString("geo"); + long updatedAtMs = rs.getLong("updated_at_ms"); + return new IpGeoCacheEntry(ip, geo, updatedAtMs); + } +} \ No newline at end of file diff --git a/shine-server-db/src/main/java/shine/db/entities/IpGeoCacheEntry.java b/shine-server-db/src/main/java/shine/db/entities/IpGeoCacheEntry.java new file mode 100644 index 0000000..ef4b654 --- /dev/null +++ b/shine-server-db/src/main/java/shine/db/entities/IpGeoCacheEntry.java @@ -0,0 +1,49 @@ +package shine.db.entities; + +/** + * Запись в таблице ip_geo_cache. + * + * Храним: + * - ip — строка IP-адреса (PRIMARY KEY) + * - geo — строка "Country, City" или любое текстовое описание + * - updatedAtMs — время последнего обновления (Unix time в мс) + */ +public class IpGeoCacheEntry { + + private String ip; + private String geo; + private long updatedAtMs; + + public IpGeoCacheEntry() { + } + + public IpGeoCacheEntry(String ip, String geo, long updatedAtMs) { + this.ip = ip; + this.geo = geo; + this.updatedAtMs = updatedAtMs; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public String getGeo() { + return geo; + } + + public void setGeo(String geo) { + this.geo = geo; + } + + public long getUpdatedAtMs() { + return updatedAtMs; + } + + public void setUpdatedAtMs(long updatedAtMs) { + this.updatedAtMs = updatedAtMs; + } +} \ No newline at end of file diff --git a/shine-server-geo/src/main/java/shine.geo/GeoLookupService.java b/shine-server-geo/src/main/java/shine.geo/GeoLookupService.java index 6aede55..a136415 100644 --- a/shine-server-geo/src/main/java/shine.geo/GeoLookupService.java +++ b/shine-server-geo/src/main/java/shine.geo/GeoLookupService.java @@ -11,16 +11,19 @@ import java.net.http.HttpResponse; /** * Сервис для геолокации по IP. - *. + * . * Основной метод: - * resolveCountryCityOrIp(ip) -> "Country, City" или исходный ip, если не удалось. + * resolveCountryCityOrIp(ip) -> "Country, City" или GEO_UNKNOWN, если не удалось. */ public final class GeoLookupService { private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); - // Сервис геолокации. Сейчас ip-api.com, можно потом вынести в конфиг. + // Константа — что возвращать, если геолокация недоступна + public static final String GEO_UNKNOWN = "unknown"; + + // Сервис геолокации (потом можно вынести в конфиг) private static final String GEO_API_URL = "http://ip-api.com/json/"; // Сервис для получения собственного внешнего IP @@ -32,12 +35,16 @@ public final class GeoLookupService { /** * Возвращает строку вида "Country, City" по IP. - * Если запрос не удался, возвращает исходный ip. + * Если запрос не удался, возвращает GEO_UNKNOWN. */ public static String resolveCountryCityOrIp(String ip) { - // На всякий случай простая защита от private/локальных IP (они всё равно не определяются) + if (ip == null || ip.isBlank()) { + return GEO_UNKNOWN; + } + + // Приватные/локальные IP — геолокация невозможна if (isPrivateOrLocalIp(ip)) { - return ip; + return GEO_UNKNOWN; } try { @@ -54,24 +61,25 @@ public final class GeoLookupService { ); if (response.statusCode() != 200) { - return ip; + return GEO_UNKNOWN; } JsonNode root = JSON_MAPPER.readTree(response.body()); String status = root.path("status").asText(); + if (!"success".equals(status)) { - // Например: {"status":"fail","message":"private range"} - return ip; + // "fail", "private range", "quota exceeded", и т.д. + return GEO_UNKNOWN; } String country = root.path("country").asText(null); String city = root.path("city").asText(null); if (country == null && city == null) { - return ip; + return GEO_UNKNOWN; } - // Собираем аккуратную строку + // Собираем строку if (country != null && city != null) { return country + ", " + city; } else if (country != null) { @@ -79,9 +87,10 @@ public final class GeoLookupService { } else { return city; } + } catch (IOException | InterruptedException e) { - // В боевом коде можно логировать - return ip; + // Ошибки сети — возвращаем unknown + return GEO_UNKNOWN; } } @@ -111,19 +120,16 @@ public final class GeoLookupService { } return body.trim(); + } catch (IOException | InterruptedException e) { - // В боевом коде можно логировать return fallbackIp; } } /** - * Примитивная проверка на частные и локальные IP. - * Для внешней геолокации они бесполезны. + * Проверка на частные/локальные IP. */ private static boolean isPrivateOrLocalIp(String ip) { - if (ip == null) return true; - ip = ip.trim(); return ip.startsWith("10.") @@ -134,4 +140,4 @@ public final class GeoLookupService { // Диапазон 172.16.0.0 – 172.31.255.255 || ip.matches("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"); } -} +} \ No newline at end of file