package com.webgame.webgamebackend.adapter.ws; import com.webgame.webgamebackend.common.auth.AuthenticationProvider; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.webgame.webgamebackend.infrastructure.persistence.entity.GameRoomEntity; import com.webgame.webgamebackend.infrastructure.persistence.repository.GameRoomRepository; import com.webgame.webgamebackend.domain.room.RoomManager; import com.webgame.webgamebackend.domain.engine.GameEngine; import com.webgame.webgamebackend.domain.engine.GameEngineRegistry; import com.webgame.webgamebackend.domain.engine.RoomContext; import com.webgame.webgamebackend.adapter.ws.dto.WsInboundMessage; import com.webgame.webgamebackend.adapter.ws.dto.WsMessage; import com.webgame.webgamebackend.adapter.ws.dto.WsOutboundMessage; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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 java.io.IOException; import java.net.URI; import java.util.Map; /** * 房间 WebSocket 处理器 * * 重构后的职责: * - 连接建立:认证 + 注册会话 + 取消断线倒计时(通过 RoomManager) * - 消息路由:房间管理消息 → RoomManager,游戏消息 → GameEngine * - 连接关闭:启动断线倒计时(通过 RoomManager) * *
采用策略路由:根据房间当前状态(WAITING/PLAYING)将消息分发给不同的处理器。 * WAITING 状态下只有房间管理消息(ready, leave, start)有效; * PLAYING 状态下游戏消息(place_stone, resign, draw_*)由 GameEngine 处理。
*/ @Slf4j @Component @RequiredArgsConstructor public class RoomWebSocketHandler extends TextWebSocketHandler { private final RoomSessionManager sessionManager; private final RoomManager roomManager; private final GameEngineRegistry engineRegistry; private final GameRoomRepository gameRoomRepository; private final WsMessageRouter router; private final AuthenticationProvider authProvider; @PostConstruct public void registerHandlers() { // ===== 房间管理消息(所有状态) ===== // 准备/取消准备 router.register(WsInboundMessage.READY, (roomId, userId, data, sm) -> roomManager.toggleReady(roomId, userId)); // 离开房间 router.register(WsInboundMessage.LEAVE, (roomId, userId, data, sm) -> roomManager.leaveRoom(roomId, userId)); // 开始游戏(仅房主) router.register(WsInboundMessage.START, (roomId, userId, data, sm) -> roomManager.startGame(roomId, userId)); // ===== 游戏消息(PLAYING 状态) ===== // 落子 router.register(WsInboundMessage.PLACE_STONE, (roomId, userId, data, sm) -> { GameRoomEntity room = gameRoomRepository.findById(roomId).orElse(null); if (room != null && "playing".equals(room.getStatus())) { // 通过 RoomManager 获取引擎的 getState 不行,需要直接找到引擎 // 改为委托模式:RoomManager 提供 executeGameAction 方法 roomManager.executeGameAction(roomId, userId, "place_stone", data); } }); // 认输 router.register(WsInboundMessage.RESIGN, (roomId, userId, data, sm) -> { roomManager.executeGameAction(roomId, userId, "resign", null); }); // 求和请求 router.register(WsInboundMessage.DRAW_REQUEST, (roomId, userId, data, sm) -> { roomManager.executeGameAction(roomId, userId, "draw_request", null); }); // 回应求和 router.register(WsInboundMessage.DRAW_RESPONSE, (roomId, userId, data, sm) -> { roomManager.executeGameAction(roomId, userId, "draw_response", data); }); // 聊天 router.register(WsInboundMessage.CHAT, (roomId, userId, data, sm) -> { String content = data != null ? data.getString("content") : null; handleChat(roomId, userId, content); }); log.info("[WS] 已注册 {} 个消息处理器", router.size()); } @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { URI uri = session.getUri(); if (uri == null) { session.close(CloseStatus.BAD_DATA); return; } String query = uri.getQuery(); if (query == null) { session.close(CloseStatus.BAD_DATA); return; } String roomId = getQueryParam(query, "roomId"); String token = getQueryParam(query, "token"); if (roomId == null || token == null) { session.close(CloseStatus.BAD_DATA); return; } // 通过 token 获取用户 ID String userId = authProvider.getUserIdByToken(token); if (userId == null) { session.close(CloseStatus.POLICY_VIOLATION); return; } // 处理重连(在注册会话之前,避免广播到尚未完成握手的当前 session) roomManager.handleReconnect(roomId, userId); // 注册会话 sessionManager.addSession(roomId, userId, session); log.info("[WS] 连接建立, roomId={}, userId={}, sessionId={}", roomId, userId, session.getId()); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload = message.getPayload(); String userId = sessionManager.getUserId(session); String roomId = sessionManager.getRoomId(session); if (userId == null || roomId == null) { sendError(session, "未加入房间"); return; } log.info("[WS] 收到消息, roomId={}, userId={}, payload={}", roomId, userId, payload); try { JSONObject json = JSON.parseObject(payload); String type = json.getString("type"); Object rawData = json.get("data"); JSONObject data = (rawData instanceof JSONObject) ? (JSONObject) rawData : null; WsMessageHandler handler = router.get(type); if (handler == null) { sendError(session, "未知消息类型: " + type); return; } handler.handle(roomId, userId, data, sessionManager); } catch (Exception e) { log.error("[WS] 处理消息异常, roomId={}, userId={}", roomId, userId, e); sendError(session, e.getMessage()); } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { String userId = sessionManager.getUserId(session); String roomId = sessionManager.getRoomId(session); log.info("[WS] 连接关闭, roomId={}, userId={}, sessionId={}, status={}", roomId, userId, session.getId(), status); if (userId != null && roomId != null) { // 通知 RoomManager 处理断线 roomManager.handleDisconnect(roomId, userId); } sessionManager.removeSession(session); } // ==================== 辅助方法 ==================== /** * 解析 URL Query 参数 */ private String getQueryParam(String query, String key) { for (String param : query.split("&")) { String[] pair = param.split("=", 2); if (pair.length == 2 && pair[0].equals(key)) { return pair[1]; } } return null; } /** 聊天消息最大长度 */ private static final int CHAT_MAX_LENGTH = 500; /** * 聊天消息处理 */ private void handleChat(String roomId, String userId, String content) { if (content == null || content.isBlank()) { return; } if (content.length() > CHAT_MAX_LENGTH) { content = content.substring(0, CHAT_MAX_LENGTH); } String sanitized = content .replace("<", "<") .replace(">", ">"); broadcastToRoom(roomId, new WsMessage(WsOutboundMessage.CHAT_BROADCAST, Map.of( "userId", userId, "content", sanitized, "timestamp", System.currentTimeMillis() ))); } /** * 向房间内所有会话广播消息 */ private void broadcastToRoom(String roomId, WsMessage message) { String json = JSON.toJSONString(message); for (WebSocketSession ws : sessionManager.getRoomSessions(roomId)) { if (ws.isOpen()) { try { ws.sendMessage(new TextMessage(json)); } catch (IOException e) { log.error("[WS] 发送消息失败, sessionId={}", ws.getId(), e); } } } } /** * 向指定会话发送错误消息 */ private void sendError(WebSocketSession session, String errorMsg) { if (session.isOpen()) { try { String json = JSON.toJSONString( new WsMessage(WsOutboundMessage.ERROR, Map.of("message", errorMsg))); session.sendMessage(new TextMessage(json)); } catch (IOException e) { log.error("[WS] 发送错误消息失败", e); } } } }