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());
+ }
+}