package com.webgame.webgamebackend.ws; import cn.dev33.satoken.stp.StpUtil; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.webgame.webgamebackend.service.GomokuGameService; import com.webgame.webgamebackend.ws.dto.WsInboundMessage; import com.webgame.webgamebackend.ws.dto.WsMessage; import com.webgame.webgamebackend.ws.dto.WsOutboundMessage; 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 处理器 * * 处理客户端连接、断开与消息路由。消息类型路由到不同的业务逻辑。 * 通过 URL 参数传递 roomId 和 token 完成认证与房间绑定。 */ @Slf4j @Component @RequiredArgsConstructor public class RoomWebSocketHandler extends TextWebSocketHandler { private final RoomSessionManager sessionManager; private final GomokuGameService gomokuGameService; @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 从 URL 参数中提取 roomId 和 token 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 Object loginId = StpUtil.getLoginIdByToken(token); if (loginId == null) { session.close(CloseStatus.POLICY_VIOLATION); return; } String userId = loginId.toString(); // 注册会话(内部会清理同用户的旧会话) sessionManager.addSession(roomId, userId, session); // 取消断线超时定时器(重连场景) gomokuGameService.cancelDisconnectTimeout(roomId, userId); 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 data = json.get("data"); switch (type) { case WsInboundMessage.PLACE_STONE -> { // data 包含 row, col int row = ((JSONObject) data).getIntValue("row"); int col = ((JSONObject) data).getIntValue("col"); gomokuGameService.placeStone(roomId, userId, row, col, sessionManager); } case WsInboundMessage.CHAT -> { String content = ((JSONObject) data).getString("content"); handleChat(roomId, userId, content); } case WsInboundMessage.READY -> { gomokuGameService.toggleReady(roomId, userId, sessionManager); } case WsInboundMessage.RESIGN -> { gomokuGameService.resign(roomId, userId, sessionManager); } case WsInboundMessage.DRAW_REQUEST -> { gomokuGameService.requestDraw(roomId, userId, sessionManager); } case WsInboundMessage.DRAW_RESPONSE -> { boolean accept = ((JSONObject) data).getBooleanValue("accept"); gomokuGameService.respondToDraw(roomId, userId, accept, sessionManager); } case WsInboundMessage.LEAVE -> { gomokuGameService.leaveRoom(roomId, userId, sessionManager); } case WsInboundMessage.START -> { gomokuGameService.startGame(roomId, userId, sessionManager); } default -> sendError(session, "未知消息类型: " + type); } } 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) { // 通知游戏服务玩家断开 gomokuGameService.handleDisconnect(roomId, userId, sessionManager); } 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 void handleChat(String roomId, String userId, String content) { // TODO: 后续可增加消息持久化 broadcastToRoom(roomId, new WsMessage(WsOutboundMessage.CHAT_BROADCAST, Map.of( "userId", userId, "content", content, "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); } } } }