10 12 25
Добавил таблицы для геолокации
This commit is contained in:
parent
2ab1bbc02c
commit
87da6efbfb
@ -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);
|
||||||
|
""");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,16 +11,19 @@ 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.")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user