From 3b3646583f8d866aeca6dcbd56e0d21c3a22d09a Mon Sep 17 00:00:00 2001 From: Bryan <15172886+bryanlufee@user.noreply.gitee.com> Date: Wed, 12 Nov 2025 18:25:52 +0800 Subject: [PATCH] 22 --- RuoYi-Vue/ruoyi-admin/pom.xml | 8 + .../ruoyi/redis/RedisMessagePublisher.java | 28 +++ .../ruoyi/redis/RedisMessageSubscriber.java | 56 +++++ .../com/ruoyi/redis/RedisPubSubConfig.java | 62 ++++++ .../server/GameRoomWebSocketHandler.java | 60 ++++- scoring/scoring/App.vue | 4 +- scoring/scoring/main.js | 23 +- scoring/scoring/package.json | 7 +- scoring/scoring/pages.json | 2 + scoring/scoring/pages/index/index.vue | 1 + scoring/scoring/pages/more/more.vue | 206 +++++++++++------- scoring/scoring/pages/single/single.vue | 60 ++++- 12 files changed, 405 insertions(+), 112 deletions(-) create mode 100644 RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/redis/RedisMessagePublisher.java create mode 100644 RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/redis/RedisMessageSubscriber.java create mode 100644 RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/redis/RedisPubSubConfig.java diff --git a/RuoYi-Vue/ruoyi-admin/pom.xml b/RuoYi-Vue/ruoyi-admin/pom.xml index 8d86e17..282b9e4 100644 --- a/RuoYi-Vue/ruoyi-admin/pom.xml +++ b/RuoYi-Vue/ruoyi-admin/pom.xml @@ -87,6 +87,14 @@ json 20231013 + + com.ruoyi + ruoyi-system + + + org.springframework.boot + spring-boot-starter-data-redis + diff --git a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/redis/RedisMessagePublisher.java b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/redis/RedisMessagePublisher.java new file mode 100644 index 0000000..4346d88 --- /dev/null +++ b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/redis/RedisMessagePublisher.java @@ -0,0 +1,28 @@ +package com.ruoyi.redis; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +@Component +public class RedisMessagePublisher { + + @Autowired + private StringRedisTemplate redisTemplate; + + /** + * 向指定频道发布消息 + * @param channel 频道名称 + * @param message 消息内容(JSON字符串) + */ + public void publish(String channel, String message) { + redisTemplate.convertAndSend(channel, message); + } + /** + * 作用:发布消息到 Redis 频道:将 message 发送到指定的 channel。 + * 触发订阅者处理:所有通过 RedisMessageListenerContainer 订阅了该频道的客户端(如其他微服务、后台任务等)会立即收到消息,并调用对应的消息处理器(如 onMessage 方法)。 + * + * channel:目标频道名称(如 "game_room_channel")。 + * message:要发送的消息,可以是任意对象(Spring 会自动序列化为字节数组或 JSON)。 + */ +} \ No newline at end of file diff --git a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/redis/RedisMessageSubscriber.java b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/redis/RedisMessageSubscriber.java new file mode 100644 index 0000000..1060efc --- /dev/null +++ b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/redis/RedisMessageSubscriber.java @@ -0,0 +1,56 @@ +package com.ruoyi.redis; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import com.ruoyi.websocket.server.GameRoomWebSocketHandler; +import java.io.IOException; +import java.util.Map; + +@Component +public class RedisMessageSubscriber { + + @Autowired + private ObjectMapper objectMapper;//是jackson里面的方法,主要处理序列化和反序列化 + + /** + * 处理从Redis订阅到的消息 + */ + public void onMessage(String redisMessage, String channel) { + try { + // 解析消息,其中应包含房间ID和要发送的实际消息体 + Map messageMap = objectMapper.readValue(redisMessage, Map.class); + String targetRoomId = (String) messageMap.get("roomId"); + Map actualMessageToSend = (Map) messageMap.get("message"); + + // 调用本地广播方法,只发送给本实例内指定房间的用户 + broadcastToLocalRoom(targetRoomId, actualMessageToSend); + + } catch (Exception e) { + System.err.println("处理Redis消息失败: " + e.getMessage()); + } + } + + /** + * 仅向本地会话(即当前JVM内的WebSocketSession)广播消息 + */ + private void broadcastToLocalRoom(String roomId, Map message) throws Exception { + // 遍历原Handler中的sessions变量(需稍作调整使其可被访问) + for (Map.Entry entry : GameRoomWebSocketHandler.getSessions().entrySet()) { + String sessionKey = entry.getKey(); + if (sessionKey.startsWith(roomId + "_")) { // 判断是否属于目标房间 + WebSocketSession session = entry.getValue(); + if (session != null && session.isOpen()) { + try { + String jsonMessage = objectMapper.writeValueAsString(message); + session.sendMessage(new TextMessage(jsonMessage)); + } catch (IOException e) { + System.err.println("向本地会话发送消息失败: " + e.getMessage()); + } + } + } + } + } +} \ No newline at end of file diff --git a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/redis/RedisPubSubConfig.java b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/redis/RedisPubSubConfig.java new file mode 100644 index 0000000..3f41fa7 --- /dev/null +++ b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/redis/RedisPubSubConfig.java @@ -0,0 +1,62 @@ +package com.ruoyi.redis; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.Topic; +import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; + +@Configuration +public class RedisPubSubConfig { + + @Autowired + private RedisConnectionFactory redisConnectionFactory; + + @Autowired + private RedisMessageSubscriber redisMessageSubscriber; + + @Bean + public RedisMessageListenerContainer redisMessageListenerContainer() { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(redisConnectionFactory);// 设置 Redis 连接 + + // 订阅游戏房间频道 + Topic gameRoomTopic = new ChannelTopic("game_room_channel"); // 订阅的频道 + // 指定消息处理器(即redisMessageSubscriber的onMessage方法) + container.addMessageListener(new MessageListenerAdapter(redisMessageSubscriber, "onMessage"), gameRoomTopic);// addMessageListener绑定监听器和频道 + /** + * 整工作流程 + * 启动应用:Spring 初始化 RedisMessageListenerContainer,并建立与 Redis 的连接。 + * 订阅频道:容器开始监听 game_room_channel 频道。 + * 发布消息:其他服务或代码通过 Redis 的 PUBLISH 命令向 game_room_channel 发送消息,例如: + * bash + * PUBLISH game_room_channel "Hello, Game Room!" + * 消息处理: + * Redis 将消息推送给所有订阅者。 + * RedisMessageListenerContainer 接收到消息后,调用 RedisMessageSubscriber.onMessage() 方法。 + * 消息内容作为参数传递给 onMessage 方法,由开发者自定义处理逻辑(如更新游戏状态、广播通知等)。 + */ + /** + * MessageListenerAdapter + * 作用:将 Redis 消息转发到指定的处理器方法(这里是 redisMessageSubscriber.onMessage)。 + * 关键点: + * 第二个参数 "onMessage" 指定处理器方法名(需在 RedisMessageSubscriber 中实现)。 + * 默认情况下,消息会以 byte[] 形式传递给处理器,Spring 会自动反序列化。 + */ + + /*** + * 发布消息的步骤 + * 选择消息中间件:根据需求(如吞吐量、持久性、延迟)选择合适的工具。 + * 创建主题(Topic)或队列(Queue): + * 主题(Topic):一对多广播(所有订阅者都能收到消息)。 + * 队列(Queue):点对点(消息被一个订阅者消费)。 + * 发布消息: + * 指定主题/队列名称。 + * 发送消息内容(通常是JSON、Protobuf等格式)。 + */ + return container; + } +} \ No newline at end of file diff --git a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/websocket/server/GameRoomWebSocketHandler.java b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/websocket/server/GameRoomWebSocketHandler.java index 26120dc..04067af 100644 --- a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/websocket/server/GameRoomWebSocketHandler.java +++ b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/websocket/server/GameRoomWebSocketHandler.java @@ -1,19 +1,32 @@ package com.ruoyi.websocket.server; +import com.ruoyi.system.domain.ScoreRoomDetail; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.socket.*; import org.springframework.web.socket.handler.TextWebSocketHandler; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; +import java.util.Collections; import java.util.Map; import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; public class GameRoomWebSocketHandler extends TextWebSocketHandler { + + + @Autowired + private StringRedisTemplate redisTemplate; // 存储所有连接的会话 private static final Map sessions = new ConcurrentHashMap<>(); private final ObjectMapper objectMapper = new ObjectMapper(); + public static Map getSessions() { + return Collections.unmodifiableMap(sessions); // 返回不可修改的视图,保证线程安全 + } + + @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 1. 从路径参数中获取 roomId 和 userId @@ -40,9 +53,12 @@ public class GameRoomWebSocketHandler extends TextWebSocketHandler { welcomeMsg.put("message", "欢迎加入房间: " + roomId); welcomeMsg.put("roomId", roomId); welcomeMsg.put("userId", userId); + welcomeMsg.put("timestamp", System.currentTimeMillis()); sendJsonMessage(session, welcomeMsg); + + broadcastJsonToRoom(roomId,welcomeMsg); } @Override @@ -64,22 +80,42 @@ public class GameRoomWebSocketHandler extends TextWebSocketHandler { try { // 解析客户端发送的 JSON 消息 Map clientMessage = objectMapper.readValue(payload, Map.class); - String msgType = (String) clientMessage.getOrDefault("type", "chat"); + String msgType = (String) clientMessage.getOrDefault("type", "system"); + System.out.println("消息类型" + msgType); + if(msgType.equals("scoreUpdate")){ + int Score = (int) clientMessage.get("Score"); + int giveUserScore = (int) clientMessage.get("giveUserScore") - Score; + int givedUserScore = (int) clientMessage.get("givedUserScore") + Score; + Map scoreUpdateMsg = new HashMap<>(); + scoreUpdateMsg.put("type", msgType); + scoreUpdateMsg.put("giveUserId",clientMessage.get("giveUserId")); + scoreUpdateMsg.put("giveUserScore",giveUserScore); + scoreUpdateMsg.put("givedUserId",clientMessage.get("givedUserId")); + scoreUpdateMsg.put("givedUserScore",givedUserScore); + broadcastJsonToRoom(roomId,scoreUpdateMsg); + }else if(msgType.equals("scoreUpdates")){ - Map broadcastMsg = new HashMap<>(); - broadcastMsg.put("type", msgType); - broadcastMsg.put("userId", userId); - broadcastMsg.put("roomId", roomId); - broadcastMsg.put("content", clientMessage.get("content")); - broadcastMsg.put("timestamp", System.currentTimeMillis()); + } - broadcastJsonToRoom(roomId, broadcastMsg); + + else if(msgType.equals("userJoined")){ + + Map broadcastMsg = new HashMap<>(); + broadcastMsg.put("type", msgType); + broadcastMsg.put("userId", userId); + broadcastMsg.put("roomId", roomId); + broadcastMsg.put("nickName", clientMessage.get("nickName")); + broadcastMsg.put("avatars", clientMessage.get("avatars")); + broadcastMsg.put("timestamp", System.currentTimeMillis()); + + broadcastJsonToRoom(roomId, broadcastMsg); + } } catch (Exception e) { System.out.println("解析客户端消息失败,按文本处理: " + e.getMessage()); Map broadcastMsg = new HashMap<>(); - broadcastMsg.put("type", "chat"); + broadcastMsg.put("type", "error"); broadcastMsg.put("userId", userId); broadcastMsg.put("roomId", roomId); broadcastMsg.put("content", payload); @@ -151,12 +187,14 @@ public class GameRoomWebSocketHandler extends TextWebSocketHandler { // 辅助方法:广播 JSON 消息到指定房间 private void broadcastJsonToRoom(String roomId, Map message) throws Exception { - String jsonMessage = objectMapper.writeValueAsString(message); + for (Map.Entry entry : sessions.entrySet()) { if (entry.getKey().startsWith(roomId + "_")) { WebSocketSession session = entry.getValue(); if (session.isOpen()) { - session.sendMessage(new TextMessage(jsonMessage)); + System.out.println(message); + sendJsonMessage(session, message); + System.out.println(111); } } } diff --git a/scoring/scoring/App.vue b/scoring/scoring/App.vue index 8c2b732..2457395 100644 --- a/scoring/scoring/App.vue +++ b/scoring/scoring/App.vue @@ -13,5 +13,5 @@ + + \ No newline at end of file diff --git a/scoring/scoring/main.js b/scoring/scoring/main.js index 96f67d2..fa1ba5f 100644 --- a/scoring/scoring/main.js +++ b/scoring/scoring/main.js @@ -1,26 +1,33 @@ import App from './App' -import { getToken,checkLoginStatus } from '@/api/login.js'; +import { + getToken, + checkLoginStatus +} from '@/api/login.js'; import StaticValue from '@/utils/StaticValue.js' // #ifndef VUE3 import Vue from 'vue' import './uni.promisify.adaptor' +import uView from 'uview-ui'; // 如果 uView 支持模块化导出 +Vue.use(uView) Vue.config.productionTip = false App.mpType = 'app' const app = new Vue({ - ...App + ...App }) app.$mount() // #endif // #ifdef VUE3 -import { createSSRApp } from 'vue' +import { + createSSRApp +} from 'vue' export function createApp() { - const app = createSSRApp(App) - app.config.globalProperties.$StaticValue = StaticValue; // 挂载全局变量 - return { - app - } + const app = createSSRApp(App) + app.config.globalProperties.$StaticValue = StaticValue; // 挂载全局变量 + return { + app + } } // #endif checkLoginStatus(); diff --git a/scoring/scoring/package.json b/scoring/scoring/package.json index dc8ed94..e48ddbe 100644 --- a/scoring/scoring/package.json +++ b/scoring/scoring/package.json @@ -1,6 +1,11 @@ { "dependencies": { "less": "^4.4.2", - "less-loader": "^12.3.0" + "less-loader": "^12.3.0", + "uview-ui": "^2.0.38", + "vue": "^3.5.24" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1" } } diff --git a/scoring/scoring/pages.json b/scoring/scoring/pages.json index f4a62f0..9dba11d 100644 --- a/scoring/scoring/pages.json +++ b/scoring/scoring/pages.json @@ -59,4 +59,6 @@ ] }, "uniIdRouter": {} + + } \ No newline at end of file diff --git a/scoring/scoring/pages/index/index.vue b/scoring/scoring/pages/index/index.vue index 87ec1bf..a81e611 100644 --- a/scoring/scoring/pages/index/index.vue +++ b/scoring/scoring/pages/index/index.vue @@ -8,6 +8,7 @@ + diff --git a/scoring/scoring/pages/more/more.vue b/scoring/scoring/pages/more/more.vue index f934607..ad74e90 100644 --- a/scoring/scoring/pages/more/more.vue +++ b/scoring/scoring/pages/more/more.vue @@ -7,23 +7,12 @@ - - 1 - 添加玩家 - - - 2 - 转让房主 - - - 3 - 语音播报 - {{}} - - - 4 - 台板(茶水) + + + 添加玩家 + 转让计分员 + {{X}} 语音播报 + {{Y}} 台板 @@ -62,7 +51,7 @@ {{item.score}} - + @@ -79,7 +68,7 @@ - +
@@ -92,7 +81,7 @@
@@ -103,12 +92,19 @@