Добавил запрос связей пользователя - друзей - для построения графа друзей

И тест добавил.

но тест пока не весь проходит
This commit is contained in:
AidarKC 2026-01-28 22:31:20 +03:00
parent 22fb35d1d4
commit 922c18db4b
8 changed files with 385 additions and 5 deletions

View File

@ -0,0 +1,128 @@
package shine.db.dao;
import shine.db.SqliteDbController;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* ConnectionsStateDAO чтение текущего состояния связей из connections_state.
*
* ВАЖНО:
* - login в запросах может быть в любом регистре, поэтому в WHERE используем COLLATE NOCASE
* - в ответах возвращаем логины в каноническом регистре через JOIN на solana_users
*
* ПРИМЕЧАНИЕ:
* Таблица пользователей тут названа "solana_users". Если у тебя иначе поменяй в SQL.
*/
public final class ConnectionsStateDAO {
private static volatile ConnectionsStateDAO instance;
private final SqliteDbController db = SqliteDbController.getInstance();
private ConnectionsStateDAO() {}
public static ConnectionsStateDAO getInstance() {
if (instance == null) {
synchronized (ConnectionsStateDAO.class) {
if (instance == null) instance = new ConnectionsStateDAO();
}
}
return instance;
}
/**
* Outgoing: список логинов (канонических), кому login поставил relType.
*/
public List<String> listOutgoingByRelTypeCanonical(Connection c, String loginAnyCase, int relType) throws SQLException {
String sql = """
SELECT u.login AS friend_login
FROM connections_state cs
JOIN solana_users u
ON u.login = cs.to_login COLLATE NOCASE
WHERE cs.login = ? COLLATE NOCASE
AND cs.rel_type = ?
ORDER BY u.login
""";
List<String> out = new ArrayList<>();
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, loginAnyCase);
ps.setInt(2, relType);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
String v = rs.getString("friend_login");
if (v != null) out.add(v);
}
}
}
return out;
}
/**
* Incoming: список логинов (канонических), кто поставил relType пользователю login.
*/
public List<String> listIncomingByRelTypeCanonical(Connection c, String loginAnyCase, int relType) throws SQLException {
String sql = """
SELECT u.login AS friend_login
FROM connections_state cs
JOIN solana_users u
ON u.login = cs.login COLLATE NOCASE
WHERE cs.to_login = ? COLLATE NOCASE
AND cs.rel_type = ?
ORDER BY u.login
""";
List<String> out = new ArrayList<>();
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, loginAnyCase);
ps.setInt(2, relType);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
String v = rs.getString("friend_login");
if (v != null) out.add(v);
}
}
}
return out;
}
/**
* Mutual: список логинов (канонических), у кого дружба в обе стороны.
*/
public List<String> listMutualByRelTypeCanonical(Connection c, String loginAnyCase, int relType) throws SQLException {
String sql = """
SELECT u.login AS friend_login
FROM connections_state a
JOIN solana_users u
ON u.login = a.to_login COLLATE NOCASE
WHERE a.login = ? COLLATE NOCASE
AND a.rel_type = ?
AND EXISTS (
SELECT 1
FROM connections_state b
WHERE b.login = a.to_login COLLATE NOCASE
AND b.to_login = a.login COLLATE NOCASE
AND b.rel_type = a.rel_type
)
ORDER BY u.login
""";
List<String> out = new ArrayList<>();
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, loginAnyCase);
ps.setInt(2, relType);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
String v = rs.getString("friend_login");
if (v != null) out.add(v);
}
}
}
return out;
}
}

View File

