Добавил таблицы для геолокации
This commit is contained in:
AidarKC 2025-12-10 13:54:15 +03:00
parent 2ab1bbc02c
commit 87da6efbfb
4 changed files with 190 additions and 21 deletions

View File

@ -19,6 +19,7 @@ import java.sql.Statement;
* - solana_users * - solana_users
* - active_sessions * - active_sessions
* - users_params * - users_params
* - ip_geo_cache
*/ */
public class DatabaseInitializer { public class DatabaseInitializer {
@ -95,7 +96,7 @@ public class DatabaseInitializer {
"""); """);
// 2. Таблица active_sessions // 2. Таблица active_sessions
// sessionId теперь TEXT (base64 от 32 байт), а не INTEGER. // sessionId TEXT (base64 от 32 байт).
st.executeUpdate(""" st.executeUpdate("""
CREATE TABLE IF NOT EXISTS active_sessions ( CREATE TABLE IF NOT EXISTS active_sessions (
sessionId TEXT NOT NULL PRIMARY KEY, sessionId TEXT NOT NULL PRIMARY KEY,
@ -136,6 +137,20 @@ public class DatabaseInitializer {
CREATE INDEX IF NOT EXISTS idx_users_params_loginId CREATE INDEX IF NOT EXISTS idx_users_params_loginId
ON 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);
""");
} }
} }
} }

View File

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

View File

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

View File

@ -13,14 +13,17 @@ import java.net.http.HttpResponse;
* Сервис для геолокации по IP. * Сервис для геолокации по IP.
* . * .
* Основной метод: * Основной метод:
* resolveCountryCityOrIp(ip) -> "Country, City" или исходный ip, если не удалось. * resolveCountryCityOrIp(ip) -> "Country, City" или GEO_UNKNOWN, если не удалось.
*/ */
public final class GeoLookupService { public final class GeoLookupService {
private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); 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/"; private static final String GEO_API_URL = "http://ip-api.com/json/";
// Сервис для получения собственного внешнего IP // Сервис для получения собственного внешнего IP
@ -32,12 +35,16 @@ public final class GeoLookupService {
/** /**
* Возвращает строку вида "Country, City" по IP. * Возвращает строку вида "Country, City" по IP.
* Если запрос не удался, возвращает исходный ip. * Если запрос не удался, возвращает GEO_UNKNOWN.
*/ */
public static String resolveCountryCityOrIp(String ip) { public static String resolveCountryCityOrIp(String ip) {
// На всякий случай простая защита от private/локальных IP (они всё равно не определяются) if (ip == null || ip.isBlank()) {
return GEO_UNKNOWN;
}
// Приватные/локальные IP геолокация невозможна
if (isPrivateOrLocalIp(ip)) { if (isPrivateOrLocalIp(ip)) {
return ip; return GEO_UNKNOWN;
} }
try { try {
@ -54,24 +61,25 @@ public final class GeoLookupService {
); );
if (response.statusCode() != 200) { if (response.statusCode() != 200) {
return ip; return GEO_UNKNOWN;
} }
JsonNode root = JSON_MAPPER.readTree(response.body()); JsonNode root = JSON_MAPPER.readTree(response.body());
String status = root.path("status").asText(); String status = root.path("status").asText();
if (!"success".equals(status)) { if (!"success".equals(status)) {
// Например: {"status":"fail","message":"private range"} // "fail", "private range", "quota exceeded", и т.д.
return ip; return GEO_UNKNOWN;
} }
String country = root.path("country").asText(null); String country = root.path("country").asText(null);
String city = root.path("city").asText(null); String city = root.path("city").asText(null);
if (country == null && city == null) { if (country == null && city == null) {
return ip; return GEO_UNKNOWN;
} }
// Собираем аккуратную строку // Собираем строку
if (country != null && city != null) { if (country != null && city != null) {
return country + ", " + city; return country + ", " + city;
} else if (country != null) { } else if (country != null) {
@ -79,9 +87,10 @@ public final class GeoLookupService {
} else { } else {
return city; return city;
} }
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
// В боевом коде можно логировать // Ошибки сети возвращаем unknown
return ip; return GEO_UNKNOWN;
} }
} }
@ -111,19 +120,16 @@ public final class GeoLookupService {
} }
return body.trim(); return body.trim();
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
// В боевом коде можно логировать
return fallbackIp; return fallbackIp;
} }
} }
/** /**
* Примитивная проверка на частные и локальные IP. * Проверка на частные/локальные IP.
* Для внешней геолокации они бесполезны.
*/ */
private static boolean isPrivateOrLocalIp(String ip) { private static boolean isPrivateOrLocalIp(String ip) {
if (ip == null) return true;
ip = ip.trim(); ip = ip.trim();
return ip.startsWith("10.") return ip.startsWith("10.")