поменял все названия таблиц и полей в таблицах на стиль только маленькие буквы и разделение через "_"  . Все тесты проходят норм
This commit is contained in:
AidarKC 2026-01-06 01:49:26 +03:00
parent 93c007b2b9
commit 8fd7f4676b
16 changed files with 467 additions and 532 deletions

View File

@ -1,132 +1,209 @@
1) Перечень таблиц и кратко о каждой
SHiNE — структура БД (актуальная версия)
solana_users — справочник пользователей: логин + ключ устройства + (опционально) Solana-ключ. База для FK почти во всех таблицах.
active_sessions — активные сессии авторизации/работы клиента: пароли сессии/хранилища, времена, push-данные, IP/инфо клиента.
users_params — “последние значения параметров” пользователя. Для каждого (login,param) хранится актуальная версия по time_ms (старые обновления игнорируются).
ip_geo_cache — кеш геолокации по IP, чтобы не дергать внешние сервисы постоянно.
blockchain_state — агрегатное состояние блокчейна по blockchainName: лимиты/размер, последний глобальный блок и последние блоки по линиям 0..7.
blocks — локальное хранилище блоков/сообщений: привязка к пользователю и блокчейну, номера/хэши, тип/подтип, тело блока (BLOB), и опциональная “ссылка на другой блок” через поля to*. (PK сейчас намеренно убран.)
2) Таблицы подробно: параметры по строкам
Перечень таблиц и назначение
solana_users
login — TEXT PK — уникальный логин пользователя.
deviceKey — TEXT NOT NULL — публичный ключ устройства (обычно Base64(32) или HEX(64)).
solanaKey — TEXT NULL — публичный ключ Solana-аккаунта (если используется).
Справочник пользователей: логин + ключ устройства + (опционально) Solana-ключ.
Базовая таблица, используется как FK почти везде.
active_sessions
sessionId — TEXT PK — идентификатор сессии (строка; по смыслу base64(32)).
login — TEXT NOT NULL, FK → solana_users(login) — владелец сессии.
sessionPwd — TEXT NOT NULL — пароль/секрет сессии (как хранится — строкой).
storagePwd — TEXT NOT NULL — пароль/секрет для storage (как хранится — строкой).
sessionCreatedAtMs — INTEGER NOT NULL — время создания сессии (Unix ms).
lastAuthirificatedAtMs — INTEGER NOT NULL — время последней авторизации/refresh (Unix ms).
pushEndpoint — TEXT NULL — endpoint для WebPush.
pushP256dhKey — TEXT NULL — p256dh ключ для WebPush.
pushAuthKey — TEXT NULL — auth ключ для WebPush.
clientIp — TEXT NULL — IP клиента на auth/refresh.
clientInfoFromClient — TEXT NULL — строка, присланная клиентом (PWA).
clientInfoFromRequest — TEXT NULL — строка, собранная сервером из запроса.
userLanguage — TEXT NULL — язык пользователя (например ru-RU).
Активные сессии авторизации/работы клиента: секреты, тайминги, WebPush-данные, IP и информация о клиенте.
users_params
login — TEXT NOT NULL, FK → solana_users(login) — владелец параметра.
param — TEXT NOT NULL — имя параметра (ключ).
time_ms — INTEGER NOT NULL — версия/время параметра (Unix ms), используется для “только если новее”.
value — TEXT NOT NULL — значение параметра.
device_key — TEXT NULL — каким ключом подписано (если используешь), строковый формат.
signature — TEXT NULL — подпись (если используешь), строковый формат.
Ограничение: UNIQUE(login, param) — один актуальный параметр на пару.
Хранилище актуальных параметров пользователя.
Для каждой пары (login, param) хранится только самая новая версия по time_ms.
ip_geo_cache
ip — TEXT PK — IP-адрес (строкой).
geo — TEXT NULL — гео-строка (Country/City или как договоритесь).
updated_at_ms — INTEGER NOT NULL — когда кеш обновлялся (Unix ms).
Кеш геолокации по IP для снижения нагрузки на внешние сервисы.
blockchain_state
blockchainName — TEXT PK — имя/ID блокчейна (уникальное).
login — TEXT NOT NULL, FK → solana_users(login) — владелец блокчейна.
blockchainKey — TEXT NOT NULL — публичный ключ блокчейна (по смыслу Base64(32)).
size_limit — INTEGER NOT NULL — лимит размера (по смыслу bytes; в Java это long).
file_size_bytes — INTEGER NOT NULL — текущий размер файла блокчейна в байтах.
last_global_number — INTEGER NOT NULL — последний глобальный номер блока (genesis = -1).
last_global_hash — TEXT NOT NULL — хэш последнего глобального блока (или пустая строка).
updated_at_ms — INTEGER NOT NULL — время обновления состояния (Unix ms).
Линии 0..7 (для каждой линии две колонки):
line0_last_number — INTEGER NOT NULL — последний номер в линии 0.
line0_last_hash — TEXT NOT NULL — последний хэш в линии 0.
line1_last_number — INTEGER NOT NULL — последний номер в линии 1.
line1_last_hash — TEXT NOT NULL — последний хэш в линии 1.
line2_last_number — INTEGER NOT NULL — последний номер в линии 2.
line2_last_hash — TEXT NOT NULL — последний хэш в линии 2.
line3_last_number — INTEGER NOT NULL — последний номер в линии 3.
line3_last_hash — TEXT NOT NULL — последний хэш в линии 3.
line4_last_number — INTEGER NOT NULL — последний номер в линии 4.
line4_last_hash — TEXT NOT NULL — последний хэш в линии 4.
line5_last_number — INTEGER NOT NULL — последний номер в линии 5.
line5_last_hash — TEXT NOT NULL — последний хэш в линии 5.
line6_last_number — INTEGER NOT NULL — последний номер в линии 6.
line6_last_hash — TEXT NOT NULL — последний хэш в линии 6.
line7_last_number — INTEGER NOT NULL — последний номер в линии 7.
line7_last_hash — TEXT NOT NULL — последний хэш в линии 7.
Агрегированное состояние блокчейна по blockchain_name:
лимиты, текущий размер, последний глобальный блок и состояние линий 0..7.
blocks
login — TEXT NOT NULL, FK → solana_users(login) — чей блок (логин).
bchName — TEXT NOT NULL, FK → blockchain_state(blockchainName) — к какому блокчейну относится блок.
blockGlobalNumber — INTEGER NOT NULL — глобальный номер блока.
blockGlobalPreHashe — TEXT NOT NULL — хэш предыдущего глобального блока.
blockLineIndex — INTEGER NOT NULL — индекс линии (0..7), по смыслу int16.
blockLineNumber — INTEGER NOT NULL — номер блока в линии.
blockLinePreHashe — TEXT NOT NULL — хэш предыдущего блока в линии.
msgType — INTEGER NOT NULL — общий тип сообщения/блока (по смыслу uint16).
msgSubType — INTEGER NOT NULL — подтип внутри msgType (по смыслу uint16).
blockByte — BLOB NULL — сырой байтовый блок/тело сообщения.
Ссылка на другой блок (nullable, для ответов/репостов/связей и т.п.):
to_login — TEXT NULL — логин “на кого/кому/с кем” (смысловая цель).
toBchName — TEXT NULL — целевой блокчейн.
toBlockGlobalNumber — INTEGER NULL — целевой глобальный номер.
toBlockHashe — TEXT NULL — хэш целевого блока.
Журнал всех блоков и сообщений.
Содержит историю событий: тексты, реакции, ответы, связи.
PRIMARY KEY намеренно отсутствует.
connections_state ⭐
Текущее состояние связей пользователя.
login — TEXT NOT NULL — владелец связи.
relType — INTEGER NOT NULL — тип связи:
10 = FRIEND, 20 = CONTACT, 30 = FOLLOW.
to_login — TEXT NOT NULL — с кем связь.
toBchName — TEXT NOT NULL — блокчейн цели.
toBlockGlobalNumber — INTEGER NULL — последний известный номер блока цели.
toBlockHashe — TEXT NULL — последний известный хэш блока цели.
Актуальное состояние связей между пользователями
(друг / контакт / подписка).
Обновляется автоматически на основе событий из blocks.
message_stats ⭐
Агрегированные счётчики лайков и ответов на конкретные сообщения.
Поддерживается триггерами из blocks.
Таблицы подробно
solana_users
login — TEXT PK — уникальный логин пользователя
device_key — TEXT NOT NULL — публичный ключ устройства (Base64(32) / HEX(64))
solana_key — TEXT NULL — публичный ключ Solana-аккаунта
active_sessions
session_id — TEXT PK — идентификатор сессии
login — TEXT NOT NULL, FK → solana_users(login)
session_pwd — TEXT NOT NULL — секрет сессии
storage_pwd — TEXT NOT NULL — секрет storage
session_created_at_ms — INTEGER NOT NULL
last_authirificated_at_ms — INTEGER NOT NULL
push_endpoint — TEXT NULL
push_p256dh_key — TEXT NULL
push_auth_key — TEXT NULL
client_ip — TEXT NULL
client_info_from_client — TEXT NULL
client_info_from_request — TEXT NULL
user_language — TEXT NULL
users_params
login — TEXT NOT NULL, FK → solana_users(login)
param — TEXT NOT NULL
time_ms — INTEGER NOT NULL
value — TEXT NOT NULL
device_key — TEXT NULL
signature — TEXT NULL
Ограничение:
UNIQUE(login, relType, to_login) — одно актуальное состояние связи.
UNIQUE(login, param)
3) Триггер логики связей
Логика:
обновление принимается только если excluded.time_ms > users_params.time_ms
Триггер trg_blocks_connection_state_ai срабатывает AFTER INSERT ON blocks:
если msgType = 3 (ConnectionBody)
msgSubType IN (10,20,30) →
добавить или обновить запись в connections_state
msgSubType IN (11,21,31) →
удалить соответствующую связь (10/20/30)
повторные добавления и удаления не вызывают ошибок
Таким образом:
ip_geo_cache
ip — TEXT PK
geo — TEXT NULL
updated_at_ms — INTEGER NOT NULL
blockchain_state
blockchain_name — TEXT PK
login — TEXT NOT NULL, FK → solana_users(login)
blockchain_key — TEXT NOT NULL
size_limit — INTEGER NOT NULL
file_size_bytes — INTEGER NOT NULL
last_global_number — INTEGER NOT NULL (-1 = genesis)
last_global_hash — TEXT NOT NULL
updated_at_ms — INTEGER NOT NULL
Линии 0..7:
для каждой линии:
lineX_last_number
lineX_last_hash
blocks
login — TEXT NOT NULL
bch_name — TEXT NOT NULL
block_global_number — INTEGER NOT NULL
block_global_pre_hash — TEXT NOT NULL
block_line_index — INTEGER NOT NULL
block_line_number — INTEGER NOT NULL
block_line_pre_hash — TEXT NOT NULL
msg_type — INTEGER NOT NULL
msg_sub_type — INTEGER NOT NULL
block_bytes — BLOB NULL
Ссылка на другой блок (nullable):
to_login
to_bch_name
to_block_global_number
to_block_hash
connections_state ⭐
Текущее агрегированное состояние связей.
login — TEXT NOT NULL
rel_type — INTEGER NOT NULL
10 = FRIEND
20 = CONTACT
30 = FOLLOW
to_login — TEXT NOT NULL
to_bch_name — TEXT NOT NULL
to_block_global_number — INTEGER NULL
to_block_hash — TEXT NULL
Ограничение:
UNIQUE(login, rel_type, to_login)
message_stats ⭐
Счётчики активности по целевому сообщению.
to_login — TEXT NOT NULL
to_bch_name — TEXT NOT NULL
to_block_global_number — INTEGER NOT NULL
to_block_hash — TEXT NOT NULL
likes_count — INTEGER NOT NULL DEFAULT 0
replies_count — INTEGER NOT NULL DEFAULT 0
UNIQUE:
(to_login, to_bch_name, to_block_global_number, to_block_hash)
Триггеры БД (полная логика)
3.1 Связи пользователей
trg_blocks_connection_state_ai
AFTER INSERT ON blocks
Условие:
msg_type = 3 (connection)
Добавление / обновление связи
msg_sub_type IN (10,20,30)
выполняется UPSERT в connections_state
Удаление связи
msg_sub_type IN (11,21,31)
удаляется соответствующая связь:
11 → 10
21 → 20
31 → 30
Итог:
blocks — журнал событий
connections_state — всегда актуальное состояние
Индексы (кратко, чтобы было понятно зачем)
idx_solana_users_login(login) — быстрый поиск по логину.
idx_active_sessions_login(login) — быстро получить сессии пользователя.
idx_users_params_login(login) — быстро получить параметры пользователя.
idx_ip_geo_cache_updated_at(updated_at_ms) — чистка старых записей.
idx_blockchain_state_login(login) — блокчейны пользователя.
idx_blockchain_state_updated_at(updated_at_ms) — выборки/обслуживание по “свежести”.
idx_blocks_chain_global(login,bchName,blockGlobalNumber) — выборки блоков по цепочке.
idx_blocks_to_target(to_login,toBchName,toBlockGlobalNumber) — быстрые выборки “по ссылке”.
3.2 Подсчёт лайков ⭐
trg_blocks_message_stats_like_ai
AFTER INSERT ON blocks
Условие:
msg_type = 2 (reaction)
msg_sub_type = 1 (like)
Действие:
определяется цель по to_bch_name, to_block_global_number, to_block_hash
to_login вычисляется как
substr(to_bch_name, 1, length(to_bch_name) - 3)
выполняется UPSERT в message_stats
likes_count += 1
3.3 Подсчёт ответов ⭐
trg_blocks_message_stats_reply_ai
AFTER INSERT ON blocks
Условие:
msg_type = 1 (text)
msg_sub_type = 2 (reply)
Действие:
цель определяется аналогично лайкам
выполняется UPSERT в message_stats
replies_count += 1
Индексы (смысл)
idx_solana_users_login — поиск пользователя
idx_active_sessions_login — сессии пользователя
idx_users_params_login — параметры пользователя
idx_ip_geo_cache_updated_at — чистка кеша
idx_blockchain_state_login — блокчейны пользователя
idx_blockchain_state_updated_at — обслуживание
idx_blocks_chain_global — чтение цепочки
idx_blocks_to_target — реакции / ответы
idx_message_stats_target — быстрый доступ к счётчикам
Итоговая модель мышления
blocks — неизменяемый журнал событий
connections_state — проекция связей
message_stats — проекция активности
всё вычисляется детерминированно через триггеры

