feat: 游戏房间
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
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.service.GameService;
|
||||
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;
|
||||
|
||||
@Override
|
||||
public GameListResponse getGameList() {
|
||||
List<GameConfigEntity> configs = gameConfigRepository.findByEnabledTrueOrderBySortOrderAsc();
|
||||
List<GameItemResponse> 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<GameRoomEntity> 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<RoomItemResponse> list = new ArrayList<>();
|
||||
for (GameRoomEntity room : rooms) {
|
||||
List<RoomPlayerResponse> 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<String> modes = JSON.parseArray(gameConfig.getModes(), String.class);
|
||||
if (!modes.contains(request.mode())) {
|
||||
throw new BusinessException(ErrorCode.BAD_REQUEST, "不支持的模式: " + request.mode());
|
||||
}
|
||||
|
||||
// 校验 stakes 在可选列表中
|
||||
List<Integer> 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);
|
||||
|
||||
// 密码加密(如果提供了密码)
|
||||
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<RoomPlayerResponse> 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);
|
||||
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<RoomPlayerEntity> 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<RoomPlayerResponse> players = buildRoomPlayers(room.getId());
|
||||
String creatorName = getNickname(room.getCreatorId());
|
||||
|
||||
log.info("[游戏服务] 玩家加入房间, roomId={}, userId={}", room.getId(), userId);
|
||||
return RoomItemResponse.from(room, players, gameConfig.getIcon(), creatorName);
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
List<RoomPlayerEntity> 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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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);
|
||||
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<RoomPlayerEntity> 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());
|
||||
}
|
||||
|
||||
// ==================== 私有辅助方法 ====================
|
||||
|
||||
/**
|
||||
* 组装房间内的玩家信息列表
|
||||
*
|
||||
* @param roomId 房间 ID
|
||||
* @return 玩家响应列表(含头像和昵称)
|
||||
*/
|
||||
private List<RoomPlayerResponse> buildRoomPlayers(String roomId) {
|
||||
List<RoomPlayerEntity> roomPlayers = roomPlayerRepository.findByRoomId(roomId);
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户昵称
|
||||
*
|
||||
* @param accountId 账号 ID
|
||||
* @return 昵称,查不到则返回 "未知玩家"
|
||||
*/
|
||||
private String getNickname(String accountId) {
|
||||
return userRepository.findByAccountId(accountId)
|
||||
.map(UserEntity::getNickName)
|
||||
.orElse("未知玩家");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user