From 44133d3667cdf4586986a0d09779666ebb9f911c Mon Sep 17 00:00:00 2001 From: merlin Date: Thu, 11 Dec 2025 10:37:03 +0800 Subject: [PATCH] feat: introduce middleware rabbitmq and redis --- pom.xml | 9 ++- .../config/RabbitMQConfig.java | 16 +++++ .../myplayerbackend/config/RedisConfig.java | 24 ++++++++ .../config/security/WebsocketInterceptor.java | 4 ++ .../controller/FriendController.java | 29 +++++++--- .../controller/GroupController.java | 5 +- .../controller/InvitingController.java | 5 +- .../service/OnlineStatusService.java | 45 ++++++++++++++ .../service/impl/FriendsServiceImpl.java | 22 +++++++ .../service/impl/GroupServiceImpl.java | 1 - .../service/impl/InvitingServiceImpl.java | 20 +++---- .../service/impl/PlayroomServiceImpl.java | 1 - .../websocket/CustomWebSocketHandler.java | 58 ++++++++++++++----- .../websocket/WebSocketMessageConsumer.java | 42 ++++++++++++++ .../websocket/WebSocketSessionManager.java | 8 +++ .../websocket/command/BaseCommandHandler.java | 10 ++++ .../websocket/command/CommandDispatcher.java | 34 +++++++++++ .../websocket/command/impl/Heartbeat.java | 22 +++++++ .../utils/websocket/command/impl/Message.java | 23 ++++++++ .../utils/websocket/command/impl/Ping.java | 24 ++++++++ .../websocket/command/impl/SystemNotify.java | 22 +++++++ .../utils/websocket/command/impl/Typing.java | 22 +++++++ 22 files changed, 404 insertions(+), 42 deletions(-) create mode 100644 src/main/java/xin/merlin/myplayerbackend/config/RabbitMQConfig.java create mode 100644 src/main/java/xin/merlin/myplayerbackend/config/RedisConfig.java create mode 100644 src/main/java/xin/merlin/myplayerbackend/service/OnlineStatusService.java create mode 100644 src/main/java/xin/merlin/myplayerbackend/utils/websocket/WebSocketMessageConsumer.java create mode 100644 src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/BaseCommandHandler.java create mode 100644 src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/CommandDispatcher.java create mode 100644 src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Heartbeat.java create mode 100644 src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Message.java create mode 100644 src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Ping.java create mode 100644 src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/SystemNotify.java create mode 100644 src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Typing.java diff --git a/pom.xml b/pom.xml index 50166df..bebc569 100644 --- a/pom.xml +++ b/pom.xml @@ -38,9 +38,14 @@ spring-boot-starter-websocket - org.springframework.kafka - spring-kafka + org.springframework.boot + spring-boot-starter-data-redis + + org.springframework.boot + spring-boot-starter-amqp + + org.springframework.boot diff --git a/src/main/java/xin/merlin/myplayerbackend/config/RabbitMQConfig.java b/src/main/java/xin/merlin/myplayerbackend/config/RabbitMQConfig.java new file mode 100644 index 0000000..9df43ba --- /dev/null +++ b/src/main/java/xin/merlin/myplayerbackend/config/RabbitMQConfig.java @@ -0,0 +1,16 @@ +package xin.merlin.myplayerbackend.config; + +import org.springframework.amqp.core.Queue; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMQConfig { + + public static final String WS_MESSAGE_QUEUE = "ws.message"; + + @Bean + public Queue wsMessageQueue() { + return new Queue(WS_MESSAGE_QUEUE, true); // 持久化队列 + } +} diff --git a/src/main/java/xin/merlin/myplayerbackend/config/RedisConfig.java b/src/main/java/xin/merlin/myplayerbackend/config/RedisConfig.java new file mode 100644 index 0000000..b1f49c0 --- /dev/null +++ b/src/main/java/xin/merlin/myplayerbackend/config/RedisConfig.java @@ -0,0 +1,24 @@ +package xin.merlin.myplayerbackend.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(factory); + + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + + return template; + } +} + diff --git a/src/main/java/xin/merlin/myplayerbackend/config/security/WebsocketInterceptor.java b/src/main/java/xin/merlin/myplayerbackend/config/security/WebsocketInterceptor.java index 6bd9d5d..5ef82af 100644 --- a/src/main/java/xin/merlin/myplayerbackend/config/security/WebsocketInterceptor.java +++ b/src/main/java/xin/merlin/myplayerbackend/config/security/WebsocketInterceptor.java @@ -26,6 +26,10 @@ public class WebsocketInterceptor implements HandshakeInterceptor { String token = request.getHeaders().getFirst("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); + if (jwtUtil.isTokenExpired(token)){ + log.info("token expired"); + return false; + } } else { return false; } diff --git a/src/main/java/xin/merlin/myplayerbackend/controller/FriendController.java b/src/main/java/xin/merlin/myplayerbackend/controller/FriendController.java index b51c4e6..23275f4 100644 --- a/src/main/java/xin/merlin/myplayerbackend/controller/FriendController.java +++ b/src/main/java/xin/merlin/myplayerbackend/controller/FriendController.java @@ -5,14 +5,14 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import xin.merlin.myplayerbackend.entity.Friends; -import xin.merlin.myplayerbackend.entity.UserInfo; import xin.merlin.myplayerbackend.service.impl.FriendsServiceImpl; import xin.merlin.myplayerbackend.service.impl.InvitingServiceImpl; import xin.merlin.myplayerbackend.utils.JwtUtil; import xin.merlin.myplayerbackend.utils.result.Response; import xin.merlin.myplayerbackend.utils.result.ResultCode; -import static com.baomidou.mybatisplus.extension.ddl.DdlScriptErrorHandler.PrintlnLogErrorHandler.log; +import java.util.List; + @Slf4j @RequestMapping("/friend") @@ -54,14 +54,27 @@ public class FriendController { @PostMapping("/nickname") Response nickname(@RequestHeader("Authorization")String token, @RequestBody Friends friends) { - token = token.substring(7); - Integer id = jwtUtil.getId(token); - if (!id.equals(friends.getId())) return Response.success(ResultCode.SERVER_ERROR); - friendsService.saveOrUpdate(friends); - return Response.success(ResultCode.SUCCESS); + try { + token = token.substring(7); + Integer id = jwtUtil.getId(token); + if (!id.equals(friends.getId())) return Response.success(ResultCode.SERVER_ERROR); + friendsService.saveOrUpdate(friends); + return Response.success(ResultCode.SUCCESS); + } catch (Exception e) { + log.error(e.getMessage()); + return Response.fail(ResultCode.SERVER_ERROR); + } + } + @PostMapping("/status") + Response status(@RequestBody List friends) { + try { + return Response.success(ResultCode.SUCCESS,friendsService.status(friends)); + } catch (Exception e) { + log.error(e.getMessage()); + return Response.fail(ResultCode.SERVER_ERROR); + } } - } diff --git a/src/main/java/xin/merlin/myplayerbackend/controller/GroupController.java b/src/main/java/xin/merlin/myplayerbackend/controller/GroupController.java index 1976b74..5efd46b 100644 --- a/src/main/java/xin/merlin/myplayerbackend/controller/GroupController.java +++ b/src/main/java/xin/merlin/myplayerbackend/controller/GroupController.java @@ -6,7 +6,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import xin.merlin.myplayerbackend.entity.GroupInfo; import xin.merlin.myplayerbackend.entity.Groups; -import xin.merlin.myplayerbackend.entity.Playrooms; import xin.merlin.myplayerbackend.entity.UserInfo; import xin.merlin.myplayerbackend.service.impl.GroupServiceImpl; import xin.merlin.myplayerbackend.service.impl.GroupsServiceImpl; @@ -56,7 +55,7 @@ public class GroupController { } @PostMapping("/search") - Response searchGroup(@RequestHeader("Authorization")String token, @RequestBody GroupInfo groupInfo){ + Response searchGroup(@RequestBody GroupInfo groupInfo){ // TODO:视情况开放api参数currentPage和 pageSize try { Integer currentPage = 1; @@ -113,7 +112,7 @@ public class GroupController { token = token.substring(7); Integer id = jwtUtil.getId(token); if(!isAdmin(id,g_id)) return Response.success(ResultCode.ACCOUNT_PERMISSION_DENY); - if(!groupService.leaveGroup(id,g_id)) return Response.success(ResultCode.GROUP_USER_NOT_EXISTED); + if(!groupService.leaveGroup(userInfo.getId(),g_id)) return Response.success(ResultCode.GROUP_USER_NOT_EXISTED); return Response.success(ResultCode.SUCCESS); } catch (Exception e) { log.error(e.getMessage()); diff --git a/src/main/java/xin/merlin/myplayerbackend/controller/InvitingController.java b/src/main/java/xin/merlin/myplayerbackend/controller/InvitingController.java index 19647a2..bc18ec0 100644 --- a/src/main/java/xin/merlin/myplayerbackend/controller/InvitingController.java +++ b/src/main/java/xin/merlin/myplayerbackend/controller/InvitingController.java @@ -5,6 +5,7 @@ import org.springframework.web.bind.annotation.*; import xin.merlin.myplayerbackend.entity.Inviting; import xin.merlin.myplayerbackend.entity.UserInfo; import xin.merlin.myplayerbackend.service.impl.InvitingServiceImpl; +import xin.merlin.myplayerbackend.service.impl.PlayroomsServiceImpl; import xin.merlin.myplayerbackend.utils.JwtUtil; import xin.merlin.myplayerbackend.utils.result.Response; import xin.merlin.myplayerbackend.utils.result.ResultCode; @@ -20,11 +21,13 @@ public class InvitingController { private final InvitingServiceImpl invitingService; + private final PlayroomsServiceImpl playroomsService; + private final JwtUtil jwtUtil; // playroom鉴权 private Boolean isAdmin(Integer id,Integer r_id){ - return invitingService.playroomIsAdmin(id,r_id)==0; + return playroomsService.playroomIsAdmin(id,r_id)==0; } @PostMapping("/friends") diff --git a/src/main/java/xin/merlin/myplayerbackend/service/OnlineStatusService.java b/src/main/java/xin/merlin/myplayerbackend/service/OnlineStatusService.java new file mode 100644 index 0000000..b5b7e33 --- /dev/null +++ b/src/main/java/xin/merlin/myplayerbackend/service/OnlineStatusService.java @@ -0,0 +1,45 @@ +package xin.merlin.myplayerbackend.service; + +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class OnlineStatusService { + + private static final String ONLINE_USERS_KEY = "online:users"; + + private final StringRedisTemplate redis; + + /** 用户上线 */ + public void online(Integer uid) { + redis.opsForSet().add(ONLINE_USERS_KEY, uid.toString()); + } + + /** 用户下线 */ + public void offline(Integer uid) { + redis.opsForSet().remove(ONLINE_USERS_KEY, uid.toString()); + } + + /** 是否在线 */ + public boolean isOnline(Integer uid) { + return Boolean.TRUE.equals(redis.opsForSet().isMember(ONLINE_USERS_KEY, uid.toString())); + } + + /** 获取所有在线用户 */ + public Set getOnlineUsers() { + return redis.opsForSet().members(ONLINE_USERS_KEY); + } + + @PreDestroy + public void cleanup() { + // 清空在线状态 + redis.delete(ONLINE_USERS_KEY); + } + +} + diff --git a/src/main/java/xin/merlin/myplayerbackend/service/impl/FriendsServiceImpl.java b/src/main/java/xin/merlin/myplayerbackend/service/impl/FriendsServiceImpl.java index dc8ff0e..4b5e56e 100644 --- a/src/main/java/xin/merlin/myplayerbackend/service/impl/FriendsServiceImpl.java +++ b/src/main/java/xin/merlin/myplayerbackend/service/impl/FriendsServiceImpl.java @@ -11,9 +11,12 @@ import xin.merlin.myplayerbackend.entity.UserInfo; import xin.merlin.myplayerbackend.entity.http.Friend; import xin.merlin.myplayerbackend.mapper.FriendsMapper; import xin.merlin.myplayerbackend.mapper.UserMapper; +import xin.merlin.myplayerbackend.service.OnlineStatusService; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Service @RequiredArgsConstructor @@ -23,6 +26,8 @@ public class FriendsServiceImpl extends ServiceImpl { private final UserMapper userMapper; + private final OnlineStatusService onlineStatusService; + public List getFriends(Integer id, Integer size, Integer page) { List friends = friendsMapper.selectList(new Page<>(page,size),Wrappers.lambdaQuery().eq(Friends::getId, id)); @@ -44,4 +49,21 @@ public class FriendsServiceImpl extends ServiceImpl { throw new RuntimeException(e); } } + + public Map status(List friends){ + try { + Map map = new HashMap<>(); + for (Integer f : friends) { + if(onlineStatusService.isOnline(f)){ + map.put(f,1); + }else { + map.put(f,0); + } + } + return map; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } diff --git a/src/main/java/xin/merlin/myplayerbackend/service/impl/GroupServiceImpl.java b/src/main/java/xin/merlin/myplayerbackend/service/impl/GroupServiceImpl.java index 3897fc3..8285a1b 100644 --- a/src/main/java/xin/merlin/myplayerbackend/service/impl/GroupServiceImpl.java +++ b/src/main/java/xin/merlin/myplayerbackend/service/impl/GroupServiceImpl.java @@ -8,7 +8,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import xin.merlin.myplayerbackend.entity.*; import xin.merlin.myplayerbackend.entity.http.GroupDetails; -import xin.merlin.myplayerbackend.entity.http.PlayroomDetails; import xin.merlin.myplayerbackend.mapper.GroupMapper; import xin.merlin.myplayerbackend.mapper.GroupsMapper; import xin.merlin.myplayerbackend.mapper.UserMapper; diff --git a/src/main/java/xin/merlin/myplayerbackend/service/impl/InvitingServiceImpl.java b/src/main/java/xin/merlin/myplayerbackend/service/impl/InvitingServiceImpl.java index 12e041e..118603e 100644 --- a/src/main/java/xin/merlin/myplayerbackend/service/impl/InvitingServiceImpl.java +++ b/src/main/java/xin/merlin/myplayerbackend/service/impl/InvitingServiceImpl.java @@ -94,16 +94,16 @@ public class InvitingServiceImpl extends ServiceImpl { } - public Integer playroomIsAdmin(Integer id, Integer r_id) { - try { - Playrooms playrooms = playroomsMapper.selectOne(Wrappers.lambdaQuery().eq(Playrooms::getId,id).eq(Playrooms::getR_id,r_id)); - if (playrooms == null) return 1; - else return playrooms.getRole(); - } catch (Exception e) { - log.error(e.getMessage()); - throw new RuntimeException(e); - } - } +// public Integer playroomIsAdmin(Integer id, Integer r_id) { +// try { +// Playrooms playrooms = playroomsMapper.selectOne(Wrappers.lambdaQuery().eq(Playrooms::getId,id).eq(Playrooms::getR_id,r_id)); +// if (playrooms == null) return 1; +// else return playrooms.getRole(); +// } catch (Exception e) { +// log.error(e.getMessage()); +// throw new RuntimeException(e); +// } +// } public Boolean handlePlayroomInviting(Inviting inviting) { try { diff --git a/src/main/java/xin/merlin/myplayerbackend/service/impl/PlayroomServiceImpl.java b/src/main/java/xin/merlin/myplayerbackend/service/impl/PlayroomServiceImpl.java index b9bc17e..b00b808 100644 --- a/src/main/java/xin/merlin/myplayerbackend/service/impl/PlayroomServiceImpl.java +++ b/src/main/java/xin/merlin/myplayerbackend/service/impl/PlayroomServiceImpl.java @@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.RequiredArgsConstructor; -import org.apache.catalina.User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import xin.merlin.myplayerbackend.entity.PlayroomInfo; diff --git a/src/main/java/xin/merlin/myplayerbackend/utils/websocket/CustomWebSocketHandler.java b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/CustomWebSocketHandler.java index 4ece913..9a3c7ce 100644 --- a/src/main/java/xin/merlin/myplayerbackend/utils/websocket/CustomWebSocketHandler.java +++ b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/CustomWebSocketHandler.java @@ -2,13 +2,19 @@ package xin.merlin.myplayerbackend.utils.websocket; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; +import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; +import xin.merlin.myplayerbackend.config.RabbitMQConfig; +import xin.merlin.myplayerbackend.service.OnlineStatusService; @Slf4j @@ -18,9 +24,16 @@ public class CustomWebSocketHandler extends TextWebSocketHandler { private final WebSocketSessionManager webSocketSessionManager; + private final RabbitTemplate rabbitTemplate; + + private final OnlineStatusService onlineStatusService; + @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { Integer userId = (Integer) session.getAttributes().get("userId"); + + onlineStatusService.online(userId); + webSocketSessionManager.addSession(userId, session); log.info("用户 {} 已连接", userId); } @@ -28,29 +41,42 @@ public class CustomWebSocketHandler extends TextWebSocketHandler { @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload = message.getPayload(); - JSONObject msg = JSON.parseObject(payload); - String type = msg.getString("type"); - switch (type) { - case "chat": - Integer toUser = msg.getInteger("toUser"); - webSocketSessionManager.sendToUser(toUser, payload); - break; + rabbitTemplate.convertAndSend(RabbitMQConfig.WS_MESSAGE_QUEUE, payload); - case "broadcast": - webSocketSessionManager.broadcast(payload); - break; - - case "signal": - webSocketSessionManager.sendToUser(msg.getInteger("toUser"), payload); - break; - - } +// JSONObject msg = JSON.parseObject(payload); +// String type = msg.getString("type"); +// switch (type) { +// case "chat": +// Integer toUser = msg.getInteger("toUser"); +// webSocketSessionManager.sendToUser(toUser, payload); +// break; +// +// case "broadcast": +// webSocketSessionManager.broadcast(payload); +// break; +// +// case "signal": +// webSocketSessionManager.sendToUser(msg.getInteger("toUser"), payload); +// break; +// +// } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { Integer userId = (Integer) session.getAttributes().get("userId"); + + onlineStatusService.offline(userId); + webSocketSessionManager.removeSession(userId); log.info("用户 {} 已断开", userId); } + + @EventListener + public void onShutdown(ContextClosedEvent event) { + log.info("Shutting down... closing websocket sessions"); + webSocketSessionManager.closeAll(); + } + + } diff --git a/src/main/java/xin/merlin/myplayerbackend/utils/websocket/WebSocketMessageConsumer.java b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/WebSocketMessageConsumer.java new file mode 100644 index 0000000..c99fb3b --- /dev/null +++ b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/WebSocketMessageConsumer.java @@ -0,0 +1,42 @@ +package xin.merlin.myplayerbackend.utils.websocket; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; +import xin.merlin.myplayerbackend.config.RabbitMQConfig; +import xin.merlin.myplayerbackend.utils.websocket.command.CommandDispatcher; + +@Slf4j +@Component +@RequiredArgsConstructor +public class WebSocketMessageConsumer { + + private final WebSocketSessionManager sessionManager; + + private final CommandDispatcher commandDispatcher; + + @RabbitListener(queues = RabbitMQConfig.WS_MESSAGE_QUEUE) + public void onMessage(String json) { + try { + JSONObject msg = JSON.parseObject(json); + + String to = msg.getString("to"); + + if (to == null || to.isEmpty()) { + // 这是广播 / 系统消息 + sessionManager.broadcast(json); + } else { + // 单发消息 +// sessionManager.sendToUser(Integer.valueOf(to), json); + commandDispatcher.dispatch(msg); + } + + } catch (Exception e) { + log.info(e.getMessage()); + } + } +} + diff --git a/src/main/java/xin/merlin/myplayerbackend/utils/websocket/WebSocketSessionManager.java b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/WebSocketSessionManager.java index aca924b..1ec73ed 100644 --- a/src/main/java/xin/merlin/myplayerbackend/utils/websocket/WebSocketSessionManager.java +++ b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/WebSocketSessionManager.java @@ -2,6 +2,7 @@ package xin.merlin.myplayerbackend.utils.websocket; import org.springframework.stereotype.Component; +import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; @@ -37,5 +38,12 @@ public class WebSocketSessionManager { } } + public void closeAll() { + websocketSessions.forEach((uid, session) -> { + try { + session.close(CloseStatus.GOING_AWAY); + } catch (Exception ignored) {} + }); + } } diff --git a/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/BaseCommandHandler.java b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/BaseCommandHandler.java new file mode 100644 index 0000000..f67122a --- /dev/null +++ b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/BaseCommandHandler.java @@ -0,0 +1,10 @@ +package xin.merlin.myplayerbackend.utils.websocket.command; + +import com.alibaba.fastjson2.JSONObject; + +import java.io.IOException; + +public interface BaseCommandHandler { + void handle(JSONObject msg) throws IOException; +} + diff --git a/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/CommandDispatcher.java b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/CommandDispatcher.java new file mode 100644 index 0000000..fa979cd --- /dev/null +++ b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/CommandDispatcher.java @@ -0,0 +1,34 @@ +package xin.merlin.myplayerbackend.utils.websocket.command; + +import com.alibaba.fastjson2.JSONObject; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import xin.merlin.myplayerbackend.utils.websocket.command.impl.*; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class CommandDispatcher { + private final Ping pingCommand; + private final Message messageCommand; + private final Typing typingCommand; + private final Heartbeat heartbeatCommand; + private final SystemNotify systemNotifyCommand; + + public void dispatch(JSONObject msg) throws IOException { + String cmd = msg.getString("cmd"); + + switch (cmd) { + case "PING" -> pingCommand.handle(msg); + case "MESSAGE" -> messageCommand.handle(msg); + case "TYPING" -> typingCommand.handle(msg); + case "HEARTBEAT" -> heartbeatCommand.handle(msg); + case "SYSTEM_NOTIFY" -> systemNotifyCommand.handle(msg); + + default -> { + System.err.println("Unknown command: " + cmd); + } + } + } +} diff --git a/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Heartbeat.java b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Heartbeat.java new file mode 100644 index 0000000..d84ec53 --- /dev/null +++ b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Heartbeat.java @@ -0,0 +1,22 @@ +package xin.merlin.myplayerbackend.utils.websocket.command.impl; + +import com.alibaba.fastjson2.JSONObject; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import xin.merlin.myplayerbackend.utils.websocket.command.BaseCommandHandler; + +import java.time.Duration; + +@Component +@RequiredArgsConstructor +public class Heartbeat implements BaseCommandHandler { + + private final RedisTemplate redis; + + @Override + public void handle(JSONObject msg) { + String userId = msg.getString("from"); + redis.opsForValue().set("online:" + userId, "1", Duration.ofMinutes(5)); + } +} diff --git a/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Message.java b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Message.java new file mode 100644 index 0000000..43fcc25 --- /dev/null +++ b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Message.java @@ -0,0 +1,23 @@ +package xin.merlin.myplayerbackend.utils.websocket.command.impl; + +import com.alibaba.fastjson2.JSONObject; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import xin.merlin.myplayerbackend.utils.websocket.WebSocketSessionManager; +import xin.merlin.myplayerbackend.utils.websocket.command.BaseCommandHandler; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class Message implements BaseCommandHandler { + + private final WebSocketSessionManager sessionManager; + + @Override + public void handle(JSONObject msg) throws IOException { + String to = msg.getString("to"); + sessionManager.sendToUser(Integer.valueOf(to), msg.toJSONString()); + } +} + diff --git a/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Ping.java b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Ping.java new file mode 100644 index 0000000..1262e3a --- /dev/null +++ b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Ping.java @@ -0,0 +1,24 @@ +package xin.merlin.myplayerbackend.utils.websocket.command.impl; + +import com.alibaba.fastjson2.JSONObject; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import xin.merlin.myplayerbackend.utils.websocket.WebSocketSessionManager; +import xin.merlin.myplayerbackend.utils.websocket.command.BaseCommandHandler; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class Ping implements BaseCommandHandler { + + private final WebSocketSessionManager sessionManager; + + @Override + public void handle(JSONObject msg) throws IOException { + String from = msg.getString("from"); + msg.put("cmd", "PONG"); + sessionManager.sendToUser(Integer.valueOf(from), msg.toJSONString()); + } +} + diff --git a/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/SystemNotify.java b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/SystemNotify.java new file mode 100644 index 0000000..68e0fc5 --- /dev/null +++ b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/SystemNotify.java @@ -0,0 +1,22 @@ +package xin.merlin.myplayerbackend.utils.websocket.command.impl; + +import com.alibaba.fastjson2.JSONObject; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import xin.merlin.myplayerbackend.utils.websocket.WebSocketSessionManager; +import xin.merlin.myplayerbackend.utils.websocket.command.BaseCommandHandler; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class SystemNotify implements BaseCommandHandler { + + private final WebSocketSessionManager sessionManager; + + @Override + public void handle(JSONObject msg) throws IOException { + sessionManager.broadcast(msg.toJSONString()); + } +} + diff --git a/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Typing.java b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Typing.java new file mode 100644 index 0000000..7c69c13 --- /dev/null +++ b/src/main/java/xin/merlin/myplayerbackend/utils/websocket/command/impl/Typing.java @@ -0,0 +1,22 @@ +package xin.merlin.myplayerbackend.utils.websocket.command.impl; + +import com.alibaba.fastjson2.JSONObject; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import xin.merlin.myplayerbackend.utils.websocket.WebSocketSessionManager; +import xin.merlin.myplayerbackend.utils.websocket.command.BaseCommandHandler; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class Typing implements BaseCommandHandler { + + private final WebSocketSessionManager sessionManager; + + @Override + public void handle(JSONObject msg) throws IOException { + String to = msg.getString("to"); + sessionManager.sendToUser(Integer.valueOf(to), msg.toJSONString()); + } +}