View File

@ -21,8 +21,8 @@ import java.sql.Statement;
* - ip_geo_cache
* - blockchain_state
* - blocks
* - connections_state (текущее состояние связей)
* - message_stats (счётчики лайков/ответов на сообщения)
* - connections_state
* - message_stats
*/
public class DatabaseInitializer {
@ -83,8 +83,8 @@ public class DatabaseInitializer {
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS solana_users (
login TEXT NOT NULL PRIMARY KEY,
deviceKey TEXT NOT NULL,
solanaKey TEXT
device_key TEXT NOT NULL,
solana_key TEXT
);
""");
@ -96,19 +96,19 @@ public class DatabaseInitializer {
// 2. active_sessions
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS active_sessions (
sessionId TEXT NOT NULL PRIMARY KEY,
session_id TEXT NOT NULL PRIMARY KEY,
login TEXT NOT NULL,
sessionPwd TEXT NOT NULL,
storagePwd TEXT NOT NULL,
sessionCreatedAtMs INTEGER NOT NULL,
lastAuthirificatedAtMs INTEGER NOT NULL,
pushEndpoint TEXT,
pushP256dhKey TEXT,
pushAuthKey TEXT,
clientIp TEXT,
clientInfoFromClient TEXT,
clientInfoFromRequest TEXT,
userLanguage TEXT,
session_pwd TEXT NOT NULL,
storage_pwd TEXT NOT NULL,
session_created_at_ms INTEGER NOT NULL,
last_authirificated_at_ms INTEGER NOT NULL,
push_endpoint TEXT,
push_p256dh_key TEXT,
push_auth_key TEXT,
client_ip TEXT,
client_info_from_client TEXT,
client_info_from_request TEXT,
user_language TEXT,
FOREIGN KEY (login) REFERENCES solana_users(login)
);
""");
@ -154,9 +154,9 @@ public class DatabaseInitializer {
// 5. blockchain_state
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS blockchain_state (
blockchainName TEXT NOT NULL PRIMARY KEY,
blockchain_name TEXT NOT NULL PRIMARY KEY,
login TEXT NOT NULL,
blockchainKey TEXT NOT NULL,
blockchain_key TEXT NOT NULL,
size_limit INTEGER NOT NULL,
file_size_bytes INTEGER NOT NULL,
@ -200,54 +200,52 @@ public class DatabaseInitializer {
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS blocks (
login TEXT NOT NULL,
bchName TEXT NOT NULL,
blockGlobalNumber INTEGER NOT NULL,
blockGlobalPreHashe TEXT NOT NULL,
bch_name TEXT NOT NULL,
block_global_number INTEGER NOT NULL,
block_global_pre_hashe TEXT NOT NULL,
blockLineIndex INTEGER NOT NULL,
blockLineNumber INTEGER NOT NULL,
blockLinePreHashe TEXT NOT NULL,
block_line_index INTEGER NOT NULL,
block_line_number INTEGER NOT NULL,
block_line_pre_hashe TEXT NOT NULL,
msgType INTEGER NOT NULL,
msgSubType INTEGER NOT NULL,
msg_type INTEGER NOT NULL,
msg_sub_type INTEGER NOT NULL,
blockByte BLOB,
block_byte BLOB,
to_login TEXT,
toBchName TEXT,
toBlockGlobalNumber INTEGER,
toBlockHashe TEXT,
to_bch_name TEXT,
to_block_global_number INTEGER,
to_block_hashe TEXT,
FOREIGN KEY (login) REFERENCES solana_users(login),
FOREIGN KEY (bchName) REFERENCES blockchain_state(blockchainName)
FOREIGN KEY (bch_name) REFERENCES blockchain_state(blockchain_name)
);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_blocks_chain_global
ON blocks (login, bchName, blockGlobalNumber);
ON blocks (login, bch_name, block_global_number);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_blocks_to_target
ON blocks (to_login, toBchName, toBlockGlobalNumber);
ON blocks (to_login, to_bch_name, to_block_global_number);
""");
// =====================================================================
// 7) connections_state текущее состояние "кто с кем и какая связь"
// =====================================================================
// 7) connections_state
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS connections_state (
login TEXT NOT NULL,
relType INTEGER NOT NULL, -- 10/20/30 (FRIEND/CONTACT/FOLLOW)
rel_type INTEGER NOT NULL,
to_login TEXT NOT NULL,
toBchName TEXT NOT NULL,
toBlockGlobalNumber INTEGER,
toBlockHashe TEXT,
to_bch_name TEXT NOT NULL,
to_block_global_number INTEGER,
to_block_hashe TEXT,
FOREIGN KEY (login) REFERENCES solana_users(login),
UNIQUE (login, relType, to_login)
UNIQUE (login, rel_type, to_login)
);
""");
@ -266,54 +264,47 @@ public class DatabaseInitializer {
ON connections_state (login, to_login);
""");
// =====================================================================
// 8) Trigger: при вставке connection-блоков в blocks обновлять connections_state
// =====================================================================
// 8) Trigger: connection state
st.executeUpdate("""
CREATE TRIGGER IF NOT EXISTS trg_blocks_connection_state_ai
AFTER INSERT ON blocks
WHEN NEW.msgType = 3
WHEN NEW.msg_type = 3
BEGIN
INSERT INTO connections_state (
login, relType, to_login, toBchName, toBlockGlobalNumber, toBlockHashe
login, rel_type, to_login, to_bch_name, to_block_global_number, to_block_hashe
)
SELECT
NEW.login,
NEW.msgSubType,
NEW.msg_sub_type,
NEW.to_login,
NEW.toBchName,
NEW.toBlockGlobalNumber,
NEW.toBlockHashe
WHERE NEW.msgSubType IN (10, 20, 30)
NEW.to_bch_name,
NEW.to_block_global_number,
NEW.to_block_hashe
WHERE NEW.msg_sub_type IN (10, 20, 30)
AND NEW.to_login IS NOT NULL
AND NEW.toBchName IS NOT NULL
ON CONFLICT(login, relType, to_login)
AND NEW.to_bch_name IS NOT NULL
ON CONFLICT(login, rel_type, to_login)
DO UPDATE SET
toBchName = excluded.toBchName,
toBlockGlobalNumber = excluded.toBlockGlobalNumber,
toBlockHashe = excluded.toBlockHashe;
to_bch_name = excluded.to_bch_name,
to_block_global_number = excluded.to_block_global_number,
to_block_hashe = excluded.to_block_hashe;
DELETE FROM connections_state
WHERE login = NEW.login
AND to_login = NEW.to_login
AND relType = CASE NEW.msgSubType
AND rel_type = CASE NEW.msg_sub_type
WHEN 11 THEN 10
WHEN 21 THEN 20
WHEN 31 THEN 30
ELSE relType
ELSE rel_type
END
AND NEW.msgSubType IN (11, 21, 31);
AND NEW.msg_sub_type IN (11, 21, 31);
END;
""");
// =====================================================================
// 9) message_stats счётчики лайков/ответов на конкретный блок-цель
//
// Правило системы:
// - to_login берём из toBchName: отрезаем последние 3 символа
// =====================================================================
// 9) message_stats
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS message_stats (
to_login TEXT NOT NULL,
@ -343,15 +334,11 @@ public class DatabaseInitializer {
ON message_stats (to_login);
""");
// =====================================================================
// 10) Trigger: LIKE (Reaction)
// - msgType=2 (REACTION)
// - msgSubType=1 (LIKE)
// =====================================================================
// 10) Trigger: LIKE
st.executeUpdate("""
CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_like_ai
AFTER INSERT ON blocks
WHEN NEW.msgType = 2 AND NEW.msgSubType = 1
WHEN NEW.msg_type = 2 AND NEW.msg_sub_type = 1
BEGIN
INSERT INTO message_stats (
to_login,
@ -362,31 +349,27 @@ public class DatabaseInitializer {
replies_count
)
SELECT
substr(NEW.toBchName, 1, length(NEW.toBchName) - 3),
NEW.toBchName,
NEW.toBlockGlobalNumber,
NEW.toBlockHashe,
substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 3),
NEW.to_bch_name,
NEW.to_block_global_number,
NEW.to_block_hashe,
1,
0
WHERE NEW.toBchName IS NOT NULL
AND length(NEW.toBchName) > 3
AND NEW.toBlockGlobalNumber IS NOT NULL
AND NEW.toBlockHashe IS NOT NULL
WHERE NEW.to_bch_name IS NOT NULL
AND length(NEW.to_bch_name) > 3
AND NEW.to_block_global_number IS NOT NULL
AND NEW.to_block_hashe IS NOT NULL
ON CONFLICT(to_login, to_bch_name, to_block_global_number, to_block_hash)
DO UPDATE SET
likes_count = message_stats.likes_count + 1;
END;
""");
// =====================================================================
// 11) Trigger: REPLY (Text)
// - msgType=1 (TEXT)
// - msgSubType=2 (REPLY)
// =====================================================================
// 11) Trigger: REPLY
st.executeUpdate("""
CREATE TRIGGER IF NOT EXISTS trg_blocks_message_stats_reply_ai
AFTER INSERT ON blocks
WHEN NEW.msgType = 1 AND NEW.msgSubType = 2
WHEN NEW.msg_type = 1 AND NEW.msg_sub_type = 2
BEGIN
INSERT INTO message_stats (
to_login,
@ -397,16 +380,16 @@ public class DatabaseInitializer {
replies_count
)
SELECT
substr(NEW.toBchName, 1, length(NEW.toBchName) - 3),
NEW.toBchName,
NEW.toBlockGlobalNumber,
NEW.toBlockHashe,
substr(NEW.to_bch_name, 1, length(NEW.to_bch_name) - 3),
NEW.to_bch_name,
NEW.to_block_global_number,
NEW.to_block_hashe,
0,
1
WHERE NEW.toBchName IS NOT NULL
AND length(NEW.toBchName) > 3
AND NEW.toBlockGlobalNumber IS NOT NULL
AND NEW.toBlockHashe IS NOT NULL
WHERE NEW.to_bch_name IS NOT NULL
AND length(NEW.to_bch_name) > 3
AND NEW.to_block_global_number IS NOT NULL
AND NEW.to_block_hashe IS NOT NULL
ON CONFLICT(to_login, to_bch_name, to_block_global_number, to_block_hash)
DO UPDATE SET
replies_count = message_stats.replies_count + 1;

View File

@ -50,10 +50,6 @@ public final class SqliteDbController {
return instance;
}
/**
* Каждый вызов возвращает НОВОЕ соединение.
* Закрывать обязан вызывающий код (try-with-resources).
*/
public Connection getConnection() throws SQLException {
Connection conn = DriverManager.getConnection(jdbcUrl);
conn.setAutoCommit(true);
@ -68,7 +64,6 @@ public final class SqliteDbController {
return conn;
}
/** Теперь close() не нужен. */
public void close() {
// no-op
}

View File

@ -36,19 +36,19 @@ public final class ActiveSessionsDAO {
public void insert(Connection c, ActiveSessionEntry session) throws SQLException {
String sql = """
INSERT INTO active_sessions (
sessionId,
session_id,
login,
sessionPwd,
storagePwd,
sessionCreatedAtMs,
lastAuthirificatedAtMs,
pushEndpoint,
pushP256dhKey,
pushAuthKey,
clientIp,
clientInfoFromClient,
clientInfoFromRequest,
userLanguage
session_pwd,
storage_pwd,
session_created_at_ms,
last_authirificated_at_ms,
push_endpoint,
push_p256dh_key,
push_auth_key,
client_ip,
client_info_from_client,
client_info_from_request,
user_language
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
@ -83,21 +83,21 @@ public final class ActiveSessionsDAO {
public ActiveSessionEntry getBySessionId(Connection c, String sessionId) throws SQLException {
String sql = """
SELECT
sessionId,
session_id,
login,
sessionPwd,
storagePwd,
sessionCreatedAtMs,
lastAuthirificatedAtMs,
pushEndpoint,
pushP256dhKey,
pushAuthKey,
clientIp,
clientInfoFromClient,
clientInfoFromRequest,
userLanguage
session_pwd,
storage_pwd,
session_created_at_ms,
last_authirificated_at_ms,
push_endpoint,
push_p256dh_key,
push_auth_key,
client_ip,
client_info_from_client,
client_info_from_request,
user_language
FROM active_sessions
WHERE sessionId = ?
WHERE session_id = ?
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
@ -120,19 +120,19 @@ public final class ActiveSessionsDAO {
public List<ActiveSessionEntry> getByLogin(Connection c, String login) throws SQLException {
String sql = """
SELECT
sessionId,
session_id,
login,
sessionPwd,
storagePwd,
sessionCreatedAtMs,
lastAuthirificatedAtMs,
pushEndpoint,
pushP256dhKey,
pushAuthKey,
clientIp,
clientInfoFromClient,
clientInfoFromRequest,
userLanguage
session_pwd,
storage_pwd,
session_created_at_ms,
last_authirificated_at_ms,
push_endpoint,
push_p256dh_key,
push_auth_key,
client_ip,
client_info_from_client,
client_info_from_request,
user_language
FROM active_sessions
WHERE login = ?
""";
@ -162,8 +162,8 @@ public final class ActiveSessionsDAO {
public void updateLastAuthirificatedAtMs(Connection c, String sessionId, long lastAuthMs) throws SQLException {
String sql = """
UPDATE active_sessions
SET lastAuthirificatedAtMs = ?
WHERE sessionId = ?
SET last_authirificated_at_ms = ?
WHERE session_id = ?
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
@ -194,12 +194,12 @@ public final class ActiveSessionsDAO {
String sql = """
UPDATE active_sessions
SET
lastAuthirificatedAtMs = ?,
clientIp = ?,
clientInfoFromClient = ?,
clientInfoFromRequest = ?,
userLanguage = ?
WHERE sessionId = ?
last_authirificated_at_ms = ?,
client_ip = ?,
client_info_from_client = ?,
client_info_from_request = ?,
user_language = ?
WHERE session_id = ?
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
@ -231,7 +231,7 @@ public final class ActiveSessionsDAO {
/** Удалить по sessionId с внешним соединением. Соединение НЕ закрывает. */
public void deleteBySessionId(Connection c, String sessionId) throws SQLException {
String sql = "DELETE FROM active_sessions WHERE sessionId = ?";
String sql = "DELETE FROM active_sessions WHERE session_id = ?";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, sessionId);
@ -249,19 +249,19 @@ public final class ActiveSessionsDAO {
// -------------------- MAPPER --------------------
private ActiveSessionEntry mapRow(ResultSet rs) throws SQLException {
String sessionId = rs.getString("sessionId");
String sessionId = rs.getString("session_id");
String login = rs.getString("login");
String sessionPwd = rs.getString("sessionPwd");
String storagePwd = rs.getString("storagePwd");
long sessionCreatedAtMs = rs.getLong("sessionCreatedAtMs");
long lastAuthirificatedAtMs = rs.getLong("lastAuthirificatedAtMs");
String pushEndpoint = rs.getString("pushEndpoint");
String pushP256dhKey = rs.getString("pushP256dhKey");
String pushAuthKey = rs.getString("pushAuthKey");
String clientIp = rs.getString("clientIp");
String clientInfoFromClient = rs.getString("clientInfoFromClient");
String clientInfoFromRequest = rs.getString("clientInfoFromRequest");
String userLanguage = rs.getString("userLanguage");
String sessionPwd = rs.getString("session_pwd");
String storagePwd = rs.getString("storage_pwd");
long sessionCreatedAtMs = rs.getLong("session_created_at_ms");
long lastAuthirificatedAtMs = rs.getLong("last_authirificated_at_ms");
String pushEndpoint = rs.getString("push_endpoint");
String pushP256dhKey = rs.getString("push_p256dh_key");
String pushAuthKey = rs.getString("push_auth_key");
String clientIp = rs.getString("client_ip");
String clientInfoFromClient = rs.getString("client_info_from_client");
String clientInfoFromRequest = rs.getString("client_info_from_request");
String userLanguage = rs.getString("user_language");
return new ActiveSessionEntry(
sessionId,

View File

@ -32,9 +32,9 @@ public final class BlockchainStateDAO {
public BlockchainStateEntry getByBlockchainName(Connection c, String blockchainName) throws SQLException {
String sql = """
SELECT
blockchainName,
blockchain_name,
login,
blockchainKey,
blockchain_key,
size_limit,
file_size_bytes,
last_global_number,
@ -49,7 +49,7 @@ public final class BlockchainStateDAO {
line6_last_number, line6_last_hash,
line7_last_number, line7_last_hash
FROM blockchain_state
WHERE blockchainName = ?
WHERE blockchain_name = ?
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
@ -79,9 +79,9 @@ public final class BlockchainStateDAO {
String sql = """
INSERT INTO blockchain_state (
blockchainName,
blockchain_name,
login,
blockchainKey,
blockchain_key,
size_limit,
file_size_bytes,
last_global_number,
@ -106,10 +106,10 @@ public final class BlockchainStateDAO {
?,?,
?,?
)
ON CONFLICT(blockchainName)
ON CONFLICT(blockchain_name)
DO UPDATE SET
login = excluded.login,
blockchainKey = excluded.blockchainKey,
blockchain_key = excluded.blockchain_key,
size_limit = excluded.size_limit,
file_size_bytes = excluded.file_size_bytes,
last_global_number = excluded.last_global_number,
@ -158,12 +158,6 @@ public final class BlockchainStateDAO {
/**
* Атомарно увеличить file_size_bytes на deltaBytes, но только если НЕ превысим size_limit.
*
* Возвращает:
* - true если обновили (лимит не превышен)
* - false если лимит превышается или blockchainName не найден
*
* ВАЖНО: это именно тот механизм, который надо дергать при добавлении блока.
*/
public boolean tryIncreaseFileSizeWithinLimit(Connection c, String blockchainName, long deltaBytes, long nowMs) throws SQLException {
String sql = """
@ -172,7 +166,7 @@ public final class BlockchainStateDAO {
file_size_bytes = file_size_bytes + ?,
updated_at_ms = ?
WHERE
blockchainName = ?
blockchain_name = ?
AND (file_size_bytes + ?) <= size_limit
""";
@ -202,11 +196,10 @@ public final class BlockchainStateDAO {
private BlockchainStateEntry mapRow(ResultSet rs) throws SQLException {
BlockchainStateEntry e = new BlockchainStateEntry();
e.setBlockchainName(rs.getString("blockchainName"));
e.setBlockchainName(rs.getString("blockchain_name"));
e.setLogin(rs.getString("login"));
e.setBlockchainKey(rs.getString("blockchainKey"));
e.setBlockchainKey(rs.getString("blockchain_key"));
// size_limit теперь long
e.setSizeLimit(rs.getLong("size_limit"));
e.setFileSizeBytes(rs.getLong("file_size_bytes"));

View File

@ -38,19 +38,19 @@ public final class BlocksDAO {
String sql = """
INSERT INTO blocks (
login,
bchName,
blockGlobalNumber,
blockGlobalPreHashe,
blockLineIndex,
blockLineNumber,
blockLinePreHashe,
msgType,
msgSubType,
blockByte,
bch_name,
block_global_number,
block_global_pre_hashe,
block_line_index,
block_line_number,
block_line_pre_hashe,
msg_type,
msg_sub_type,
block_byte,
to_login,
toBchName,
toBlockGlobalNumber,
toBlockHashe
to_bch_name,
to_block_global_number,
to_block_hashe
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
@ -69,16 +69,11 @@ public final class BlocksDAO {
// -------------------- UPSERT (UPDATE -> INSERT) --------------------
/**
* Сохранить (условный upsert) с внешним соединением. Соединение НЕ закрывает.
* Без PK/UNIQUE делаем: UPDATE по "ключевым" полям -> если 0 строк, то INSERT.
*/
public void upsert(Connection c, BlockEntry e) throws SQLException {
int updated = update(c, e);
if (updated == 0) insert(c, e);
}
/** Сохранить (upsert) без внешнего соединения. Сам открывает/закрывает. */
public void upsert(BlockEntry e) throws SQLException {
try (Connection c = db.getConnection()) {
upsert(c, e);
@ -87,7 +82,6 @@ public final class BlocksDAO {
// -------------------- SELECT --------------------
/** Получить блок по "PK-подобному" набору полей с внешним соединением. Соединение НЕ закрывает. */
public BlockEntry getByPk(Connection c,
String login,
String bchName,
@ -98,26 +92,26 @@ public final class BlocksDAO {
String sql = """
SELECT
login,
bchName,
blockGlobalNumber,
blockGlobalPreHashe,
blockLineIndex,
blockLineNumber,
blockLinePreHashe,
msgType,
msgSubType,
blockByte,
bch_name,
block_global_number,
block_global_pre_hashe,
block_line_index,
block_line_number,
block_line_pre_hashe,
msg_type,
msg_sub_type,
block_byte,
to_login,
toBchName,
toBlockGlobalNumber,
toBlockHashe
to_bch_name,
to_block_global_number,
to_block_hashe
FROM blocks
WHERE
login = ?
AND bchName = ?
AND blockGlobalNumber = ?
AND blockLineIndex = ?
AND blockLineNumber = ?
AND bch_name = ?
AND block_global_number = ?
AND block_line_index = ?
AND block_line_number = ?
LIMIT 1
""";
@ -135,7 +129,6 @@ public final class BlocksDAO {
}
}
/** Получить блок по "PK-подобному" набору полей без внешнего соединения. Сам открывает/закрывает. */
public BlockEntry getByPk(String login,
String bchName,
int blockGlobalNumber,
@ -148,29 +141,25 @@ public final class BlocksDAO {
// -------------------- UPDATE --------------------
/**
* Обновить (строго UPDATE) по "PK-подобному" набору полей с внешним соединением. Соединение НЕ закрывает.
* Может обновить >1 строк, если в таблице появились дубликаты.
*/
public int update(Connection c, BlockEntry e) throws SQLException {
String sql = """
UPDATE blocks
SET
blockGlobalPreHashe = ?,
blockLinePreHashe = ?,
msgType = ?,
msgSubType = ?,
blockByte = ?,
block_global_pre_hashe = ?,
block_line_pre_hashe = ?,
msg_type = ?,
msg_sub_type = ?,
block_byte = ?,
to_login = ?,
toBchName = ?,
toBlockGlobalNumber = ?,
toBlockHashe = ?
to_bch_name = ?,
to_block_global_number = ?,
to_block_hashe = ?
WHERE
login = ?
AND bchName = ?
AND blockGlobalNumber = ?
AND blockLineIndex = ?
AND blockLineNumber = ?
AND bch_name = ?
AND block_global_number = ?
AND block_line_index = ?
AND block_line_number = ?
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
@ -207,7 +196,6 @@ public final class BlocksDAO {
}
}
/** Обновить без внешнего соединения. Сам открывает/закрывает. */
public int update(BlockEntry e) throws SQLException {
try (Connection c = db.getConnection()) {
return update(c, e);
@ -216,10 +204,6 @@ public final class BlocksDAO {
// -------------------- DELETE --------------------
/**
* Удалить по "PK-подобному" набору полей с внешним соединением. Соединение НЕ закрывает.
* Может удалить >1 строк, если есть дубликаты.
*/
public int deleteByPk(Connection c,
String login,
String bchName,
@ -231,10 +215,10 @@ public final class BlocksDAO {
DELETE FROM blocks
WHERE
login = ?
AND bchName = ?
AND blockGlobalNumber = ?
AND blockLineIndex = ?
AND blockLineNumber = ?
AND bch_name = ?
AND block_global_number = ?
AND block_line_index = ?
AND block_line_number = ?
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
@ -247,7 +231,6 @@ public final class BlocksDAO {
}
}
/** Удалить по "PK-подобному" набору полей без внешнего соединения. Сам открывает/закрывает. */
public int deleteByPk(String login,
String bchName,
int blockGlobalNumber,
@ -260,7 +243,6 @@ public final class BlocksDAO {
// -------------------- INTERNAL --------------------
/** Единая привязка параметров под INSERT — чтобы не разъезжалось. */
private static void bindAll(PreparedStatement ps, BlockEntry e) throws SQLException {
int i = 1;
@ -297,29 +279,29 @@ public final class BlocksDAO {
BlockEntry e = new BlockEntry();
e.setLogin(rs.getString("login"));
e.setBchName(rs.getString("bchName"));
e.setBlockGlobalNumber(rs.getInt("blockGlobalNumber"));
e.setBlockGlobalPreHashe(rs.getString("blockGlobalPreHashe"));
e.setBchName(rs.getString("bch_name"));
e.setBlockGlobalNumber(rs.getInt("block_global_number"));
e.setBlockGlobalPreHashe(rs.getString("block_global_pre_hashe"));
e.setBlockLineIndex(rs.getInt("blockLineIndex"));
e.setBlockLineNumber(rs.getInt("blockLineNumber"));
e.setBlockLinePreHashe(rs.getString("blockLinePreHashe"));
e.setBlockLineIndex(rs.getInt("block_line_index"));
e.setBlockLineNumber(rs.getInt("block_line_number"));
e.setBlockLinePreHashe(rs.getString("block_line_pre_hashe"));
e.setMsgType(rs.getInt("msgType"));
e.setMsgSubType(rs.getInt("msgSubType"));
e.setMsgType(rs.getInt("msg_type"));
e.setMsgSubType(rs.getInt("msg_sub_type"));
e.setBlockByte(rs.getBytes("blockByte"));
e.setBlockByte(rs.getBytes("block_byte"));
e.setToLogin(rs.getString("to_login"));
String toBchName = rs.getString("toBchName");
String toBchName = rs.getString("to_bch_name");
if (rs.wasNull()) toBchName = null;
e.setToBchName(toBchName);
Integer toBlockGlobalNumber = (Integer) rs.getObject("toBlockGlobalNumber");
Integer toBlockGlobalNumber = (Integer) rs.getObject("to_block_global_number");
e.setToBlockGlobalNumber(toBlockGlobalNumber);
String toBlockHashe = rs.getString("toBlockHashe");
String toBlockHashe = rs.getString("to_block_hashe");
if (rs.wasNull()) toBlockHashe = null;
e.setToBlockHashe(toBlockHashe);

View File

@ -8,10 +8,10 @@ import java.sql.*;
/**
* DAO для таблицы ip_geo_cache.
*
* * Таблица:
* * - ip TEXT PRIMARY KEY
* * - geo TEXT
* * - updated_at_ms INTEGER NOT NULL
* Таблица:
* - ip TEXT PRIMARY KEY
* - geo TEXT
* - updated_at_ms INTEGER NOT NULL
*
* Правило:
* - методы с Connection НЕ закрывают соединение

View File

@ -14,18 +14,8 @@ import java.util.List;
*
* Колонки:
* - login TEXT PRIMARY KEY
* Уникальный логин пользователя (case-insensitive используется на уровне запросов).
*
* - deviceKey TEXT NOT NULL
* Публичный ключ устройства пользователя.
* Хранится в Base64(32 bytes) или HEX(64 chars).
*
* - solanaKey TEXT NULLABLE
* Публичный ключ Solana-аккаунта пользователя (если есть).
*
* Назначение таблицы:
* - локальное сопоставление login deviceKey / solanaKey
* - используется для аутентификации, валидации подписей и связки с блокчейном
* - device_key TEXT NOT NULL
* - solana_key TEXT NULLABLE
*
* Правило работы с соединениями:
* - методы с Connection НЕ закрывают соединение
@ -52,7 +42,7 @@ public final class SolanaUsersDAO {
/** Вставка с внешним соединением. Соединение НЕ закрывает. */
public void insert(Connection c, SolanaUserEntry user) throws SQLException {
String sql = """
INSERT INTO solana_users (login, deviceKey, solanaKey)
INSERT INTO solana_users (login, device_key, solana_key)
VALUES (?, ?, ?)
""";
@ -102,7 +92,7 @@ public final class SolanaUsersDAO {
/** Получить по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */
public SolanaUserEntry getByLogin(Connection c, String login) throws SQLException {
String sql = """
SELECT login, deviceKey, solanaKey
SELECT login, device_key, solana_key
FROM solana_users
WHERE LOWER(login) = LOWER(?)
""";
@ -126,7 +116,7 @@ public final class SolanaUsersDAO {
/** Поиск по префиксу с внешним соединением. Соединение НЕ закрывает. */
public List<SolanaUserEntry> searchByLoginPrefix(Connection c, String prefix) throws SQLException {
String sql = """
SELECT login, deviceKey, solanaKey
SELECT login, device_key, solana_key
FROM solana_users
WHERE LOWER(login) LIKE ?
ORDER BY login
@ -157,10 +147,10 @@ public final class SolanaUsersDAO {
private SolanaUserEntry mapRow(ResultSet rs) throws SQLException {
SolanaUserEntry e = new SolanaUserEntry(
rs.getString("login"),
rs.getString("deviceKey")
rs.getString("device_key")
);
String solanaKey = rs.getString("solanaKey");
String solanaKey = rs.getString("solana_key");
if (rs.wasNull()) solanaKey = null;
e.setSolanaKey(solanaKey);

View File

@ -8,8 +8,8 @@ import java.sql.*;
/**
* UserCreateDAO атомарное добавление пользователя:
* - solana_users (login, deviceKey)
* - blockchain_state (blockchainName, login, blockchainKey, size_limit, ... last_global_number=-1 ...)
* - solana_users (login, device_key)
* - blockchain_state (blockchain_name, login, blockchain_key, size_limit, ... last_global_number=-1 ...)
*
* ВАЖНО:
* - только INSERT
@ -86,8 +86,6 @@ public final class UserCreateDAO {
} catch (SQLException e) {
c.rollback();
// SQLITE_CONSTRAINT -> "уже существует"
// Мы не делаем UPDATE, только insert.
String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase();
if (msg.contains("constraint")) {
return false;

View File

@ -17,11 +17,6 @@ import java.util.List;
* ЛОГИКА time_ms:
* - БД принимает запись только если она "новее" (time_ms строго больше текущего).
* - Реализовано атомарно одним SQL: UPSERT + WHERE users_params.time_ms < excluded.time_ms
*
* Возврат результата:
* - upsertIfNewer(...) возвращает количество изменённых строк:
* 1 = вставили/обновили
* 0 = проигнорировали (запись уже новее или равная)
*/
public final class UserParamsDAO {
@ -41,11 +36,6 @@ public final class UserParamsDAO {
// -------------------- UPSERT (IF NEWER) --------------------
/**
* Атомарный UPSERT "только если новее".
*
* @return 1 если вставили/обновили; 0 если запись не тронули (existing.time_ms >= incoming.time_ms).
*/
public int upsertIfNewer(Connection c, UserParamEntry e) throws SQLException {
String sql = """
INSERT INTO users_params (
@ -77,11 +67,10 @@ public final class UserParamsDAO {
if (e.getSignature() != null) ps.setString(6, e.getSignature());
else ps.setNull(6, Types.VARCHAR);
return ps.executeUpdate(); // 1 или 0
return ps.executeUpdate();
}
}
/** То же самое, но сам открывает/закрывает соединение. */
public int upsertIfNewer(UserParamEntry e) throws SQLException {
try (Connection c = db.getConnection()) {
return upsertIfNewer(c, e);
@ -90,7 +79,6 @@ public final class UserParamsDAO {
// -------------------- SELECT --------------------
/** Получить параметр по (login,param) с внешним соединением. Соединение НЕ закрывает. */
public UserParamEntry getByLoginAndParam(Connection c, String login, String param) throws SQLException {
String sql = """
SELECT
@ -116,14 +104,12 @@ public final class UserParamsDAO {
}
}
/** Получить параметр по (login,param) без внешнего соединения. Сам открывает/закрывает. */
public UserParamEntry getByLoginAndParam(String login, String param) throws SQLException {
try (Connection c = db.getConnection()) {
return getByLoginAndParam(c, login, param);
}
}
/** Получить все параметры пользователя с внешним соединением. */
public List<UserParamEntry> getByLogin(Connection c, String login) throws SQLException {
String sql = """
SELECT
@ -148,7 +134,6 @@ public final class UserParamsDAO {
return list;
}
/** Получить все параметры пользователя без внешнего соединения. */
public List<UserParamEntry> getByLogin(String login) throws SQLException {
try (Connection c = db.getConnection()) {
return getByLogin(c, login);

View File

@ -2,27 +2,23 @@ package shine.db.entities;
/**
* Модель активной сессии (таблица active_sessions).
*
* Теперь вместо loginId:
* - login TEXT NOT NULL (FK -> solana_users(login))
*/
public class ActiveSessionEntry {
private String sessionId; // TEXT base64(32 bytes)
private String login; // TEXT NOT NULL
private String sessionPwd; // TEXT
private String storagePwd; // TEXT
private long sessionCreatedAtMs; // INTEGER
private long lastAuthirificatedAtMs; // INTEGER
private String pushEndpoint; // TEXT (nullable)
private String pushP256dhKey; // TEXT (nullable)
private String pushAuthKey; // TEXT (nullable)
private String sessionId;
private String login;
private String sessionPwd;
private String storagePwd;
private long sessionCreatedAtMs;
private long lastAuthirificatedAtMs;
private String pushEndpoint;
private String pushP256dhKey;
private String pushAuthKey;
// Новые поля
private String clientIp; // IP клиента при auth/refresh
private String clientInfoFromClient; // строка от клиента (PWA)
private String clientInfoFromRequest; // строка, собранная на сервере
private String userLanguage; // prefer-language (например, "ru-RU")
private String clientIp;
private String clientInfoFromClient;
private String clientInfoFromRequest;
private String userLanguage;
public ActiveSessionEntry() {
}

View File

@ -2,41 +2,28 @@ package shine.db.entities;
/**
* Запись блока (таблица blocks).
*
* Теперь:
* - login TEXT NOT NULL
* - bchName TEXT NOT NULL (идёт сразу после login)
* - to_login TEXT nullable
* - toBchName TEXT nullable
* - toBlockGlobalNumber INTEGER nullable
* - toBlockHashe TEXT nullable
*
* ДОБАВЛЕНО:
* - msgSubType INTEGER (uint16 по смыслу, храним как int)
*
* PRIMARY KEY пока убран вообще.
*/
public class BlockEntry {
private String login; // TEXT
private String bchName; // TEXT
private String login;
private String bchName;
private int blockGlobalNumber; // int32
private String blockGlobalPreHashe; // TEXT
private int blockGlobalNumber;
private String blockGlobalPreHashe;
private int blockLineIndex; // int16 (храним как int)
private int blockLineNumber; // int32
private String blockLinePreHashe; // TEXT
private int blockLineIndex;
private int blockLineNumber;
private String blockLinePreHashe;
private int msgType; // int16 (храним как int)
private int msgSubType; // int16 (храним как int)
private int msgType;
private int msgSubType;
private byte[] blockByte; // BLOB
private byte[] blockByte;
private String toLogin; // TEXT nullable
private String toBchName; // TEXT nullable
private Integer toBlockGlobalNumber; // INTEGER nullable
private String toBlockHashe; // TEXT nullable
private String toLogin;
private String toBchName;
private Integer toBlockGlobalNumber;
private String toBlockHashe;
public BlockEntry() {}

View File

@ -5,29 +5,22 @@ import java.util.Base64;
/**
* Агрегатная сущность текущего состояния блокчейна.
* 1 строка = 1 blockchainName, плюс состояние линий 0..7.
* 1 строка = 1 blockchain_name, плюс состояние линий 0..7.
*/
public final class BlockchainStateEntry {
private String blockchainName;
private String login;
/** Ключ блокчейна (pubkey), которым подписываются блоки. Base64(32 bytes). */
private String blockchainKey;
/** Лимит (теперь long). */
private long sizeLimit;
/** Размер файла блокчейна в байтах (то, что будем сверять/чинить при старте). */
private long fileSizeBytes;
private int lastGlobalNumber;
private String lastGlobalHash; // HEX(64) либо пустая строка для "нулевого"
private String lastGlobalHash;
/** line 0..7 */
private final int[] lastLineNumbers = new int[8];
/** line 0..7 */
private final String[] lastLineHashes = new String[8];
private long updatedAtMs;
@ -78,7 +71,6 @@ public final class BlockchainStateEntry {
public String getBlockchainKey() { return blockchainKey; }
public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }
/** blockchainKey в байтах (32) или null, если битый. */
public byte[] getBlockchainKeyBytes() {
if (blockchainKey == null) return null;
String s = blockchainKey.trim();
@ -103,7 +95,6 @@ public final class BlockchainStateEntry {
public String getLastGlobalHash() { return lastGlobalHash; }
public void setLastGlobalHash(String lastGlobalHash) { this.lastGlobalHash = lastGlobalHash == null ? "" : lastGlobalHash; }
/** line in [0..7] */
public int getLastLineNumber(int line) {
checkLine(line);
return lastLineNumbers[line];
@ -113,7 +104,6 @@ public final class BlockchainStateEntry {
lastLineNumbers[line] = value;
}
/** line in [0..7] */
public String getLastLineHash(int line) {
checkLine(line);
return lastLineHashes[line];

View File

@ -2,11 +2,6 @@ package shine.db.entities;
/**
* Запись в таблице ip_geo_cache.
*
* Храним:
* - ip строка IP-адреса (PRIMARY KEY)
* - geo строка "Country, City" или любое текстовое описание
* - updatedAtMs время последнего обновления (Unix time в мс)
*/
public class IpGeoCacheEntry {

View File

@ -9,34 +9,14 @@ import java.util.Base64;
*
* Поля:
* - login PRIMARY KEY (TEXT)
* Уникальный логин пользователя.
*
* - deviceKey TEXT NOT NULL
* Публичный ключ устройства.
* Поддерживаемые форматы:
* Base64 (32 байта)
* HEX (64 hex-символа)
*
* - solanaKey TEXT NULLABLE
* Публичный ключ Solana-аккаунта пользователя (если используется).
*
* Назначение:
* - хранение минимальной локальной информации о пользователе
* - используется для:
* проверки подписи запросов
* привязки пользователя к блокчейну
* сопоставления login ключи
*
* ВАЖНО:
* - deviceKey обязателен всегда
* - solanaKey может отсутствовать (null)
* - device_key TEXT NOT NULL
* - solana_key TEXT NULLABLE
*/
public class SolanaUserEntry {
private String login; // TEXT PK
private String deviceKey; // TEXT NOT NULL (Base64(32 bytes))
private String solanaKey; // TEXT
private String login;
private String deviceKey;
private String solanaKey;
public SolanaUserEntry() {}
@ -54,32 +34,22 @@ public class SolanaUserEntry {
public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; }
/** Публичный ключ устройства (device key). */
public String getDeviceKey() { return deviceKey; }
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
public String getSolanaKey() { return solanaKey; }
public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }
/**
* Device key в байтах (32 байта) или null, если ключ битый/пустой.
*
* Поддержка форматов:
* - Base64 (предпочтительно)
* - HEX (ровно 64 hex-символа, без пробелов)
*/
public byte[] getDeviceKeyByte() {
if (deviceKey == null) return null;
String s = deviceKey.trim();
if (s.isEmpty()) return null;
// 1) пробуем Base64
try {
byte[] b = Base64.getDecoder().decode(s);
if (b != null && b.length == 32) return b;
} catch (IllegalArgumentException ignore) {}
// 2) пробуем HEX (64 символа)
if (s.length() == 64 && s.matches("^[0-9a-fA-F]+$")) {
byte[] out = new byte[32];
for (int i = 0; i < 32; i++) {

View File

@ -10,12 +10,6 @@ package shine.db.entities;
* - value TEXT NOT NULL
* - device_key TEXT NULL
* - signature TEXT NULL
*
* UNIQUE(login, param)
*
* Смысл:
* - в таблице всегда хранится "последнее" значение параметра по времени.
* - time_ms монотонно растёт для каждого (login,param) сервер не принимает более старые обновления.
*/
public class UserParamEntry {
@ -24,8 +18,8 @@ public class UserParamEntry {
private long timeMs;
private String value;
private String deviceKey; // base64(32) можно хранить как "каким ключом подписано"
private String signature; // base64(64)
private String deviceKey;
private String signature;
public UserParamEntry() {}