28 01 25
Добавил запрос связей пользователя - друзей - для построения графа друзей И тест добавил. но тест пока не весь проходит
This commit is contained in:
parent
22fb35d1d4
commit
922c18db4b
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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_UpsertUserParam_Request;
|
||||
|
||||
// !!! подставь реальные пакеты/имена, как у тебя в проекте:
|
||||
// --- subscriptions ---
|
||||
//import server.logic.ws_protocol.JSON.handlers.subscriptions.Net_GetSubscribedChannels_Handler;
|
||||
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;
|
||||
|
||||
/**
|
||||
@ -54,7 +58,6 @@ import java.util.Map;
|
||||
*/
|
||||
public final class JsonHandlerRegistry {
|
||||
|
||||
// Map.of(...) поддерживает максимум 10 пар => используем Map.ofEntries(...)
|
||||
private static final Map<String, JsonMessageHandler> HANDLERS = Map.ofEntries(
|
||||
Map.entry("AddUser", new Net_AddUser_Handler()),
|
||||
Map.entry("GetUser", new Net_GetUser_Handler()),
|
||||
@ -76,7 +79,10 @@ public final class JsonHandlerRegistry {
|
||||
// --- userParams ---
|
||||
Map.entry("UpsertUserParam", new Net_UpsertUserParam_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 ---
|
||||
// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
|
||||
@ -106,7 +112,10 @@ public final class JsonHandlerRegistry {
|
||||
Map.entry("ListUserParams", Net_ListUserParams_Request.class),
|
||||
|
||||
// --- 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() { }
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -4,6 +4,7 @@ import test.it.cases.IT_01_AddUser;
|
||||
import test.it.cases.IT_02_Sessions;
|
||||
import test.it.cases.IT_03_AddBlock_NoAuth;
|
||||
import test.it.cases.IT_04_UserParams_NoAuth;
|
||||
import test.it.cases.IT_05_Connections_NoAuth;
|
||||
import test.it.utils.log.TestLog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -48,6 +49,9 @@ public class IT_RunAllMain {
|
||||
String s4 = IT_04_UserParams_NoAuth.run(); summaries.add(s4);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -75,6 +75,21 @@ public final class JsonBuilders {
|
||||
""".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 ----------------
|
||||
|
||||
public static String authChallenge(String login) {
|
||||
|
||||
@ -166,6 +166,42 @@ public final class JsonParsers {
|
||||
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) {
|
||||
try {
|
||||
JsonNode root = MAPPER.readTree(json);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user