fix: 修改房间逻辑
This commit is contained in:
@@ -62,6 +62,14 @@ public interface GomokuGameService {
|
||||
*/
|
||||
void handleDisconnect(String roomId, String userId, RoomSessionManager sessionManager);
|
||||
|
||||
/**
|
||||
* 取消断连超时定时器(重连时调用)
|
||||
*
|
||||
* @param roomId 房间 ID
|
||||
* @param userId 用户 ID
|
||||
*/
|
||||
void cancelDisconnectTimeout(String roomId, String userId);
|
||||
|
||||
/**
|
||||
* 从走棋记录重建棋盘状态(静态工具方法)
|
||||
*
|
||||
|
||||
@@ -338,6 +338,14 @@ public class GameServiceImpl implements GameService {
|
||||
return newStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始游戏(HTTP 端点保留,实际开始游戏由 WebSocket 处理)
|
||||
*
|
||||
* WS 的 "start" 消息会触发 GomokuGameServiceImpl.startGame(),
|
||||
* 包含完整校验 + GameRecord 创建 + WS BOARD_SYNC + SSE ROOM_UPDATED。
|
||||
*
|
||||
* TODO: 金币结算逻辑预留,后期处理
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public void startGame(String roomId, String ownerId) {
|
||||
@@ -360,43 +368,20 @@ public class GameServiceImpl implements GameService {
|
||||
throw new BusinessException(ErrorCode.PLAYER_COUNT_INSUFFICIENT);
|
||||
}
|
||||
|
||||
// 校验所有玩家已准备(房主除外,房主默认准备)
|
||||
// 校验所有玩家已准备
|
||||
for (RoomPlayerEntity player : players) {
|
||||
if (!player.getReadyStatus() && !player.getUserId().equals(room.getCreatorId())) {
|
||||
if (!player.getReadyStatus()) {
|
||||
throw new BusinessException(ErrorCode.NOT_ALL_READY);
|
||||
}
|
||||
}
|
||||
|
||||
// 扣除房主底注
|
||||
UserEntity ownerUser = userRepository.findByAccountId(ownerId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
if (ownerUser.getBalance() < room.getStakes()) {
|
||||
throw new BusinessException(ErrorCode.BALANCE_INSUFFICIENT);
|
||||
}
|
||||
ownerUser.setBalance(ownerUser.getBalance() - room.getStakes());
|
||||
userRepository.save(ownerUser);
|
||||
|
||||
// 扣除其他玩家底注
|
||||
for (RoomPlayerEntity player : players) {
|
||||
if (!player.getUserId().equals(ownerId)) {
|
||||
UserEntity user = userRepository.findByAccountId(player.getUserId())
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
|
||||
if (user.getBalance() < room.getStakes()) {
|
||||
throw new BusinessException(ErrorCode.BALANCE_INSUFFICIENT,
|
||||
"玩家 " + user.getNickName() + " 余额不足");
|
||||
}
|
||||
user.setBalance(user.getBalance() - room.getStakes());
|
||||
userRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新房间状态为进行中
|
||||
room.setStatus("playing");
|
||||
gameRoomRepository.save(room);
|
||||
|
||||
log.info("[游戏服务] 游戏开始, roomId={}, 参与人数={}, 底注={}", roomId, players.size(), room.getStakes());
|
||||
log.info("[游戏服务] 游戏开始, roomId={}, 参与人数={}", roomId, players.size());
|
||||
|
||||
// 通知房间状态更新
|
||||
// 通知房间状态更新(大厅刷新)
|
||||
List<RoomPlayerResponse> allPlayers = buildRoomPlayers(roomId);
|
||||
GameConfigEntity gameConfig = gameConfigRepository.findByGameId(room.getGameId())
|
||||
.orElse(null);
|
||||
@@ -404,28 +389,6 @@ public class GameServiceImpl implements GameService {
|
||||
String creatorName = getNickname(room.getCreatorId());
|
||||
RoomItemResponse response = RoomItemResponse.from(room, allPlayers, icon, creatorName);
|
||||
sseEventPublisher.broadcast(SseEventType.ROOM_UPDATED, response);
|
||||
|
||||
// 通知房间内每个玩家游戏已开始
|
||||
for (RoomPlayerEntity player : players) {
|
||||
sseEventPublisher.sendTo(player.getUserId(), SseEventType.GAME_STARTED,
|
||||
Map.of("roomId", roomId));
|
||||
}
|
||||
|
||||
// 通知余额变动(房主已在前面扣除了底注)
|
||||
ownerUser = userRepository.findByAccountId(ownerId).orElse(null);
|
||||
if (ownerUser != null) {
|
||||
sseEventPublisher.sendTo(ownerId, SseEventType.BALANCE_CHG,
|
||||
Map.of("balance", ownerUser.getBalance(), "delta", -room.getStakes()));
|
||||
}
|
||||
for (RoomPlayerEntity player : players) {
|
||||
if (!player.getUserId().equals(ownerId)) {
|
||||
UserEntity user = userRepository.findByAccountId(player.getUserId()).orElse(null);
|
||||
if (user != null) {
|
||||
sseEventPublisher.sendTo(player.getUserId(), SseEventType.BALANCE_CHG,
|
||||
Map.of("balance", user.getBalance(), "delta", -room.getStakes()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package com.webgame.webgamebackend.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.webgame.webgamebackend.common.dto.game.RoomItemResponse;
|
||||
import com.webgame.webgamebackend.common.dto.game.RoomPlayerResponse;
|
||||
import com.webgame.webgamebackend.common.event.SseEventPublisher;
|
||||
import com.webgame.webgamebackend.common.event.SseEventType;
|
||||
import com.webgame.webgamebackend.common.exception.BusinessException;
|
||||
import com.webgame.webgamebackend.common.exception.ErrorCode;
|
||||
import com.webgame.webgamebackend.entities.*;
|
||||
@@ -18,14 +22,22 @@ import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 五子棋对局逻辑服务实现
|
||||
*
|
||||
* 管理五子棋对局的完整生命周期:落子校验、胜负判定、走棋记录。
|
||||
* 管理五子棋对局的完整生命周期:落子校验、胜负判定、走棋记录、断线处理。
|
||||
* 通过 RoomSessionManager 向房间内 WebSocket 会话广播状态变更。
|
||||
* 通过 SseEventPublisher 向大厅推送房间状态变更。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -37,6 +49,19 @@ public class GomokuGameServiceImpl implements GomokuGameService {
|
||||
private final GameRecordRepository gameRecordRepository;
|
||||
private final RoomPlayerRepository roomPlayerRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final GameConfigRepository gameConfigRepository;
|
||||
private final SseEventPublisher sseEventPublisher;
|
||||
|
||||
/** 断线超时时间(秒) */
|
||||
private static final long DISCONNECT_TIMEOUT_SECONDS = 60;
|
||||
|
||||
/** 断线超时定时器线程池 */
|
||||
private final ScheduledExecutorService disconnectScheduler =
|
||||
Executors.newSingleThreadScheduledExecutor(r ->
|
||||
new Thread(r, "disconnect-timeout"));
|
||||
|
||||
/** 断线超时任务映射: "roomId:userId" → ScheduledFuture */
|
||||
private final ConcurrentHashMap<String, ScheduledFuture<?>> disconnectTasks = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@@ -59,7 +84,6 @@ public class GomokuGameServiceImpl implements GomokuGameService {
|
||||
List<GameMoveEntity> moves = gameMoveRepository.findByRoomIdOrderByMoveIndexAsc(roomId);
|
||||
int moveCount = moves.size();
|
||||
|
||||
// 判定当前玩家应该下哪一方(黑先白后)
|
||||
// 按座位号排序,确保黑方/白方分配与座位对应(seat=1 为黑方先手)
|
||||
List<RoomPlayerEntity> players = roomPlayerRepository.findByRoomId(roomId)
|
||||
.stream()
|
||||
@@ -139,6 +163,7 @@ public class GomokuGameServiceImpl implements GomokuGameService {
|
||||
|
||||
log.info("[五子棋] 准备状态变更, roomId={}, userId={}, ready={}", roomId, userId, newStatus);
|
||||
|
||||
// WS 广播准备状态变更(房间内实时同步)
|
||||
broadcast(roomId, sessionManager, new WsMessage(WsOutboundMessage.READY_CHANGE, Map.of(
|
||||
"userId", userId, "ready", newStatus
|
||||
)));
|
||||
@@ -186,24 +211,43 @@ public class GomokuGameServiceImpl implements GomokuGameService {
|
||||
@Override
|
||||
@Transactional
|
||||
public void leaveRoom(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
// 复用 GameService 离开逻辑,通过 GameController 的 leaveRoom 复用
|
||||
// 这里简单处理:删除玩家记录
|
||||
GameRoomEntity room = getRoom(roomId);
|
||||
|
||||
// 检查玩家是否在房间中
|
||||
RoomPlayerEntity player = roomPlayerRepository.findByRoomIdAndUserId(roomId, userId)
|
||||
.orElse(null);
|
||||
if (player != null) {
|
||||
roomPlayerRepository.delete(player);
|
||||
if (player == null) {
|
||||
return; // 已不在房间,无需操作
|
||||
}
|
||||
|
||||
roomPlayerRepository.delete(player);
|
||||
List<RoomPlayerEntity> remaining = roomPlayerRepository.findByRoomId(roomId);
|
||||
|
||||
// WS 广播玩家离开
|
||||
broadcast(roomId, sessionManager, new WsMessage(WsOutboundMessage.PLAYER_LEAVE, Map.of(
|
||||
"userId", userId
|
||||
)));
|
||||
|
||||
// 检查是否还有玩家
|
||||
List<RoomPlayerEntity> remaining = roomPlayerRepository.findByRoomId(roomId);
|
||||
if (remaining.isEmpty()) {
|
||||
GameRoomEntity room = getRoom(roomId);
|
||||
// 无人了,房间标记为结束
|
||||
room.setStatus("finished");
|
||||
gameRoomRepository.save(room);
|
||||
log.info("[五子棋] 房间无人,标记为结束, roomId={}", roomId);
|
||||
// SSE: 大厅移除房间
|
||||
sseEventPublisher.broadcast(SseEventType.ROOM_REMOVED, Map.of("roomId", roomId));
|
||||
} else if (userId.equals(room.getCreatorId())) {
|
||||
// 房主离开,顺位转让给剩余第一个玩家
|
||||
RoomPlayerEntity nextOwner = remaining.get(0);
|
||||
room.setCreatorId(nextOwner.getUserId());
|
||||
nextOwner.setReadyStatus(true);
|
||||
roomPlayerRepository.save(nextOwner);
|
||||
gameRoomRepository.save(room);
|
||||
log.info("[五子棋] 房主离开,转让给 userId={}, roomId={}", nextOwner.getUserId(), roomId);
|
||||
// SSE: 大厅更新房间信息
|
||||
pushRoomUpdated(room);
|
||||
} else {
|
||||
// SSE: 大厅更新房间信息
|
||||
pushRoomUpdated(room);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +272,7 @@ public class GomokuGameServiceImpl implements GomokuGameService {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新房间状态为进行中
|
||||
room.setStatus("playing");
|
||||
gameRoomRepository.save(room);
|
||||
|
||||
@@ -239,18 +284,44 @@ public class GomokuGameServiceImpl implements GomokuGameService {
|
||||
|
||||
log.info("[五子棋] 对局开始, roomId={}, 玩家数={}", roomId, players.size());
|
||||
|
||||
// 广播棋盘同步(空棋盘)
|
||||
// WS: 广播棋盘同步(空棋盘 + 黑方先行)
|
||||
broadcast(roomId, sessionManager, new WsMessage(WsOutboundMessage.BOARD_SYNC, Map.of(
|
||||
"board", new int[BOARD_SIZE][BOARD_SIZE],
|
||||
"currentTurn", players.get(0).getUserId() // 第一位玩家先行(黑方)
|
||||
)));
|
||||
|
||||
// SSE: 大厅更新房间状态(waiting → playing)
|
||||
pushRoomUpdated(room);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleDisconnect(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
// 断连不立即离开房间,等重连超时后再清理
|
||||
// 首期简单处理:延迟 30 秒后如果玩家未重连则离开
|
||||
log.info("[五子棋] 玩家断连, roomId={}, userId={}", roomId, userId);
|
||||
String taskKey = roomId + ":" + userId;
|
||||
log.info("[五子棋] 玩家断连,启动{}秒超时定时器, roomId={}, userId={}",
|
||||
DISCONNECT_TIMEOUT_SECONDS, roomId, userId);
|
||||
|
||||
// 启动断线超时定时器
|
||||
ScheduledFuture<?> task = disconnectScheduler.schedule(() -> {
|
||||
disconnectTasks.remove(taskKey);
|
||||
try {
|
||||
onDisconnectTimeout(roomId, userId, sessionManager);
|
||||
} catch (Exception e) {
|
||||
log.error("[五子棋] 断线超时处理异常, roomId={}, userId={}", roomId, userId, e);
|
||||
}
|
||||
}, DISCONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
|
||||
disconnectTasks.put(taskKey, task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelDisconnectTimeout(String roomId, String userId) {
|
||||
String taskKey = roomId + ":" + userId;
|
||||
ScheduledFuture<?> task = disconnectTasks.remove(taskKey);
|
||||
if (task != null) {
|
||||
boolean cancelled = task.cancel(false);
|
||||
log.info("[五子棋] 玩家重连,取消断线超时定时器, roomId={}, userId={}, cancelled={}",
|
||||
roomId, userId, cancelled);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有辅助方法 ====================
|
||||
@@ -279,8 +350,49 @@ public class GomokuGameServiceImpl implements GomokuGameService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送房间更新到大厅(SSE)
|
||||
*/
|
||||
private void pushRoomUpdated(GameRoomEntity room) {
|
||||
List<RoomPlayerEntity> roomPlayers = roomPlayerRepository.findByRoomId(room.getId());
|
||||
List<RoomPlayerResponse> players = buildRoomPlayerResponses(roomPlayers);
|
||||
String creatorName = getNickname(room.getCreatorId());
|
||||
String icon = gameConfigRepository.findByGameId(room.getGameId())
|
||||
.map(GameConfigEntity::getIcon)
|
||||
.orElse("");
|
||||
RoomItemResponse response = RoomItemResponse.from(room, players, icon, creatorName);
|
||||
sseEventPublisher.broadcast(SseEventType.ROOM_UPDATED, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断线超时处理:对局中自动认输,等待中自动离开
|
||||
*/
|
||||
@Transactional
|
||||
public void onDisconnectTimeout(String roomId, String userId, RoomSessionManager sessionManager) {
|
||||
GameRoomEntity room = getRoom(roomId);
|
||||
|
||||
// 检查玩家是否还在房间中(可能已被踢出或已离开)
|
||||
boolean stillInRoom = roomPlayerRepository.findByRoomIdAndUserId(roomId, userId).isPresent();
|
||||
if (!stillInRoom) {
|
||||
log.info("[五子棋] 断线超时:玩家已不在房间, roomId={}, userId={}", roomId, userId);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("playing".equals(room.getStatus())) {
|
||||
log.info("[五子棋] 断线超时:对局中自动判负, roomId={}, userId={}", roomId, userId);
|
||||
// 自动认输
|
||||
resign(roomId, userId, sessionManager);
|
||||
} else {
|
||||
log.info("[五子棋] 断线超时:等待中自动离开, roomId={}, userId={}", roomId, userId);
|
||||
// 自动离开房间
|
||||
leaveRoom(roomId, userId, sessionManager);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对局结束
|
||||
*
|
||||
* TODO: 金币结算逻辑预留,后期处理
|
||||
*/
|
||||
@Transactional
|
||||
public void endGame(String roomId, String winnerId, String resultType,
|
||||
@@ -301,11 +413,14 @@ public class GomokuGameServiceImpl implements GomokuGameService {
|
||||
log.info("[五子棋] 对局结束, roomId={}, winnerId={}, resultType={}",
|
||||
roomId, winnerId, resultType);
|
||||
|
||||
// 广播对局结束
|
||||
// WS: 广播对局结束
|
||||
broadcast(roomId, sessionManager, new WsMessage(WsOutboundMessage.GAME_OVER, Map.of(
|
||||
"winnerId", winnerId,
|
||||
"resultType", resultType
|
||||
)));
|
||||
|
||||
// SSE: 大厅更新房间状态
|
||||
pushRoomUpdated(room);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,4 +461,37 @@ public class GomokuGameServiceImpl implements GomokuGameService {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装房间玩家响应列表(含头像和昵称)
|
||||
*/
|
||||
private List<RoomPlayerResponse> buildRoomPlayerResponses(List<RoomPlayerEntity> roomPlayers) {
|
||||
if (roomPlayers.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> userIds = roomPlayers.stream()
|
||||
.map(RoomPlayerEntity::getUserId)
|
||||
.toList();
|
||||
List<UserEntity> users = userRepository.findByAccountIdIn(userIds);
|
||||
Map<String, UserEntity> userMap = users.stream()
|
||||
.collect(Collectors.toMap(u -> u.getAccount().getId(), u -> u));
|
||||
|
||||
return roomPlayers.stream()
|
||||
.map(rp -> {
|
||||
UserEntity user = userMap.get(rp.getUserId());
|
||||
String avatar = user != null ? user.getAvatar() : "";
|
||||
String nickname = user != null ? user.getNickName() : "未知玩家";
|
||||
return new RoomPlayerResponse(avatar, nickname);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户昵称
|
||||
*/
|
||||
private String getNickname(String accountId) {
|
||||
return userRepository.findByAccountId(accountId)
|
||||
.map(UserEntity::getNickName)
|
||||
.orElse("未知玩家");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,12 @@ public class RoomWebSocketHandler extends TextWebSocketHandler {
|
||||
}
|
||||
String userId = loginId.toString();
|
||||
|
||||
// 注册会话
|
||||
// 注册会话(内部会清理同用户的旧会话)
|
||||
sessionManager.addSession(roomId, userId, session);
|
||||
|
||||
// 取消断线超时定时器(重连场景)
|
||||
gomokuGameService.cancelDisconnectTimeout(roomId, userId);
|
||||
|
||||
log.info("[WS] 连接建立, roomId={}, userId={}, sessionId={}", roomId, userId, session.getId());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user