package com.webgame.webgamebackend.service.impl; import com.alibaba.fastjson2.JSON; import com.webgame.webgamebackend.common.dto.game.*; import com.webgame.webgamebackend.common.exception.BusinessException; import com.webgame.webgamebackend.common.exception.ErrorCode; import com.webgame.webgamebackend.entities.*; import com.webgame.webgamebackend.repository.*; import com.webgame.webgamebackend.common.event.SseEventPublisher; import com.webgame.webgamebackend.common.event.SseEventType; import com.webgame.webgamebackend.service.GameService; import com.webgame.webgamebackend.service.GomokuGameService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * 游戏服务实现 */ @Slf4j @Service @RequiredArgsConstructor public class GameServiceImpl implements GameService { private final GameConfigRepository gameConfigRepository; private final GameRoomRepository gameRoomRepository; private final RoomPlayerRepository roomPlayerRepository; private final UserRepository userRepository; private final BCryptPasswordEncoder passwordEncoder; private final SseEventPublisher sseEventPublisher; private final GameMoveRepository gameMoveRepository; @Override public GameListResponse getGameList() { List configs = gameConfigRepository.findByEnabledTrueOrderBySortOrderAsc(); List list = configs.stream() .map(GameItemResponse::from) .toList(); log.info("[游戏服务] 获取游戏列表, 共 {} 款", list.size()); return new GameListResponse(list); } @Override public RoomListResponse getRoomList(String gameId, String status) { // 校验游戏存在且启用 GameConfigEntity gameConfig = gameConfigRepository.findByGameId(gameId) .orElseThrow(() -> new BusinessException(ErrorCode.GAME_NOT_FOUND)); if (!gameConfig.getEnabled()) { throw new BusinessException(ErrorCode.GAME_NOT_FOUND); } // 查询房间:有状态筛选则按状态查,否则查所有 List rooms; if (status != null && !status.isBlank()) { rooms = gameRoomRepository.findByGameIdAndStatus(gameId, status); } else { // 默认不返回已结束的房间 rooms = gameRoomRepository.findByGameId(gameId).stream() .filter(r -> !"finished".equals(r.getStatus())) .toList(); } // 组装每个房间的玩家信息和房主昵称 List list = new ArrayList<>(); for (GameRoomEntity room : rooms) { List players = buildRoomPlayers(room.getId()); String creatorName = getNickname(room.getCreatorId()); list.add(RoomItemResponse.from(room, players, gameConfig.getIcon(), creatorName)); } log.info("[游戏服务] 获取房间列表, gameId={}, status={}, 共 {} 个", gameId, status, list.size()); return new RoomListResponse(list); } @Override @Transactional public CreateRoomResponse createRoom(CreateRoomRequest request, String creatorId) { // 校验游戏存在且启用 GameConfigEntity gameConfig = gameConfigRepository.findByGameId(request.gameId()) .orElseThrow(() -> new BusinessException(ErrorCode.GAME_NOT_FOUND)); if (!gameConfig.getEnabled()) { throw new BusinessException(ErrorCode.GAME_NOT_FOUND); } // 校验 mode 在可选列表中 List modes = JSON.parseArray(gameConfig.getModes(), String.class); if (!modes.contains(request.mode())) { throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的模式: " + request.mode()); } // 校验 stakes 在可选列表中 List stakesOptions = JSON.parseArray(gameConfig.getStakesOptions(), Integer.class); if (!stakesOptions.contains(request.stakes())) { throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的底注: " + request.stakes()); } // 检查用户是否已在其他房间中 if (roomPlayerRepository.existsByUserId(creatorId)) { throw new BusinessException(ErrorCode.ALREADY_IN_ROOM); } // 生成桌号 Integer maxRoomNumber = gameRoomRepository.findMaxRoomNumberByGameId(request.gameId()); int nextRoomNumber = (maxRoomNumber == null) ? 1 : maxRoomNumber + 1; // 生成桌名 String roomName = (request.name() != null && !request.name().isBlank()) ? request.name() : "桌 " + String.format("%02d", nextRoomNumber); // 创建房间 GameRoomEntity room = new GameRoomEntity() .setGameId(request.gameId()) .setRoomNumber(nextRoomNumber) .setName(roomName) .setMaxPlayers(request.maxPlayers()) .setMode(request.mode()) .setStakes(request.stakes()) .setStatus("waiting") .setCreatorId(creatorId) .setAllowSpectate(request.allowSpectate()); // 密码加密(如果提供了密码) if (request.password() != null && !request.password().isBlank()) { room.setPassword(passwordEncoder.encode(request.password())); } gameRoomRepository.save(room); // 房主自动加入房间(座位 1,已准备) RoomPlayerEntity creatorPlayer = new RoomPlayerEntity() .setRoomId(room.getId()) .setUserId(creatorId) .setSeatNumber(1) .setReadyStatus(true) .setJoinTime(LocalDateTime.now()); roomPlayerRepository.save(creatorPlayer); // 组装响应 List players = buildRoomPlayers(room.getId()); String creatorName = getNickname(creatorId); RoomItemResponse roomResponse = RoomItemResponse.from(room, players, gameConfig.getIcon(), creatorName); log.info("[游戏服务] 创建房间成功, roomId={}, gameId={}, creatorId={}", room.getId(), request.gameId(), creatorId); // 通知所有在线用户:新房间创建 sseEventPublisher.broadcast(SseEventType.ROOM_CREATED, roomResponse); return new CreateRoomResponse(roomResponse); } @Override @Transactional public RoomItemResponse joinRoom(JoinRoomRequest request, String userId) { GameRoomEntity room = gameRoomRepository.findById(request.roomId()) .orElseThrow(() -> new BusinessException(ErrorCode.ROOM_NOT_FOUND)); // 检查房间状态 if (!"waiting".equals(room.getStatus())) { throw new BusinessException(ErrorCode.ROOM_ALREADY_STARTED); } // 检查密码 if (room.getPassword() != null && !room.getPassword().isBlank()) { if (request.password() == null || request.password().isBlank()) { throw new BusinessException(ErrorCode.ROOM_PASSWORD_ERROR, "请输入房间密码"); } if (!passwordEncoder.matches(request.password(), room.getPassword())) { throw new BusinessException(ErrorCode.ROOM_PASSWORD_ERROR); } } // 检查房间是否已满 int currentCount = roomPlayerRepository.countByRoomId(room.getId()); if (currentCount >= room.getMaxPlayers()) { throw new BusinessException(ErrorCode.ROOM_FULL); } // 检查用户是否已在其他房间 if (roomPlayerRepository.existsByUserId(userId)) { throw new BusinessException(ErrorCode.ALREADY_IN_ROOM); } // 分配座位号 List existingPlayers = roomPlayerRepository.findByRoomId(room.getId()); int nextSeat = 1; if (!existingPlayers.isEmpty()) { int maxSeat = existingPlayers.stream() .mapToInt(RoomPlayerEntity::getSeatNumber) .max() .orElse(0); nextSeat = maxSeat + 1; } // 加入房间 RoomPlayerEntity player = new RoomPlayerEntity() .setRoomId(room.getId()) .setUserId(userId) .setSeatNumber(nextSeat) .setReadyStatus(false) .setJoinTime(LocalDateTime.now()); roomPlayerRepository.save(player); // 组装响应 GameConfigEntity gameConfig = gameConfigRepository.findByGameId(room.getGameId()) .orElseThrow(() -> new BusinessException(ErrorCode.GAME_NOT_FOUND)); List players = buildRoomPlayers(room.getId()); String creatorName = getNickname(room.getCreatorId()); log.info("[游戏服务] 玩家加入房间, roomId={}, userId={}", room.getId(), userId); RoomItemResponse response = RoomItemResponse.from(room, players, gameConfig.getIcon(), creatorName); // 通知所有在线用户:房间信息更新 sseEventPublisher.broadcast(SseEventType.ROOM_UPDATED, response); return response; } @Override @Transactional public void leaveRoom(String roomId, String userId) { GameRoomEntity room = gameRoomRepository.findById(roomId) .orElseThrow(() -> new BusinessException(ErrorCode.ROOM_NOT_FOUND)); // 检查玩家是否在房间中 RoomPlayerEntity player = roomPlayerRepository.findByRoomIdAndUserId(roomId, userId) .orElseThrow(() -> new BusinessException(ErrorCode.PLAYER_NOT_IN_ROOM)); roomPlayerRepository.delete(player); // remaining 需要在后面的 SSE 推送中用到,提取到方法级 List remaining = roomPlayerRepository.findByRoomId(roomId); if (remaining.isEmpty()) { // 无人了,房间标记为结束 room.setStatus("finished"); gameRoomRepository.save(room); log.info("[游戏服务] 房间无人,标记为结束, 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); } log.info("[游戏服务] 玩家离开房间, roomId={}, userId={}", roomId, userId); // 获取剩余玩家重新组装房间信息并推送事件 if (remaining.isEmpty()) { sseEventPublisher.broadcast(SseEventType.ROOM_REMOVED, Map.of("roomId", roomId)); } else { List players = buildRoomPlayers(roomId); GameConfigEntity gameConfig = gameConfigRepository.findByGameId(room.getGameId()) .orElse(null); String icon = gameConfig != null ? gameConfig.getIcon() : ""; String creatorName = getNickname(room.getCreatorId()); RoomItemResponse response = RoomItemResponse.from(room, players, icon, creatorName); sseEventPublisher.broadcast(SseEventType.ROOM_UPDATED, response); } } @Override @Transactional public void kickPlayer(String roomId, String ownerId, String targetUserId) { GameRoomEntity room = gameRoomRepository.findById(roomId) .orElseThrow(() -> new BusinessException(ErrorCode.ROOM_NOT_FOUND)); // 校验操作者是房主 if (!ownerId.equals(room.getCreatorId())) { throw new BusinessException(ErrorCode.NOT_ROOM_OWNER); } // 不能踢自己 if (ownerId.equals(targetUserId)) { throw new BusinessException(ErrorCode.BAD_REQUEST, "不能踢出自己,请使用离开房间"); } // 检查目标玩家是否在房间中 RoomPlayerEntity target = roomPlayerRepository.findByRoomIdAndUserId(roomId, targetUserId) .orElseThrow(() -> new BusinessException(ErrorCode.PLAYER_NOT_IN_ROOM)); roomPlayerRepository.delete(target); log.info("[游戏服务] 房主踢出玩家, roomId={}, targetUserId={}", roomId, targetUserId); // 通知被踢玩家 sseEventPublisher.sendTo(targetUserId, SseEventType.KICKED, Map.of("roomId", roomId)); // 通知房间内其他玩家:房间更新 List players = buildRoomPlayers(roomId); GameConfigEntity gameConfig = gameConfigRepository.findByGameId(room.getGameId()) .orElse(null); String icon = gameConfig != null ? gameConfig.getIcon() : ""; String creatorName = getNickname(room.getCreatorId()); RoomItemResponse response = RoomItemResponse.from(room, players, icon, creatorName); sseEventPublisher.broadcast(SseEventType.ROOM_UPDATED, response); } @Override @Transactional public boolean toggleReady(String roomId, String userId) { GameRoomEntity room = gameRoomRepository.findById(roomId) .orElseThrow(() -> new BusinessException(ErrorCode.ROOM_NOT_FOUND)); if (!"waiting".equals(room.getStatus())) { throw new BusinessException(ErrorCode.ROOM_ALREADY_STARTED); } RoomPlayerEntity player = roomPlayerRepository.findByRoomIdAndUserId(roomId, userId) .orElseThrow(() -> new BusinessException(ErrorCode.PLAYER_NOT_IN_ROOM)); boolean newStatus = !player.getReadyStatus(); player.setReadyStatus(newStatus); roomPlayerRepository.save(player); log.info("[游戏服务] 玩家{}准备, roomId={}, userId={}", newStatus ? "" : "取消", roomId, userId); // 通知房间更新 List players = buildRoomPlayers(roomId); GameConfigEntity gameConfig = gameConfigRepository.findByGameId(room.getGameId()) .orElse(null); String icon = gameConfig != null ? gameConfig.getIcon() : ""; String creatorName = getNickname(room.getCreatorId()); RoomItemResponse response = RoomItemResponse.from(room, players, icon, creatorName); sseEventPublisher.broadcast(SseEventType.ROOM_UPDATED, response); return newStatus; } @Override @Transactional public void startGame(String roomId, String ownerId) { GameRoomEntity room = gameRoomRepository.findById(roomId) .orElseThrow(() -> new BusinessException(ErrorCode.ROOM_NOT_FOUND)); // 校验操作者是房主 if (!ownerId.equals(room.getCreatorId())) { throw new BusinessException(ErrorCode.NOT_ROOM_OWNER); } // 校验房间状态 if (!"waiting".equals(room.getStatus())) { throw new BusinessException(ErrorCode.ROOM_ALREADY_STARTED); } // 校验玩家人数 >= 2 List players = roomPlayerRepository.findByRoomId(roomId); if (players.size() < 2) { throw new BusinessException(ErrorCode.PLAYER_COUNT_INSUFFICIENT); } // 校验所有玩家已准备(房主除外,房主默认准备) for (RoomPlayerEntity player : players) { if (!player.getReadyStatus() && !player.getUserId().equals(room.getCreatorId())) { 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()); // 通知房间状态更新 List allPlayers = buildRoomPlayers(roomId); GameConfigEntity gameConfig = gameConfigRepository.findByGameId(room.getGameId()) .orElse(null); String icon = gameConfig != null ? gameConfig.getIcon() : ""; 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 public RoomDetailResponse getRoomDetail(String roomId) { GameRoomEntity room = gameRoomRepository.findById(roomId) .orElseThrow(() -> new BusinessException(ErrorCode.ROOM_NOT_FOUND)); GameConfigEntity gameConfig = gameConfigRepository.findByGameId(room.getGameId()) .orElseThrow(() -> new BusinessException(ErrorCode.GAME_NOT_FOUND)); // 组装玩家详情(含座位号和准备状态) List roomPlayers = roomPlayerRepository.findByRoomId(roomId); List userIds = roomPlayers.stream() .map(RoomPlayerEntity::getUserId).toList(); Map userMap = userRepository.findByAccountIdIn(userIds).stream() .collect(Collectors.toMap(u -> u.getAccount().getId(), u -> u)); List playerDetails = roomPlayers.stream() .map(rp -> { UserEntity user = userMap.get(rp.getUserId()); return new RoomDetailResponse.RoomPlayerDetail( rp.getUserId(), user != null ? user.getAvatar() : "", user != null ? user.getNickName() : "未知玩家", rp.getSeatNumber(), rp.getReadyStatus() ); }).toList(); String creatorName = getNickname(room.getCreatorId()); // 对局中或有对局记录时,查询棋盘状态 int[][] boardState = null; String currentTurn = null; List moves = List.of(); if ("playing".equals(room.getStatus()) || "finished".equals(room.getStatus())) { // 查询走棋记录 List moveEntities = gameMoveRepository.findByRoomIdOrderByMoveIndexAsc(roomId); moves = moveEntities.stream() .map(m -> new RoomDetailResponse.MoveItem( m.getMoveIndex(), m.getPlayerId(), m.getRowNum(), m.getColNum())) .toList(); // 从走棋记录重建棋盘 boardState = GomokuGameService.rebuildBoard(moveEntities, GomokuGameService.BOARD_SIZE); // currentTurn 由服务端运行时管理,HTTP 快照中暂不返回 } log.info("[游戏服务] 获取房间详情, roomId={}, status={}, 玩家数={}", roomId, room.getStatus(), playerDetails.size()); return new RoomDetailResponse( room.getId(), room.getName(), room.getGameId(), gameConfig.getIcon(), gameConfig.getName(), room.getMaxPlayers(), room.getMode(), room.getStakes(), room.getStatus(), room.getCreatorId(), creatorName, room.getAllowSpectate(), playerDetails, boardState, currentTurn, moves ); } // ==================== 私有辅助方法 ==================== /** * 组装房间内的玩家信息列表 * * @param roomId 房间 ID * @return 玩家响应列表(含头像和昵称) */ private List buildRoomPlayers(String roomId) { List roomPlayers = roomPlayerRepository.findByRoomId(roomId); if (roomPlayers.isEmpty()) { return List.of(); } // 批量查询玩家用户信息 List userIds = roomPlayers.stream() .map(RoomPlayerEntity::getUserId) .toList(); List users = userRepository.findByAccountIdIn(userIds); Map 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(); } /** * 获取用户昵称 * * @param accountId 账号 ID * @return 昵称,查不到则返回 "未知玩家" */ private String getNickname(String accountId) { return userRepository.findByAccountId(accountId) .map(UserEntity::getNickName) .orElse("未知玩家"); } }