@ -42,10 +42,14 @@ import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserPar
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request; import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request; import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;
// !!! подставь реальные пакеты/имена, как у тебя в проекте: // --- subscriptions ---
//import server.logic.ws_protocol.JSON.handlers.subscriptions.Net_GetSubscribedChannels_Handler; //import server.logic.ws_protocol.JSON.handlers.subscriptions.Net_GetSubscribedChannels_Handler;
import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Request; import server.logic.ws_protocol.JSON.handlers.subscriptions.entyties.Net_GetSubscribedChannels_Request;
// --- NEW: connections friends lists ---
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetFriendsLists_Handler;
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;
import java.util.Map; import java.util.Map;
/** /**
@ -54,7 +58,6 @@ import java.util.Map;
*/ */
public final class JsonHandlerRegistry { public final class JsonHandlerRegistry {
// Map.of(...) поддерживает максимум 10 пар => используем Map.ofEntries(...)
private static final Map<String, JsonMessageHandler> HANDLERS = Map.ofEntries( private static final Map<String, JsonMessageHandler> HANDLERS = Map.ofEntries(
Map.entry("AddUser", new Net_AddUser_Handler()), Map.entry("AddUser", new Net_AddUser_Handler()),
Map.entry("GetUser", new Net_GetUser_Handler()), Map.entry("GetUser", new Net_GetUser_Handler()),
@ -76,7 +79,10 @@ public final class JsonHandlerRegistry {
// --- userParams --- // --- userParams ---
Map.entry("UpsertUserParam", new Net_UpsertUserParam_Handler()), Map.entry("UpsertUserParam", new Net_UpsertUserParam_Handler()),
Map.entry("GetUserParam", new Net_GetUserParam_Handler()), Map.entry("GetUserParam", new Net_GetUserParam_Handler()),
Map.entry("ListUserParams", new Net_ListUserParams_Handler()) Map.entry("ListUserParams", new Net_ListUserParams_Handler()),
// --- connections ---
Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler())
// --- subscriptions --- // --- subscriptions ---
// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler()) // Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
@ -106,7 +112,10 @@ public final class JsonHandlerRegistry {
Map.entry("ListUserParams", Net_ListUserParams_Request.class), Map.entry("ListUserParams", Net_ListUserParams_Request.class),
// --- subscriptions --- // --- subscriptions ---
Map.entry("ListSubscribedChannels", Net_GetSubscribedChannels_Request.class) Map.entry("ListSubscribedChannels", Net_GetSubscribedChannels_Request.class),
// --- connections ---
Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class)
); );
private JsonHandlerRegistry() { } private JsonHandlerRegistry() { }

View File

@ -0,0 +1,117 @@
package server.logic.ws_protocol.JSON.handlers.connections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.MsgSubType;
import shine.db.SqliteDbController;
import shine.db.dao.ConnectionsStateDAO;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.List;
/**
* GetFriendsLists получить 3 списка (для того что бы построить граф связей друзей пользователя):
* - out_friends: кому login поставил FRIEND
* - in_friends: кто поставил FRIEND этому login
* - mutual_friends: FRIEND в обе стороны
*
* ВАЖНО:
* - login в запросе может быть любым регистром
* - в ответе возвращаем канонический регистр (как в solana_users.login)
*
* ПРИМЕЧАНИЕ:
* Таблица пользователей тут названа "solana_users". Если у тебя иначе поменяй SQL.
*/
public class Net_GetFriendsLists_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_GetFriendsLists_Handler.class);
@Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
Net_GetFriendsLists_Request req = (Net_GetFriendsLists_Request) baseRequest;
if (req.getLogin() == null || req.getLogin().isBlank()) {
return NetExceptionResponseFactory.error(
req,
WireCodes.Status.BAD_REQUEST,
"BAD_FIELDS",
"Некорректные поля: login"
);
}
final String loginAnyCase = req.getLogin().trim();
try {
SqliteDbController db = SqliteDbController.getInstance();
ConnectionsStateDAO dao = ConnectionsStateDAO.getInstance();
try (Connection c = db.getConnection()) {
// 1) Канонизируем login через solana_users (NOCASE)
String canonicalLogin = findCanonicalLogin(c, loginAnyCase);
if (canonicalLogin == null) {
return NetExceptionResponseFactory.error(
req,
404,
"USER_NOT_FOUND",
"Пользователь не найден"
);
}
int relType = (int) MsgSubType.CONNECTION_FRIEND;
// 2) Три списка (все логины канонические)
List<String> outFriends = dao.listOutgoingByRelTypeCanonical(c, canonicalLogin, relType);
List<String> inFriends = dao.listIncomingByRelTypeCanonical(c, canonicalLogin, relType);
List<String> mutual = dao.listMutualByRelTypeCanonical(c, canonicalLogin, relType);
Net_GetFriendsLists_Response resp = new Net_GetFriendsLists_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
resp.setLogin(canonicalLogin);
resp.setOut_friends(outFriends);
resp.setIn_friends(inFriends);
resp.setMutual_friends(mutual);
return resp;
}
} catch (Exception e) {
log.error("❌ Internal error GetFriendsLists", e);
return NetExceptionResponseFactory.error(
req,
WireCodes.Status.INTERNAL_ERROR,
"INTERNAL_ERROR",
"Внутренняя ошибка сервера"
);
}
}
private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
String sql = """
SELECT login
FROM solana_users
WHERE login = ? COLLATE NOCASE
LIMIT 1
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, loginAnyCase);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return null;
return rs.getString("login");
}
}
}
}

View File

@ -0,0 +1,29 @@
package server.logic.ws_protocol.JSON.handlers.connections.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
/**
* Запрос GetFriendsLists получить два списка "друзей" по connections_state.
*
* {
* "op": "GetFriendsLists",
* "requestId": "req-100",
* "payload": {
* "login": "anya"
* }
* }
*
* Возвращает:
* - out_friends: кому login поставил FRIEND
* - in_friends: кто поставил FRIEND этому login
*
* ПРО ДОСТУП (на будущее):
* Сейчас (MVP) без ограничений. Позже можно ограничить видимость связей.
*/
public class Net_GetFriendsLists_Request extends Net_Request {
private String login;
public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; }
}

View File

@ -0,0 +1,42 @@
package server.logic.ws_protocol.JSON.handlers.connections.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import java.util.ArrayList;
import java.util.List;
/**
* Ответ GetFriendsLists.
*
* {
* "op": "GetFriendsLists",
* "requestId": "req-100",
* "status": 200,
* "payload": {
* "login": "Anya", // канонический регистр из БД
* "out_friends": ["Bob", "Kate"], // кому login поставил FRIEND
* "in_friends": ["Alex", "Kate"], // кто поставил FRIEND login
* "mutual_friends": ["Kate"] // взаимные (два направления)
* }
* }
*/
public class Net_GetFriendsLists_Response extends Net_Response {
private String login;
private List<String> out_friends = new ArrayList<>();
private List<String> in_friends = new ArrayList<>();
private List<String> mutual_friends = new ArrayList<>();
public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; }
public List<String> getOut_friends() { return out_friends; }
public void setOut_friends(List<String> out_friends) { this.out_friends = out_friends; }
public List<String> getIn_friends() { return in_friends; }
public void setIn_friends(List<String> in_friends) { this.in_friends = in_friends; }
public List<String> getMutual_friends() { return mutual_friends; }
public void setMutual_friends(List<String> mutual_friends) { this.mutual_friends = mutual_friends; }
}

View File

@ -4,6 +4,7 @@ import test.it.cases.IT_01_AddUser;
import test.it.cases.IT_02_Sessions; import test.it.cases.IT_02_Sessions;
import test.it.cases.IT_03_AddBlock_NoAuth; import test.it.cases.IT_03_AddBlock_NoAuth;
import test.it.cases.IT_04_UserParams_NoAuth; import test.it.cases.IT_04_UserParams_NoAuth;
import test.it.cases.IT_05_Connections_NoAuth;
import test.it.utils.log.TestLog; import test.it.utils.log.TestLog;
import java.util.ArrayList; import java.util.ArrayList;
@ -48,6 +49,9 @@ public class IT_RunAllMain {
String s4 = IT_04_UserParams_NoAuth.run(); summaries.add(s4); String s4 = IT_04_UserParams_NoAuth.run(); summaries.add(s4);
if (s4.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); } if (s4.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); }
String s5 = IT_05_Connections_NoAuth.run(); summaries.add(s5);
if (s5.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); }
return finish(summaries, failed); return finish(summaries, failed);
} }
@ -65,4 +69,4 @@ public class IT_RunAllMain {
return failed; return failed;
} }
} }

View File

@ -75,6 +75,21 @@ public final class JsonBuilders {
""".formatted(requestId, prefix); """.formatted(requestId, prefix);
} }
// ---------------- GetFriendsLists ----------------
public static String getFriendsLists(String login) {
String requestId = TestIds.next("friends");
return """
{
"op": "GetFriendsLists",
"requestId": "%s",
"payload": {
"login": "%s"
}
}
""".formatted(requestId, login);
}
// ---------------- AuthChallenge ---------------- // ---------------- AuthChallenge ----------------
public static String authChallenge(String login) { public static String authChallenge(String login) {

View File

@ -166,6 +166,42 @@ public final class JsonParsers {
return res; return res;
} }
// ---------------- Friends helpers ----------------
/** payload.login (канонический) */
public static String friendsLogin(String json) {
return getPayloadText(json, "login");
}
public static List<String> friendsOut(String json) {
return getPayloadStringArray(json, "out_friends");
}
public static List<String> friendsIn(String json) {
return getPayloadStringArray(json, "in_friends");
}
public static List<String> friendsMutual(String json) {
return getPayloadStringArray(json, "mutual_friends");
}
private static List<String> getPayloadStringArray(String json, String field) {
List<String> res = new ArrayList<>();
try {
JsonNode root = MAPPER.readTree(json);
JsonNode payload = root.get("payload");
if (payload == null) return res;
JsonNode arr = payload.get(field);
if (arr == null || !arr.isArray()) return res;
for (JsonNode x : arr) {
if (x != null && !x.isNull()) res.add(x.asText());
}
} catch (Exception ignored) {}
return res;
}
private static String getPayloadText(String json, String field) { private static String getPayloadText(String json, String field) {
try { try {
JsonNode root = MAPPER.readTree(json); JsonNode root = MAPPER.readTree(json);