diff --git a/backend/src/CollabApp.Application/Services/Room/RoomService.cs b/backend/src/CollabApp.Application/Services/Room/RoomService.cs index 92794d8453d903bfd330deb8c2c3f037d803bb74..446d42a220b9dcf506f1c617b9c140b138b80f51 100644 --- a/backend/src/CollabApp.Application/Services/Room/RoomService.cs +++ b/backend/src/CollabApp.Application/Services/Room/RoomService.cs @@ -239,7 +239,7 @@ public class RoomService : IRoomService }; } - // 检查用户是否已在房间中 + // 检查用户是否已在房间中(包括软删除的记录) var existingPlayer = await _roomPlayerRepository.GetSingleAsync(rp => rp.RoomId == roomId && rp.UserId == userId); if (existingPlayer != null) { @@ -251,6 +251,42 @@ public class RoomService : IRoomService }; } + // 检查是否存在已软删除的记录,如果有,重新激活而不是创建新记录 + // 使用原生查询来检查软删除的记录 + var softDeletedPlayerQuery = await _roomPlayerRepository.GetManyAsync(rp => true); // 获取所有记录 + var allPlayers = softDeletedPlayerQuery.ToList(); + var softDeletedPlayer = allPlayers.FirstOrDefault(p => p.RoomId == roomId && p.UserId == userId && p.IsDeleted); + + if (softDeletedPlayer != null) + { + // 重新激活已软删除的记录 + softDeletedPlayer.IsDeleted = false; + softDeletedPlayer.SetReady(false); // 重置准备状态 + softDeletedPlayer.UpdatedAt = DateTime.UtcNow; + + // 重新分配加入顺序和颜色 + var currentPlayersInRoom = await _roomPlayerRepository.GetManyAsync(rp => rp.RoomId == roomId); + var playersList = currentPlayersInRoom.ToList(); + softDeletedPlayer.SetJoinOrder(playersList.Count + 1); + softDeletedPlayer.SetPlayerColor(GetNextAvailableColor(playersList)); + + await _roomPlayerRepository.UpdateAsync(softDeletedPlayer); + await _roomPlayerRepository.SaveChangesAsync(); + + // 更新房间的玩家数量 + room.IncrementPlayerCount(); + await _roomRepository.UpdateAsync(room); + await _roomRepository.SaveChangesAsync(); + + var reactivatedRoomDetail = await GetRoomDetailAsync(roomId, userId); + return new JoinRoomResult + { + Success = true, + Message = "成功重新加入房间", + RoomDetail = reactivatedRoomDetail + }; + } + // 验证用户是否存在 var user = await _userRepository.GetByIdAsync(userId); if (user == null) @@ -264,15 +300,15 @@ public class RoomService : IRoomService } // 获取当前房间玩家数量 - var currentPlayersInRoom = await _roomPlayerRepository.GetManyAsync(rp => rp.RoomId == roomId); - var playersList = currentPlayersInRoom.ToList(); + var currentActivePlayersInRoom = await _roomPlayerRepository.GetManyAsync(rp => rp.RoomId == roomId); + var activePlayersList = currentActivePlayersInRoom.ToList(); - // 创建RoomPlayer记录 + // 创建新的RoomPlayer记录 var roomPlayer = RoomPlayer.CreateRoomPlayer( roomId, userId, - playersList.Count + 1, // 设置加入顺序 - GetNextAvailableColor(playersList) + activePlayersList.Count + 1, // 设置加入顺序 + GetNextAvailableColor(activePlayersList) ); await _roomPlayerRepository.AddAsync(roomPlayer); diff --git a/backend/src/CollabApp.Infrastructure/Data/ApplicationDbContext.cs b/backend/src/CollabApp.Infrastructure/Data/ApplicationDbContext.cs index 9615dd4b05af8b9ff99cebba7405d76adcdf67c4..19ec3029e46092782746235ddfa0f639f943bca2 100644 --- a/backend/src/CollabApp.Infrastructure/Data/ApplicationDbContext.cs +++ b/backend/src/CollabApp.Infrastructure/Data/ApplicationDbContext.cs @@ -159,8 +159,11 @@ public class ApplicationDbContext : DbContext .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.Cascade); - // 复合唯一索引 - 确保同一用户在同一房间只能有一条记录 - entity.HasIndex(e => new { e.RoomId, e.UserId }).IsUnique().HasDatabaseName("IX_RoomPlayers_RoomId_UserId"); + // 复合唯一索引 - 确保同一用户在同一房间只能有一条记录(只对未删除的记录生效) + entity.HasIndex(e => new { e.RoomId, e.UserId }) + .IsUnique() + .HasDatabaseName("IX_RoomPlayers_RoomId_UserId") + .HasFilter("NOT is_deleted"); // 只对未删除的记录应用唯一约束 entity.HasIndex(e => e.JoinOrder).HasDatabaseName("IX_RoomPlayers_JoinOrder"); }); diff --git a/backend/src/CollabApp.Infrastructure/Migrations/20250819163316_FixRoomPlayerUniqueConstraint.Designer.cs b/backend/src/CollabApp.Infrastructure/Migrations/20250819163316_FixRoomPlayerUniqueConstraint.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..ab3264b30b459615c41058e0b0d70393dffeba78 --- /dev/null +++ b/backend/src/CollabApp.Infrastructure/Migrations/20250819163316_FixRoomPlayerUniqueConstraint.Designer.cs @@ -0,0 +1,1058 @@ +// +using System; +using CollabApp.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CollabApp.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250819163316_FixRoomPlayerUniqueConstraint")] + partial class FixRoomPlayerUniqueConstraint + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CollabApp.Domain.Entities.Auth.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessToken") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("access_token"); + + b.Property("AccessTokenExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("access_token_expires_at"); + + b.Property("AvatarUrl") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("avatar_url"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeviceInfo") + .HasColumnType("json") + .HasColumnName("device_info"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastActivityAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_activity_at"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_login_at"); + + b.Property("Nickname") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("nickname"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("password_hash"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("password_salt"); + + b.Property("PrivacySettings") + .HasColumnType("json") + .HasColumnName("privacy_settings"); + + b.Property("RefreshToken") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("refresh_token"); + + b.Property("RefreshTokenExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("refresh_token_expires_at"); + + b.Property("RememberMe") + .HasColumnType("boolean") + .HasColumnName("remember_me"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("TokenRevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("token_revoked_at"); + + b.Property("TokenRevokedReason") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("token_revoked_reason"); + + b.Property("TokenStatus") + .HasColumnType("integer") + .HasColumnName("token_status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("username"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .HasDatabaseName("IX_Users_AccessToken") + .HasFilter("[access_token] IS NOT NULL"); + + b.HasIndex("LastActivityAt") + .HasDatabaseName("IX_Users_LastActivity"); + + b.HasIndex("RefreshToken") + .HasDatabaseName("IX_Users_RefreshToken") + .HasFilter("[refresh_token] IS NOT NULL"); + + b.HasIndex("Status") + .HasDatabaseName("IX_Users_Status"); + + b.HasIndex("Username") + .IsUnique() + .HasDatabaseName("IX_Users_Username"); + + b.HasIndex("TokenStatus", "AccessTokenExpiresAt") + .HasDatabaseName("IX_Users_TokenStatus_AccessTokenExpires"); + + b.HasIndex("TokenStatus", "RefreshTokenExpiresAt") + .HasDatabaseName("IX_Users_TokenStatus_RefreshTokenExpires"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Auth.UserStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CurrentRank") + .HasColumnType("integer") + .HasColumnName("current_rank"); + + b.Property("HighestRank") + .HasColumnType("integer") + .HasColumnName("highest_rank"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Losses") + .HasColumnType("integer") + .HasColumnName("losses"); + + b.Property("MaxArea") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("max_area"); + + b.Property("TotalGames") + .HasColumnType("integer") + .HasColumnName("total_games"); + + b.Property("TotalPlayTime") + .HasColumnType("integer") + .HasColumnName("total_play_time"); + + b.Property("TotalScore") + .HasColumnType("integer") + .HasColumnName("total_score"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("WinRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)") + .HasColumnName("win_rate"); + + b.Property("Wins") + .HasColumnType("integer") + .HasColumnName("wins"); + + b.HasKey("Id"); + + b.HasIndex("CurrentRank") + .HasDatabaseName("IX_UserStatistics_CurrentRank"); + + b.HasIndex("TotalScore") + .HasDatabaseName("IX_UserStatistics_TotalScore"); + + b.HasIndex("UserId") + .IsUnique() + .HasDatabaseName("IX_UserStatistics_UserId"); + + b.HasIndex("WinRate") + .HasDatabaseName("IX_UserStatistics_WinRate"); + + b.ToTable("user_statistics"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Game.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Duration") + .HasColumnType("integer") + .HasColumnName("duration"); + + b.Property("EnableDynamicBalance") + .HasColumnType("boolean") + .HasColumnName("enable_dynamic_balance"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_at"); + + b.Property("GameData") + .HasColumnType("json") + .HasColumnName("game_data"); + + b.Property("GameMode") + .IsRequired() + .HasColumnType("text") + .HasColumnName("game_mode"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("MapHeight") + .HasColumnType("integer") + .HasColumnName("map_height"); + + b.Property("MapShape") + .IsRequired() + .HasColumnType("text") + .HasColumnName("map_shape"); + + b.Property("MapWidth") + .HasColumnType("integer") + .HasColumnName("map_width"); + + b.Property("MaxPowerUps") + .HasColumnType("integer") + .HasColumnName("max_powerups"); + + b.Property("PowerUpSpawnInterval") + .HasColumnType("integer") + .HasColumnName("powerup_spawn_interval"); + + b.Property("RoomId") + .HasColumnType("uuid") + .HasColumnName("room_id"); + + b.Property("SpecialEventChance") + .HasColumnType("integer") + .HasColumnName("special_event_chance"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("WinnerId") + .HasColumnType("uuid") + .HasColumnName("winner_id"); + + b.HasKey("Id"); + + b.HasIndex("FinishedAt") + .HasDatabaseName("IX_Games_FinishedAt"); + + b.HasIndex("RoomId") + .HasDatabaseName("IX_Games_RoomId"); + + b.HasIndex("StartedAt") + .HasDatabaseName("IX_Games_StartedAt"); + + b.HasIndex("Status") + .HasDatabaseName("IX_Games_Status"); + + b.HasIndex("WinnerId") + .HasDatabaseName("IX_Games_WinnerId"); + + b.ToTable("games"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Game.GameAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionData") + .IsRequired() + .HasColumnType("json") + .HasColumnName("action_data"); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("action_type"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("GameId") + .HasColumnType("uuid") + .HasColumnName("game_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Timestamp") + .HasColumnType("bigint") + .HasColumnName("timestamp"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("ActionType") + .HasDatabaseName("IX_GameActions_ActionType"); + + b.HasIndex("GameId") + .HasDatabaseName("IX_GameActions_GameId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_GameActions_Timestamp"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_GameActions_UserId"); + + b.HasIndex("GameId", "Timestamp") + .HasDatabaseName("IX_GameActions_GameId_Timestamp"); + + b.ToTable("game_actions"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Game.GamePlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionsCount") + .HasColumnType("integer") + .HasColumnName("actions_count"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CurrentPowerUp") + .HasColumnType("text") + .HasColumnName("current_powerup"); + + b.Property("DeathCount") + .HasColumnType("integer") + .HasColumnName("death_count"); + + b.Property("FinalArea") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("final_area"); + + b.Property("FinalRank") + .HasColumnType("integer") + .HasColumnName("final_rank"); + + b.Property("GameId") + .HasColumnType("uuid") + .HasColumnName("game_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("KillCount") + .HasColumnType("integer") + .HasColumnName("kill_count"); + + b.Property("MaxTerritoryArea") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("max_territory_area"); + + b.Property("PlayTime") + .HasColumnType("integer") + .HasColumnName("play_time"); + + b.Property("PlayerColor") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character varying(7)") + .HasColumnName("player_color"); + + b.Property("PositionX") + .HasColumnType("real") + .HasColumnName("position_x"); + + b.Property("PositionY") + .HasColumnType("real") + .HasColumnName("position_y"); + + b.Property("PowerUpUsageCount") + .HasColumnType("integer") + .HasColumnName("powerup_usage_count"); + + b.Property("RespawnTimestamp") + .HasColumnType("bigint") + .HasColumnName("respawn_timestamp"); + + b.Property("ScoreChange") + .HasColumnType("integer") + .HasColumnName("score_change"); + + b.Property("SpawnX") + .HasColumnType("real") + .HasColumnName("spawn_x"); + + b.Property("SpawnY") + .HasColumnType("real") + .HasColumnName("spawn_y"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("TeamId") + .HasColumnType("integer") + .HasColumnName("team_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FinalArea") + .HasDatabaseName("IX_GamePlayers_FinalArea"); + + b.HasIndex("FinalRank") + .HasDatabaseName("IX_GamePlayers_FinalRank"); + + b.HasIndex("ScoreChange") + .HasDatabaseName("IX_GamePlayers_ScoreChange"); + + b.HasIndex("UserId"); + + b.HasIndex("GameId", "UserId") + .IsUnique() + .HasDatabaseName("IX_GamePlayers_GameId_UserId"); + + b.ToTable("game_players"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Data") + .HasColumnType("json") + .HasColumnName("data"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("IsRead") + .HasColumnType("boolean") + .HasColumnName("is_read"); + + b.Property("NotificationType") + .HasColumnType("integer") + .HasColumnName("notification_type"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("read_at"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_Notifications_CreatedAt"); + + b.HasIndex("NotificationType") + .HasDatabaseName("IX_Notifications_Type"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_Notifications_UserId"); + + b.HasIndex("UserId", "CreatedAt") + .HasDatabaseName("IX_Notifications_UserId_CreatedAt"); + + b.HasIndex("UserId", "IsRead") + .HasDatabaseName("IX_Notifications_UserId_IsRead"); + + b.ToTable("notifications"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Ranking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CurrentRank") + .HasColumnType("integer") + .HasColumnName("current_rank"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("period_end"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone") + .HasColumnName("period_start"); + + b.Property("RankingType") + .HasColumnType("integer") + .HasColumnName("ranking_type"); + + b.Property("Score") + .HasColumnType("integer") + .HasColumnName("score"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UpdatedAt") + .HasDatabaseName("IX_Rankings_UpdatedAt"); + + b.HasIndex("RankingType", "CurrentRank") + .HasDatabaseName("IX_Rankings_Type_Rank"); + + b.HasIndex("RankingType", "Score") + .HasDatabaseName("IX_Rankings_Type_Score"); + + b.HasIndex("UserId", "RankingType", "PeriodStart", "PeriodEnd") + .IsUnique() + .HasDatabaseName("IX_Rankings_UserId_Type_Period"); + + b.ToTable("rankings"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.RankingHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Rank") + .HasColumnType("integer") + .HasColumnName("rank"); + + b.Property("RankingType") + .HasColumnType("integer") + .HasColumnName("ranking_type"); + + b.Property("RecordedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("recorded_at"); + + b.Property("Score") + .HasColumnType("integer") + .HasColumnName("score"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_RankingHistories_UserId"); + + b.HasIndex("RankingType", "RecordedAt") + .HasDatabaseName("IX_RankingHistories_Type_RecordedAt"); + + b.HasIndex("UserId", "RankingType", "RecordedAt") + .HasDatabaseName("IX_RankingHistories_UserId_Type_RecordedAt"); + + b.ToTable("ranking_histories"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Room.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CurrentPlayers") + .HasColumnType("integer") + .HasColumnName("current_players"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("IsPrivate") + .HasColumnType("boolean") + .HasColumnName("is_private"); + + b.Property("MaxPlayers") + .HasColumnType("integer") + .HasColumnName("max_players"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("uuid") + .HasColumnName("owner_id"); + + b.Property("Password") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("password"); + + b.Property("Settings") + .HasColumnType("json") + .HasColumnName("settings"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_Rooms_CreatedAt"); + + b.HasIndex("OwnerId") + .HasDatabaseName("IX_Rooms_OwnerId"); + + b.HasIndex("Status") + .HasDatabaseName("IX_Rooms_Status"); + + b.HasIndex("Status", "IsPrivate") + .HasDatabaseName("IX_Rooms_Status_IsPrivate"); + + b.ToTable("rooms"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Room.RoomMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("MessageType") + .HasColumnType("integer") + .HasColumnName("message_type"); + + b.Property("RoomId") + .HasColumnType("uuid") + .HasColumnName("room_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_RoomMessages_CreatedAt"); + + b.HasIndex("RoomId") + .HasDatabaseName("IX_RoomMessages_RoomId"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_RoomMessages_UserId"); + + b.HasIndex("RoomId", "CreatedAt") + .HasDatabaseName("IX_RoomMessages_RoomId_CreatedAt"); + + b.ToTable("room_messages"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Room.RoomPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("IsReady") + .HasColumnType("boolean") + .HasColumnName("is_ready"); + + b.Property("JoinOrder") + .HasColumnType("integer") + .HasColumnName("join_order"); + + b.Property("PlayerColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)") + .HasColumnName("player_color"); + + b.Property("RoomId") + .HasColumnType("uuid") + .HasColumnName("room_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("JoinOrder") + .HasDatabaseName("IX_RoomPlayers_JoinOrder"); + + b.HasIndex("UserId"); + + b.HasIndex("RoomId", "UserId") + .IsUnique() + .HasDatabaseName("IX_RoomPlayers_RoomId_UserId") + .HasFilter("NOT is_deleted"); + + b.ToTable("room_players"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Auth.UserStatistics", b => + { + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithOne("Statistics") + .HasForeignKey("CollabApp.Domain.Entities.Auth.UserStatistics", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Game.Game", b => + { + b.HasOne("CollabApp.Domain.Entities.Room.Room", "Room") + .WithMany("Games") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CollabApp.Domain.Entities.Auth.User", "Winner") + .WithMany() + .HasForeignKey("WinnerId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Room"); + + b.Navigation("Winner"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Game.GameAction", b => + { + b.HasOne("CollabApp.Domain.Entities.Game.Game", "Game") + .WithMany("Actions") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Game.GamePlayer", b => + { + b.HasOne("CollabApp.Domain.Entities.Game.Game", "Game") + .WithMany("Players") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithMany("GamePlayers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Notification", b => + { + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Ranking", b => + { + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.RankingHistory", b => + { + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Room.Room", b => + { + b.HasOne("CollabApp.Domain.Entities.Auth.User", "Owner") + .WithMany("OwnedRooms") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Room.RoomMessage", b => + { + b.HasOne("CollabApp.Domain.Entities.Room.Room", "Room") + .WithMany("Messages") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Room.RoomPlayer", b => + { + b.HasOne("CollabApp.Domain.Entities.Room.Room", "Room") + .WithMany("Players") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithMany("RoomPlayers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Auth.User", b => + { + b.Navigation("GamePlayers"); + + b.Navigation("Notifications"); + + b.Navigation("OwnedRooms"); + + b.Navigation("RoomPlayers"); + + b.Navigation("Statistics"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Game.Game", b => + { + b.Navigation("Actions"); + + b.Navigation("Players"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Room.Room", b => + { + b.Navigation("Games"); + + b.Navigation("Messages"); + + b.Navigation("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/CollabApp.Infrastructure/Migrations/20250819163316_FixRoomPlayerUniqueConstraint.cs b/backend/src/CollabApp.Infrastructure/Migrations/20250819163316_FixRoomPlayerUniqueConstraint.cs new file mode 100644 index 0000000000000000000000000000000000000000..878007e586c3eea6cbaecc8888acab7af02da683 --- /dev/null +++ b/backend/src/CollabApp.Infrastructure/Migrations/20250819163316_FixRoomPlayerUniqueConstraint.cs @@ -0,0 +1,652 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CollabApp.Infrastructure.Migrations +{ + /// + public partial class FixRoomPlayerUniqueConstraint : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + username = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + password_hash = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + password_salt = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + nickname = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + avatar_url = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + privacy_settings = table.Column(type: "json", nullable: true), + last_login_at = table.Column(type: "timestamp with time zone", nullable: true), + status = table.Column(type: "integer", nullable: false), + access_token = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + refresh_token = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + access_token_expires_at = table.Column(type: "timestamp with time zone", nullable: true), + refresh_token_expires_at = table.Column(type: "timestamp with time zone", nullable: true), + remember_me = table.Column(type: "boolean", nullable: false), + token_status = table.Column(type: "integer", nullable: false), + last_activity_at = table.Column(type: "timestamp with time zone", nullable: true), + device_info = table.Column(type: "json", nullable: true), + token_revoked_reason = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), + token_revoked_at = table.Column(type: "timestamp with time zone", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + is_deleted = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "notifications", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: false), + notification_type = table.Column(type: "integer", nullable: false), + title = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + content = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + is_read = table.Column(type: "boolean", nullable: false), + data = table.Column(type: "json", nullable: true), + read_at = table.Column(type: "timestamp with time zone", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + is_deleted = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_notifications", x => x.Id); + table.ForeignKey( + name: "FK_notifications_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ranking_histories", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: false), + ranking_type = table.Column(type: "integer", nullable: false), + rank = table.Column(type: "integer", nullable: false), + score = table.Column(type: "integer", nullable: false), + recorded_at = table.Column(type: "timestamp with time zone", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + is_deleted = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ranking_histories", x => x.Id); + table.ForeignKey( + name: "FK_ranking_histories_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "rankings", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: false), + ranking_type = table.Column(type: "integer", nullable: false), + current_rank = table.Column(type: "integer", nullable: false), + score = table.Column(type: "integer", nullable: false), + period_start = table.Column(type: "timestamp with time zone", nullable: false), + period_end = table.Column(type: "timestamp with time zone", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + is_deleted = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_rankings", x => x.Id); + table.ForeignKey( + name: "FK_rankings_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "rooms", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + owner_id = table.Column(type: "uuid", nullable: false), + max_players = table.Column(type: "integer", nullable: false), + current_players = table.Column(type: "integer", nullable: false), + password = table.Column(type: "character varying(255)", maxLength: 255, nullable: true), + is_private = table.Column(type: "boolean", nullable: false), + status = table.Column(type: "integer", nullable: false), + settings = table.Column(type: "json", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + is_deleted = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_rooms", x => x.Id); + table.ForeignKey( + name: "FK_rooms_users_owner_id", + column: x => x.owner_id, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "user_statistics", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: false), + total_games = table.Column(type: "integer", nullable: false), + wins = table.Column(type: "integer", nullable: false), + losses = table.Column(type: "integer", nullable: false), + win_rate = table.Column(type: "numeric(5,2)", precision: 5, scale: 2, nullable: false), + total_score = table.Column(type: "integer", nullable: false), + max_area = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false), + total_play_time = table.Column(type: "integer", nullable: false), + current_rank = table.Column(type: "integer", nullable: false), + highest_rank = table.Column(type: "integer", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + is_deleted = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_user_statistics", x => x.Id); + table.ForeignKey( + name: "FK_user_statistics_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "games", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + room_id = table.Column(type: "uuid", nullable: false), + game_mode = table.Column(type: "text", nullable: false), + map_width = table.Column(type: "integer", nullable: false), + map_height = table.Column(type: "integer", nullable: false), + duration = table.Column(type: "integer", nullable: false), + map_shape = table.Column(type: "text", nullable: false), + powerup_spawn_interval = table.Column(type: "integer", nullable: false), + max_powerups = table.Column(type: "integer", nullable: false), + special_event_chance = table.Column(type: "integer", nullable: false), + enable_dynamic_balance = table.Column(type: "boolean", nullable: false), + status = table.Column(type: "integer", nullable: false), + winner_id = table.Column(type: "uuid", nullable: true), + started_at = table.Column(type: "timestamp with time zone", nullable: true), + finished_at = table.Column(type: "timestamp with time zone", nullable: true), + game_data = table.Column(type: "json", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + is_deleted = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_games", x => x.Id); + table.ForeignKey( + name: "FK_games_rooms_room_id", + column: x => x.room_id, + principalTable: "rooms", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_games_users_winner_id", + column: x => x.winner_id, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "room_messages", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + room_id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: false), + message = table.Column(type: "text", nullable: false), + message_type = table.Column(type: "integer", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + is_deleted = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_room_messages", x => x.Id); + table.ForeignKey( + name: "FK_room_messages_rooms_room_id", + column: x => x.room_id, + principalTable: "rooms", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_room_messages_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "room_players", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + room_id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: false), + is_ready = table.Column(type: "boolean", nullable: false), + join_order = table.Column(type: "integer", nullable: true), + player_color = table.Column(type: "character varying(7)", maxLength: 7, nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + is_deleted = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_room_players", x => x.Id); + table.ForeignKey( + name: "FK_room_players_rooms_room_id", + column: x => x.room_id, + principalTable: "rooms", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_room_players_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "game_actions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + game_id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: false), + action_type = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + action_data = table.Column(type: "json", nullable: false), + timestamp = table.Column(type: "bigint", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + is_deleted = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_game_actions", x => x.Id); + table.ForeignKey( + name: "FK_game_actions_games_game_id", + column: x => x.game_id, + principalTable: "games", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_game_actions_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "game_players", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + game_id = table.Column(type: "uuid", nullable: false), + user_id = table.Column(type: "uuid", nullable: false), + player_color = table.Column(type: "character varying(7)", maxLength: 7, nullable: false), + final_area = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false), + final_rank = table.Column(type: "integer", nullable: true), + score_change = table.Column(type: "integer", nullable: false), + actions_count = table.Column(type: "integer", nullable: false), + play_time = table.Column(type: "integer", nullable: false), + spawn_x = table.Column(type: "real", nullable: false), + spawn_y = table.Column(type: "real", nullable: false), + position_x = table.Column(type: "real", nullable: false), + position_y = table.Column(type: "real", nullable: false), + status = table.Column(type: "integer", nullable: false), + death_count = table.Column(type: "integer", nullable: false), + kill_count = table.Column(type: "integer", nullable: false), + max_territory_area = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: false), + current_powerup = table.Column(type: "text", nullable: true), + powerup_usage_count = table.Column(type: "integer", nullable: false), + respawn_timestamp = table.Column(type: "bigint", nullable: true), + team_id = table.Column(type: "integer", nullable: true), + created_at = table.Column(type: "timestamp with time zone", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + is_deleted = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_game_players", x => x.Id); + table.ForeignKey( + name: "FK_game_players_games_game_id", + column: x => x.game_id, + principalTable: "games", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_game_players_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_GameActions_ActionType", + table: "game_actions", + column: "action_type"); + + migrationBuilder.CreateIndex( + name: "IX_GameActions_GameId", + table: "game_actions", + column: "game_id"); + + migrationBuilder.CreateIndex( + name: "IX_GameActions_GameId_Timestamp", + table: "game_actions", + columns: new[] { "game_id", "timestamp" }); + + migrationBuilder.CreateIndex( + name: "IX_GameActions_Timestamp", + table: "game_actions", + column: "timestamp"); + + migrationBuilder.CreateIndex( + name: "IX_GameActions_UserId", + table: "game_actions", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "IX_game_players_user_id", + table: "game_players", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "IX_GamePlayers_FinalArea", + table: "game_players", + column: "final_area"); + + migrationBuilder.CreateIndex( + name: "IX_GamePlayers_FinalRank", + table: "game_players", + column: "final_rank"); + + migrationBuilder.CreateIndex( + name: "IX_GamePlayers_GameId_UserId", + table: "game_players", + columns: new[] { "game_id", "user_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_GamePlayers_ScoreChange", + table: "game_players", + column: "score_change"); + + migrationBuilder.CreateIndex( + name: "IX_Games_FinishedAt", + table: "games", + column: "finished_at"); + + migrationBuilder.CreateIndex( + name: "IX_Games_RoomId", + table: "games", + column: "room_id"); + + migrationBuilder.CreateIndex( + name: "IX_Games_StartedAt", + table: "games", + column: "started_at"); + + migrationBuilder.CreateIndex( + name: "IX_Games_Status", + table: "games", + column: "status"); + + migrationBuilder.CreateIndex( + name: "IX_Games_WinnerId", + table: "games", + column: "winner_id"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_CreatedAt", + table: "notifications", + column: "created_at"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_Type", + table: "notifications", + column: "notification_type"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_UserId", + table: "notifications", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_UserId_CreatedAt", + table: "notifications", + columns: new[] { "user_id", "created_at" }); + + migrationBuilder.CreateIndex( + name: "IX_Notifications_UserId_IsRead", + table: "notifications", + columns: new[] { "user_id", "is_read" }); + + migrationBuilder.CreateIndex( + name: "IX_RankingHistories_Type_RecordedAt", + table: "ranking_histories", + columns: new[] { "ranking_type", "recorded_at" }); + + migrationBuilder.CreateIndex( + name: "IX_RankingHistories_UserId", + table: "ranking_histories", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "IX_RankingHistories_UserId_Type_RecordedAt", + table: "ranking_histories", + columns: new[] { "user_id", "ranking_type", "recorded_at" }); + + migrationBuilder.CreateIndex( + name: "IX_Rankings_Type_Rank", + table: "rankings", + columns: new[] { "ranking_type", "current_rank" }); + + migrationBuilder.CreateIndex( + name: "IX_Rankings_Type_Score", + table: "rankings", + columns: new[] { "ranking_type", "score" }); + + migrationBuilder.CreateIndex( + name: "IX_Rankings_UpdatedAt", + table: "rankings", + column: "updated_at"); + + migrationBuilder.CreateIndex( + name: "IX_Rankings_UserId_Type_Period", + table: "rankings", + columns: new[] { "user_id", "ranking_type", "period_start", "period_end" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RoomMessages_CreatedAt", + table: "room_messages", + column: "created_at"); + + migrationBuilder.CreateIndex( + name: "IX_RoomMessages_RoomId", + table: "room_messages", + column: "room_id"); + + migrationBuilder.CreateIndex( + name: "IX_RoomMessages_RoomId_CreatedAt", + table: "room_messages", + columns: new[] { "room_id", "created_at" }); + + migrationBuilder.CreateIndex( + name: "IX_RoomMessages_UserId", + table: "room_messages", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "IX_room_players_user_id", + table: "room_players", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "IX_RoomPlayers_JoinOrder", + table: "room_players", + column: "join_order"); + + migrationBuilder.CreateIndex( + name: "IX_RoomPlayers_RoomId_UserId", + table: "room_players", + columns: new[] { "room_id", "user_id" }, + unique: true, + filter: "NOT is_deleted"); + + migrationBuilder.CreateIndex( + name: "IX_Rooms_CreatedAt", + table: "rooms", + column: "created_at"); + + migrationBuilder.CreateIndex( + name: "IX_Rooms_OwnerId", + table: "rooms", + column: "owner_id"); + + migrationBuilder.CreateIndex( + name: "IX_Rooms_Status", + table: "rooms", + column: "status"); + + migrationBuilder.CreateIndex( + name: "IX_Rooms_Status_IsPrivate", + table: "rooms", + columns: new[] { "status", "is_private" }); + + migrationBuilder.CreateIndex( + name: "IX_UserStatistics_CurrentRank", + table: "user_statistics", + column: "current_rank"); + + migrationBuilder.CreateIndex( + name: "IX_UserStatistics_TotalScore", + table: "user_statistics", + column: "total_score"); + + migrationBuilder.CreateIndex( + name: "IX_UserStatistics_UserId", + table: "user_statistics", + column: "user_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UserStatistics_WinRate", + table: "user_statistics", + column: "win_rate"); + + migrationBuilder.CreateIndex( + name: "IX_Users_AccessToken", + table: "users", + column: "access_token", + filter: "[access_token] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_Users_LastActivity", + table: "users", + column: "last_activity_at"); + + migrationBuilder.CreateIndex( + name: "IX_Users_RefreshToken", + table: "users", + column: "refresh_token", + filter: "[refresh_token] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_Users_Status", + table: "users", + column: "status"); + + migrationBuilder.CreateIndex( + name: "IX_Users_TokenStatus_AccessTokenExpires", + table: "users", + columns: new[] { "token_status", "access_token_expires_at" }); + + migrationBuilder.CreateIndex( + name: "IX_Users_TokenStatus_RefreshTokenExpires", + table: "users", + columns: new[] { "token_status", "refresh_token_expires_at" }); + + migrationBuilder.CreateIndex( + name: "IX_Users_Username", + table: "users", + column: "username", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "game_actions"); + + migrationBuilder.DropTable( + name: "game_players"); + + migrationBuilder.DropTable( + name: "notifications"); + + migrationBuilder.DropTable( + name: "ranking_histories"); + + migrationBuilder.DropTable( + name: "rankings"); + + migrationBuilder.DropTable( + name: "room_messages"); + + migrationBuilder.DropTable( + name: "room_players"); + + migrationBuilder.DropTable( + name: "user_statistics"); + + migrationBuilder.DropTable( + name: "games"); + + migrationBuilder.DropTable( + name: "rooms"); + + migrationBuilder.DropTable( + name: "users"); + } + } +} diff --git a/backend/src/CollabApp.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/src/CollabApp.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000000000000000000000000000000000000..4a8c2d24e70d6814215d9a75b23dbd2c53f5d05a --- /dev/null +++ b/backend/src/CollabApp.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,1055 @@ +// +using System; +using CollabApp.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CollabApp.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CollabApp.Domain.Entities.Auth.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessToken") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("access_token"); + + b.Property("AccessTokenExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("access_token_expires_at"); + + b.Property("AvatarUrl") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("avatar_url"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeviceInfo") + .HasColumnType("json") + .HasColumnName("device_info"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("LastActivityAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_activity_at"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_login_at"); + + b.Property("Nickname") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("nickname"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("password_hash"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("password_salt"); + + b.Property("PrivacySettings") + .HasColumnType("json") + .HasColumnName("privacy_settings"); + + b.Property("RefreshToken") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasColumnName("refresh_token"); + + b.Property("RefreshTokenExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("refresh_token_expires_at"); + + b.Property("RememberMe") + .HasColumnType("boolean") + .HasColumnName("remember_me"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("TokenRevokedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("token_revoked_at"); + + b.Property("TokenRevokedReason") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("token_revoked_reason"); + + b.Property("TokenStatus") + .HasColumnType("integer") + .HasColumnName("token_status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("username"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .HasDatabaseName("IX_Users_AccessToken") + .HasFilter("[access_token] IS NOT NULL"); + + b.HasIndex("LastActivityAt") + .HasDatabaseName("IX_Users_LastActivity"); + + b.HasIndex("RefreshToken") + .HasDatabaseName("IX_Users_RefreshToken") + .HasFilter("[refresh_token] IS NOT NULL"); + + b.HasIndex("Status") + .HasDatabaseName("IX_Users_Status"); + + b.HasIndex("Username") + .IsUnique() + .HasDatabaseName("IX_Users_Username"); + + b.HasIndex("TokenStatus", "AccessTokenExpiresAt") + .HasDatabaseName("IX_Users_TokenStatus_AccessTokenExpires"); + + b.HasIndex("TokenStatus", "RefreshTokenExpiresAt") + .HasDatabaseName("IX_Users_TokenStatus_RefreshTokenExpires"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Auth.UserStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CurrentRank") + .HasColumnType("integer") + .HasColumnName("current_rank"); + + b.Property("HighestRank") + .HasColumnType("integer") + .HasColumnName("highest_rank"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Losses") + .HasColumnType("integer") + .HasColumnName("losses"); + + b.Property("MaxArea") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("max_area"); + + b.Property("TotalGames") + .HasColumnType("integer") + .HasColumnName("total_games"); + + b.Property("TotalPlayTime") + .HasColumnType("integer") + .HasColumnName("total_play_time"); + + b.Property("TotalScore") + .HasColumnType("integer") + .HasColumnName("total_score"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("WinRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)") + .HasColumnName("win_rate"); + + b.Property("Wins") + .HasColumnType("integer") + .HasColumnName("wins"); + + b.HasKey("Id"); + + b.HasIndex("CurrentRank") + .HasDatabaseName("IX_UserStatistics_CurrentRank"); + + b.HasIndex("TotalScore") + .HasDatabaseName("IX_UserStatistics_TotalScore"); + + b.HasIndex("UserId") + .IsUnique() + .HasDatabaseName("IX_UserStatistics_UserId"); + + b.HasIndex("WinRate") + .HasDatabaseName("IX_UserStatistics_WinRate"); + + b.ToTable("user_statistics"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Game.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Duration") + .HasColumnType("integer") + .HasColumnName("duration"); + + b.Property("EnableDynamicBalance") + .HasColumnType("boolean") + .HasColumnName("enable_dynamic_balance"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished_at"); + + b.Property("GameData") + .HasColumnType("json") + .HasColumnName("game_data"); + + b.Property("GameMode") + .IsRequired() + .HasColumnType("text") + .HasColumnName("game_mode"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("MapHeight") + .HasColumnType("integer") + .HasColumnName("map_height"); + + b.Property("MapShape") + .IsRequired() + .HasColumnType("text") + .HasColumnName("map_shape"); + + b.Property("MapWidth") + .HasColumnType("integer") + .HasColumnName("map_width"); + + b.Property("MaxPowerUps") + .HasColumnType("integer") + .HasColumnName("max_powerups"); + + b.Property("PowerUpSpawnInterval") + .HasColumnType("integer") + .HasColumnName("powerup_spawn_interval"); + + b.Property("RoomId") + .HasColumnType("uuid") + .HasColumnName("room_id"); + + b.Property("SpecialEventChance") + .HasColumnType("integer") + .HasColumnName("special_event_chance"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("WinnerId") + .HasColumnType("uuid") + .HasColumnName("winner_id"); + + b.HasKey("Id"); + + b.HasIndex("FinishedAt") + .HasDatabaseName("IX_Games_FinishedAt"); + + b.HasIndex("RoomId") + .HasDatabaseName("IX_Games_RoomId"); + + b.HasIndex("StartedAt") + .HasDatabaseName("IX_Games_StartedAt"); + + b.HasIndex("Status") + .HasDatabaseName("IX_Games_Status"); + + b.HasIndex("WinnerId") + .HasDatabaseName("IX_Games_WinnerId"); + + b.ToTable("games"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Game.GameAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionData") + .IsRequired() + .HasColumnType("json") + .HasColumnName("action_data"); + + b.Property("ActionType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("action_type"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("GameId") + .HasColumnType("uuid") + .HasColumnName("game_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Timestamp") + .HasColumnType("bigint") + .HasColumnName("timestamp"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("ActionType") + .HasDatabaseName("IX_GameActions_ActionType"); + + b.HasIndex("GameId") + .HasDatabaseName("IX_GameActions_GameId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_GameActions_Timestamp"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_GameActions_UserId"); + + b.HasIndex("GameId", "Timestamp") + .HasDatabaseName("IX_GameActions_GameId_Timestamp"); + + b.ToTable("game_actions"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Game.GamePlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionsCount") + .HasColumnType("integer") + .HasColumnName("actions_count"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CurrentPowerUp") + .HasColumnType("text") + .HasColumnName("current_powerup"); + + b.Property("DeathCount") + .HasColumnType("integer") + .HasColumnName("death_count"); + + b.Property("FinalArea") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("final_area"); + + b.Property("FinalRank") + .HasColumnType("integer") + .HasColumnName("final_rank"); + + b.Property("GameId") + .HasColumnType("uuid") + .HasColumnName("game_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("KillCount") + .HasColumnType("integer") + .HasColumnName("kill_count"); + + b.Property("MaxTerritoryArea") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasColumnName("max_territory_area"); + + b.Property("PlayTime") + .HasColumnType("integer") + .HasColumnName("play_time"); + + b.Property("PlayerColor") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character varying(7)") + .HasColumnName("player_color"); + + b.Property("PositionX") + .HasColumnType("real") + .HasColumnName("position_x"); + + b.Property("PositionY") + .HasColumnType("real") + .HasColumnName("position_y"); + + b.Property("PowerUpUsageCount") + .HasColumnType("integer") + .HasColumnName("powerup_usage_count"); + + b.Property("RespawnTimestamp") + .HasColumnType("bigint") + .HasColumnName("respawn_timestamp"); + + b.Property("ScoreChange") + .HasColumnType("integer") + .HasColumnName("score_change"); + + b.Property("SpawnX") + .HasColumnType("real") + .HasColumnName("spawn_x"); + + b.Property("SpawnY") + .HasColumnType("real") + .HasColumnName("spawn_y"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("TeamId") + .HasColumnType("integer") + .HasColumnName("team_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FinalArea") + .HasDatabaseName("IX_GamePlayers_FinalArea"); + + b.HasIndex("FinalRank") + .HasDatabaseName("IX_GamePlayers_FinalRank"); + + b.HasIndex("ScoreChange") + .HasDatabaseName("IX_GamePlayers_ScoreChange"); + + b.HasIndex("UserId"); + + b.HasIndex("GameId", "UserId") + .IsUnique() + .HasDatabaseName("IX_GamePlayers_GameId_UserId"); + + b.ToTable("game_players"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Data") + .HasColumnType("json") + .HasColumnName("data"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("IsRead") + .HasColumnType("boolean") + .HasColumnName("is_read"); + + b.Property("NotificationType") + .HasColumnType("integer") + .HasColumnName("notification_type"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("read_at"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_Notifications_CreatedAt"); + + b.HasIndex("NotificationType") + .HasDatabaseName("IX_Notifications_Type"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_Notifications_UserId"); + + b.HasIndex("UserId", "CreatedAt") + .HasDatabaseName("IX_Notifications_UserId_CreatedAt"); + + b.HasIndex("UserId", "IsRead") + .HasDatabaseName("IX_Notifications_UserId_IsRead"); + + b.ToTable("notifications"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Ranking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CurrentRank") + .HasColumnType("integer") + .HasColumnName("current_rank"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("period_end"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone") + .HasColumnName("period_start"); + + b.Property("RankingType") + .HasColumnType("integer") + .HasColumnName("ranking_type"); + + b.Property("Score") + .HasColumnType("integer") + .HasColumnName("score"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UpdatedAt") + .HasDatabaseName("IX_Rankings_UpdatedAt"); + + b.HasIndex("RankingType", "CurrentRank") + .HasDatabaseName("IX_Rankings_Type_Rank"); + + b.HasIndex("RankingType", "Score") + .HasDatabaseName("IX_Rankings_Type_Score"); + + b.HasIndex("UserId", "RankingType", "PeriodStart", "PeriodEnd") + .IsUnique() + .HasDatabaseName("IX_Rankings_UserId_Type_Period"); + + b.ToTable("rankings"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.RankingHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Rank") + .HasColumnType("integer") + .HasColumnName("rank"); + + b.Property("RankingType") + .HasColumnType("integer") + .HasColumnName("ranking_type"); + + b.Property("RecordedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("recorded_at"); + + b.Property("Score") + .HasColumnType("integer") + .HasColumnName("score"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_RankingHistories_UserId"); + + b.HasIndex("RankingType", "RecordedAt") + .HasDatabaseName("IX_RankingHistories_Type_RecordedAt"); + + b.HasIndex("UserId", "RankingType", "RecordedAt") + .HasDatabaseName("IX_RankingHistories_UserId_Type_RecordedAt"); + + b.ToTable("ranking_histories"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Room.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CurrentPlayers") + .HasColumnType("integer") + .HasColumnName("current_players"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("IsPrivate") + .HasColumnType("boolean") + .HasColumnName("is_private"); + + b.Property("MaxPlayers") + .HasColumnType("integer") + .HasColumnName("max_players"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("uuid") + .HasColumnName("owner_id"); + + b.Property("Password") + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("password"); + + b.Property("Settings") + .HasColumnType("json") + .HasColumnName("settings"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_Rooms_CreatedAt"); + + b.HasIndex("OwnerId") + .HasDatabaseName("IX_Rooms_OwnerId"); + + b.HasIndex("Status") + .HasDatabaseName("IX_Rooms_Status"); + + b.HasIndex("Status", "IsPrivate") + .HasDatabaseName("IX_Rooms_Status_IsPrivate"); + + b.ToTable("rooms"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Room.RoomMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("MessageType") + .HasColumnType("integer") + .HasColumnName("message_type"); + + b.Property("RoomId") + .HasColumnType("uuid") + .HasColumnName("room_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_RoomMessages_CreatedAt"); + + b.HasIndex("RoomId") + .HasDatabaseName("IX_RoomMessages_RoomId"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_RoomMessages_UserId"); + + b.HasIndex("RoomId", "CreatedAt") + .HasDatabaseName("IX_RoomMessages_RoomId_CreatedAt"); + + b.ToTable("room_messages"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Room.RoomPlayer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("IsReady") + .HasColumnType("boolean") + .HasColumnName("is_ready"); + + b.Property("JoinOrder") + .HasColumnType("integer") + .HasColumnName("join_order"); + + b.Property("PlayerColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)") + .HasColumnName("player_color"); + + b.Property("RoomId") + .HasColumnType("uuid") + .HasColumnName("room_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("JoinOrder") + .HasDatabaseName("IX_RoomPlayers_JoinOrder"); + + b.HasIndex("UserId"); + + b.HasIndex("RoomId", "UserId") + .IsUnique() + .HasDatabaseName("IX_RoomPlayers_RoomId_UserId") + .HasFilter("NOT is_deleted"); + + b.ToTable("room_players"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Auth.UserStatistics", b => + { + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithOne("Statistics") + .HasForeignKey("CollabApp.Domain.Entities.Auth.UserStatistics", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Game.Game", b => + { + b.HasOne("CollabApp.Domain.Entities.Room.Room", "Room") + .WithMany("Games") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CollabApp.Domain.Entities.Auth.User", "Winner") + .WithMany() + .HasForeignKey("WinnerId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Room"); + + b.Navigation("Winner"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Game.GameAction", b => + { + b.HasOne("CollabApp.Domain.Entities.Game.Game", "Game") + .WithMany("Actions") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Game.GamePlayer", b => + { + b.HasOne("CollabApp.Domain.Entities.Game.Game", "Game") + .WithMany("Players") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithMany("GamePlayers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Notification", b => + { + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Ranking", b => + { + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.RankingHistory", b => + { + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Room.Room", b => + { + b.HasOne("CollabApp.Domain.Entities.Auth.User", "Owner") + .WithMany("OwnedRooms") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Room.RoomMessage", b => + { + b.HasOne("CollabApp.Domain.Entities.Room.Room", "Room") + .WithMany("Messages") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Room.RoomPlayer", b => + { + b.HasOne("CollabApp.Domain.Entities.Room.Room", "Room") + .WithMany("Players") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CollabApp.Domain.Entities.Auth.User", "User") + .WithMany("RoomPlayers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Auth.User", b => + { + b.Navigation("GamePlayers"); + + b.Navigation("Notifications"); + + b.Navigation("OwnedRooms"); + + b.Navigation("RoomPlayers"); + + b.Navigation("Statistics"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Game.Game", b => + { + b.Navigation("Actions"); + + b.Navigation("Players"); + }); + + modelBuilder.Entity("CollabApp.Domain.Entities.Room.Room", b => + { + b.Navigation("Games"); + + b.Navigation("Messages"); + + b.Navigation("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 0ba1bd3fba4fcf51e96e122ca336de9299d1bee6..be0130db60074838a69f3d593a9e1fee0962e5d5 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -14,7 +14,7 @@ const api = axios.create({ api.interceptors.request.use( (config) => { // 添加token到请求头 - const token = localStorage.getItem('token') + const token = localStorage.getItem('token') || sessionStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } diff --git a/frontend/src/stores/README.md b/frontend/src/stores/README.md deleted file mode 100644 index 1265942cc3a908bbc38db96b59f7be5d0806cb4c..0000000000000000000000000000000000000000 --- a/frontend/src/stores/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# stores - -状态管理目录,建议使用Pinia或Vuex,存放全局和模块化的状态管理文件。 diff --git a/frontend/src/stores/user.js b/frontend/src/stores/user.js index 6bdd97fc0160596972fb31e09875ec42f34a4810..3a9210445fb5433ceda7642ff83999ce7536f099 100644 --- a/frontend/src/stores/user.js +++ b/frontend/src/stores/user.js @@ -90,17 +90,29 @@ export const useUserStore = defineStore('user', () => { const fetchUserInfo = async () => { try { - const [profileRes, statsRes] = await Promise.all([ - userAPI.getUserInfo(), - userAPI.getUserStats() - ]) - - setUserInfo({ - ...profileRes.data, - ...statsRes.data - }) - - return { ...profileRes.data, ...statsRes.data } + const response = await authAPI.getCurrentUser() + + if (response.code === 1000 && response.data) { + setUserInfo({ + id: response.data.id, + username: response.data.username, + nickname: response.data.nickname, + avatarUrl: response.data.avatarUrl, + status: response.data.status, + // 设置默认值 + email: '', + level: 1, + experience: 0, + totalGames: 0, + winRate: 0, + ranking: null, + bestStreak: 0, + joinDate: response.data.createdAt + }) + return response.data + } else { + throw new Error(response.message || '获取用户信息失败') + } } catch (error) { console.error('获取用户信息失败:', error) throw error diff --git a/frontend/src/views/lobby/RoomPage.vue b/frontend/src/views/lobby/RoomPage.vue index e8455787985ed67ba04abf68427b0915576f812e..ae411bf311736b94a3629161c2b329fc25594262 100644 --- a/frontend/src/views/lobby/RoomPage.vue +++ b/frontend/src/views/lobby/RoomPage.vue @@ -6,15 +6,18 @@ @@ -30,25 +33,6 @@

👥 玩家列表 ({{ roomInfo.players.length }}/{{ roomInfo.maxPlayers }})

-
- - -
-
+ class="player-item empty-slot">
等待玩家加入...
@@ -94,15 +79,15 @@
- -
- 请点击"准备"按钮 + ✅ 已准备,等待房主开始游戏 + 请点击"准备"按钮
@@ -115,7 +100,7 @@
-
+
- 头像 + 头像
{{ message.userName }} {{ formatTime(message.timestamp) }}
-
{{ - formatMessageText(message.content) }}
+
{{ message.content }}
+ +
+
+

正在加载聊天记录...

+
-
@@ -161,45 +150,6 @@
- - -
@@ -236,8 +188,11 @@ @@ -819,6 +640,12 @@ onMounted(async () => { text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } +.room-id-wrapper { + display: flex; + align-items: center; + gap: 0.5rem; +} + .room-id { color: rgba(255, 255, 255, 0.8); font-size: 1rem; @@ -829,6 +656,22 @@ onMounted(async () => { backdrop-filter: blur(10px); } +.copy-btn { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; + padding: 0.25rem 0.5rem; + color: #fff; + cursor: pointer; + transition: all 0.3s ease; + font-size: 0.9rem; +} + +.copy-btn:hover { + background: rgba(255, 255, 255, 0.2); + transform: translateY(-1px); +} + .nav-actions { display: flex; gap: 1rem; @@ -933,45 +776,6 @@ onMounted(async () => { box-shadow: 0 2px 8px rgba(79, 172, 254, 0.4); } -.ai-controls-inline { - display: flex; - gap: 0.75rem; - align-items: center; -} - -.ai-controls-inline .btn { - min-width: 44px; - height: 44px; - display: flex; - align-items: center; - justify-content: center; - font-size: 1rem; - padding: 0; - border-radius: 12px; - transition: all 0.3s ease; - position: relative; - overflow: hidden; -} - -.ai-controls-inline .btn::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); - transition: left 0.4s ease; -} - -.ai-controls-inline .btn:hover::before { - left: 100%; -} - -.ai-controls-inline .btn:hover { - transform: translateY(-2px) scale(1.05); -} - .players-list { display: grid; grid-template-columns: 1fr; @@ -1032,34 +836,16 @@ onMounted(async () => { box-shadow: 0 8px 32px rgba(255, 215, 0, 0.15), inset 0 1px 0 rgba(255, 215, 0, 0.2); - position: relative; -} - -.player-item.is-owner::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(45deg, transparent 30%, rgba(255, 215, 0, 0.1) 50%, transparent 70%); - pointer-events: none; } .player-item.is-ready { border: 2px solid #4facfe; background: linear-gradient(135deg, rgba(79, 172, 254, 0.2) 0%, rgba(79, 172, 254, 0.08) 100%); - box-shadow: - 0 8px 32px rgba(79, 172, 254, 0.15), - inset 0 1px 0 rgba(79, 172, 254, 0.2); } .player-item.is-current-user { border: 2px solid #667eea; background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(102, 126, 234, 0.08) 100%); - box-shadow: - 0 8px 32px rgba(102, 126, 234, 0.15), - inset 0 1px 0 rgba(102, 126, 234, 0.2); } .player-item.empty-slot { @@ -1070,19 +856,9 @@ onMounted(async () => { animation: pulse-empty 3s ease-in-out infinite; } -.player-item.empty-slot:hover { - transform: none; - background: rgba(255, 255, 255, 0.04); - border-color: rgba(255, 255, 255, 0.3); -} - @keyframes pulse-empty { - 0%, 100% { - opacity: 0.7; - } - 50% { - opacity: 0.5; - } + 0%, 100% { opacity: 0.7; } + 50% { opacity: 0.5; } } .player-avatar { @@ -1099,86 +875,15 @@ onMounted(async () => { box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1), inset 0 1px 2px rgba(255, 255, 255, 0.1); - position: relative; transition: all 0.3s ease; overflow: hidden; } .player-avatar .avatar-img { width: 100%; - height: 100%; - object-fit: cover; - border-radius: 50%; -} - -.player-avatar::after { - content: ''; - position: absolute; - top: -2px; - left: -2px; - right: -2px; - bottom: -2px; - border-radius: 50%; - background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%); - opacity: 0; - transition: opacity 0.3s ease; - pointer-events: none; -} - -.player-item:hover .player-avatar::after { - opacity: 1; -} - -.player-info { - flex: 1; - min-width: 0; -} - -.player-name { - color: #fff; - font-weight: 700; - font-size: 1.1rem; - margin-bottom: 0.5rem; - display: flex; - align-items: center; - gap: 0.5rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.owner-badge { - font-size: 1rem; - filter: drop-shadow(0 2px 4px rgba(255, 215, 0, 0.5)); - animation: crown-glow 2s ease-in-out infinite alternate; -} - -@keyframes crown-glow { - from { - filter: drop-shadow(0 2px 4px rgba(255, 215, 0, 0.5)); - } - to { - filter: drop-shadow(0 2px 8px rgba(255, 215, 0, 0.8)); - } -} - -.player-status { - font-size: 0.9rem; - font-weight: 600; -} - -.status-ready { - color: #4facfe; - display: flex; - align-items: center; - gap: 0.25rem; -} - -.status-waiting { - color: rgba(255, 255, 255, 0.6); - display: flex; - align-items: center; - gap: 0.25rem; + height: 100%; + object-fit: cover; + border-radius: 50%; } .player-avatar.empty { @@ -1189,12 +894,6 @@ onMounted(async () => { font-size: 1.2rem; } -.player-item.empty-slot .player-name { - color: rgba(255, 255, 255, 0.5); - font-style: italic; - font-weight: 500; -} - .player-info { flex: 1; min-width: 0; @@ -1215,7 +914,13 @@ onMounted(async () => { .owner-badge { font-size: 1rem; - filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)); + filter: drop-shadow(0 2px 4px rgba(255, 215, 0, 0.5)); + animation: crown-glow 2s ease-in-out infinite alternate; +} + +@keyframes crown-glow { + from { filter: drop-shadow(0 2px 4px rgba(255, 215, 0, 0.5)); } + to { filter: drop-shadow(0 2px 8px rgba(255, 215, 0, 0.8)); } } .player-status { @@ -1267,10 +972,6 @@ onMounted(async () => { box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4); } -.btn:active { - transform: translateY(0); -} - .btn:disabled { opacity: 0.5; cursor: not-allowed; @@ -1380,6 +1081,29 @@ onMounted(async () => { text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } +.loading-chat { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: rgba(255, 255, 255, 0.7); + gap: 1rem; +} + +.loading-spinner { + width: 40px; + height: 40px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #4facfe; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + .chat-messages { flex: 1; overflow-y: auto; @@ -1497,21 +1221,6 @@ onMounted(async () => { backdrop-filter: blur(15px); font-size: 0.95rem; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); - position: relative; - user-select: text; /* 允许聊天消息文本选择 */ - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; -} - -.message-text::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 1px; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); } .is-own-message .message-text { @@ -1520,6 +1229,13 @@ onMounted(async () => { box-shadow: 0 8px 24px rgba(102, 126, 234, 0.2); } +.selectable-text { + user-select: text !important; + -webkit-user-select: text !important; + -moz-user-select: text !important; + -ms-user-select: text !important; +} + .chat-input { padding: 1.5rem; border-top: 1px solid rgba(255, 255, 255, 0.1); @@ -1541,7 +1257,7 @@ onMounted(async () => { font-size: 0.95rem; transition: all 0.3s ease; backdrop-filter: blur(10px); - user-select: text; /* 允许输入框文本选择 */ + user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text; @@ -1583,13 +1299,18 @@ onMounted(async () => { font-weight: 500; } -.quick-btn:hover { +.quick-btn:hover:not(:disabled) { background: rgba(255, 255, 255, 0.2); color: #fff; transform: translateY(-1px); border-color: rgba(255, 255, 255, 0.3); } +.quick-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + /* 弹窗样式 */ .modal-overlay { position: fixed; @@ -1605,97 +1326,6 @@ onMounted(async () => { backdrop-filter: blur(3px); } -.modal-content { - background: linear-gradient(135deg, rgba(102, 126, 234, 0.9) 0%, rgba(118, 75, 162, 0.9) 100%); - backdrop-filter: blur(20px); - border-radius: 20px; - max-width: 500px; - width: 90%; - max-height: 80vh; - overflow: hidden; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); - border: 1px solid rgba(255, 255, 255, 0.2); -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1.5rem; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); -} - -.modal-header h3 { - color: #fff; - margin: 0; - font-size: 1.4rem; - font-weight: 700; -} - -.modal-close { - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - color: rgba(255, 255, 255, 0.8); - font-size: 1.5rem; - cursor: pointer; - padding: 0.5rem; - border-radius: 50%; - transition: all 0.3s ease; - width: 44px; - height: 44px; - display: flex; - align-items: center; - justify-content: center; - backdrop-filter: blur(10px); -} - -.modal-close:hover { - background: rgba(255, 255, 255, 0.2); - color: #fff; - transform: rotate(90deg); -} - -.modal-body { - padding: 1.5rem; -} - -.modal-footer { - display: flex; - gap: 1rem; - padding: 1.5rem; - border-top: 1px solid rgba(255, 255, 255, 0.1); - justify-content: flex-end; -} - -.settings-form { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.form-group { - display: flex; - flex-direction: column; -} - -.form-group label { - color: #fff; - margin-bottom: 0.75rem; - font-size: 1rem; - font-weight: 600; -} - -select.form-input { - background: rgba(255, 255, 255, 0.1); - color: #fff; -} - -select.form-input option { - background: #2a4a72; - color: #fff; -} - -/* 离开房间确认弹窗样式 */ .leave-modal { background: linear-gradient(135deg, rgba(102, 126, 234, 0.95) 0%, rgba(118, 75, 162, 0.95) 100%); backdrop-filter: blur(25px); @@ -1708,7 +1338,6 @@ select.form-input option { 0 8px 32px rgba(102, 126, 234, 0.2); border: 1px solid rgba(255, 255, 255, 0.2); animation: modalSlideIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); - transform-origin: center; } @keyframes modalSlideIn { @@ -1728,7 +1357,6 @@ select.form-input option { gap: 0.75rem; padding: 1.25rem 1.5rem 0.75rem 1.5rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1); - background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%); } .leave-icon-small { @@ -1742,7 +1370,6 @@ select.form-input option { font-size: 1.1rem; font-weight: 700; flex: 1; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } .leave-close { @@ -1759,7 +1386,6 @@ select.form-input option { display: flex; align-items: center; justify-content: center; - backdrop-filter: blur(10px); } .leave-close:hover { @@ -1776,18 +1402,12 @@ select.form-input option { .leave-warning { font-size: 2.5rem; margin-bottom: 1rem; - animation: warning-pulse-small 2s ease-in-out infinite; + animation: warning-pulse 2s ease-in-out infinite; } -@keyframes warning-pulse-small { - 0%, 100% { - transform: scale(1); - opacity: 1; - } - 50% { - transform: scale(1.05); - opacity: 0.9; - } +@keyframes warning-pulse { + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.05); opacity: 0.9; } } .leave-message-compact { @@ -1795,7 +1415,6 @@ select.form-input option { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } .leave-note-compact { @@ -1825,15 +1444,19 @@ select.form-input option { border: 1px solid rgba(255, 255, 255, 0.2); } +.btn-compact:disabled { + opacity: 0.5; + cursor: not-allowed; +} + .btn-compact-secondary { background: rgba(255, 255, 255, 0.15); color: #fff; } -.btn-compact-secondary:hover { +.btn-compact-secondary:hover:not(:disabled) { background: rgba(255, 255, 255, 0.25); transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(255, 255, 255, 0.2); } .btn-compact-danger { @@ -1842,10 +1465,9 @@ select.form-input option { border-color: rgba(255, 107, 107, 0.5); } -.btn-compact-danger:hover { +.btn-compact-danger:hover:not(:disabled) { background: linear-gradient(135deg, #ff5252 0%, #e53e3e 100%); transform: translateY(-1px); - box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4); } /* 通知组件样式 */ @@ -1873,22 +1495,18 @@ select.form-input option { .notification-success { background: linear-gradient(135deg, rgba(17, 153, 142, 0.9) 0%, rgba(56, 239, 125, 0.9) 100%); - border-color: rgba(56, 239, 125, 0.5); } .notification-warning { background: linear-gradient(135deg, rgba(255, 193, 7, 0.9) 0%, rgba(255, 152, 0, 0.9) 100%); - border-color: rgba(255, 193, 7, 0.5); } .notification-error { background: linear-gradient(135deg, rgba(255, 107, 107, 0.9) 0%, rgba(238, 90, 82, 0.9) 100%); - border-color: rgba(255, 107, 107, 0.5); } .notification-info { background: linear-gradient(135deg, rgba(79, 172, 254, 0.9) 0%, rgba(0, 242, 254, 0.9) 100%); - border-color: rgba(79, 172, 254, 0.5); } .notification-icon { @@ -1925,114 +1543,11 @@ select.form-input option { } } -/* 移动端通知适配 */ -@media (max-width: 768px) { - .notification-overlay { - top: 10px; - right: 10px; - left: 10px; - } - - .notification { - min-width: auto; - width: 100%; - } - - .notification-message { - font-size: 0.9rem; - } - - /* 移动端离开房间弹窗适配 */ - .leave-modal { - width: 340px; - max-width: 85vw; - } - - .leave-modal-header { - padding: 1rem 1.25rem 0.5rem 1.25rem; - } - - .leave-modal-header h3 { - font-size: 1rem; - } - - .leave-modal-body { - padding: 1.25rem; - } - - .leave-warning { - font-size: 2rem; - margin-bottom: 0.75rem; - } - - .leave-message-compact { - font-size: 1rem; - } - - .leave-note-compact { - font-size: 0.85rem; - } - - .leave-modal-footer { - padding: 0 1.25rem 1.25rem 1.25rem; - } - - .btn-compact { - padding: 0.65rem 1.25rem; - font-size: 0.85rem; - min-width: 70px; - } -} - /* 响应式设计 */ @media (max-width: 1200px) { - .room-layout { - grid-template-columns: 380px 1fr; - gap: 2rem; - } - - .card { - border-radius: 20px; - } - - .players-section, - .game-controls { - padding: 1.75rem; - } -} - -@media (max-width: 1024px) { .room-layout { grid-template-columns: 1fr; gap: 2rem; - max-width: 100%; - } - - .sidebar { - position: relative; - top: 0; - order: 2; - } - - .chat-section { - order: 1; - } - - .chat-container { - height: 550px; - } - - .container { - padding: 0 1.5rem; - } - - .card { - border-radius: 18px; - } - - .players-section, - .game-controls { - padding: 1.5rem; } } @@ -2043,145 +1558,36 @@ select.form-input option { .nav-content { flex-direction: column; - text-align: center; + gap: 1rem; } - .room-info-nav h1 { - font-size: 1.5rem; - } - - .players-header { + .room-id-wrapper { flex-direction: column; align-items: flex-start; - gap: 1.5rem; - } - - .ai-controls-inline { - order: -1; - align-self: flex-end; - } - - .player-item { - flex-direction: column; - text-align: center; - gap: 1rem; - padding: 1rem; - } - - .player-actions { - width: 100%; - justify-content: center; - } - - .player-actions .btn { - flex: 1; - min-width: 120px; - } - - .user-message { - max-width: 95%; + gap: 0.5rem; } .chat-container { - height: 400px; + height: 500px; } - .modal-content { - width: 95%; - margin: 1rem; - } -} - -@media (max-width: 480px) { .players-section, - .game-controls, - .chat-header, - .chat-input { - padding: 1rem; + .game-controls { + padding: 1.5rem; } - .room-info-nav h1 { - font-size: 1.3rem; + .card:hover { + transform: none; } - .btn-large { - font-size: 1.1rem; - padding: 1rem; + .notification { + min-width: auto; + width: 100%; } - .player-avatar { - width: 45px; - height: 45px; - font-size: 1.3rem; - } -} - -/* 滚动条样式优化 */ -.players-list::-webkit-scrollbar { - width: 6px; -} - -.players-list::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.1); - border-radius: 3px; -} - -.players-list::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 3px; -} - -.players-list::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -/* 动画效果 */ -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.card { - animation: fadeInUp 0.6s ease-out; -} - -.player-item { - animation: fadeInUp 0.4s ease-out; -} - -.message { - animation: fadeInUp 0.3s ease-out; -} - -/* 加载状态 */ -.btn:disabled { - position: relative; - overflow: hidden; -} - -.btn:disabled::after { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); - animation: loading 1.5s infinite; -} - -@keyframes loading { - 0% { - left: -100%; - } - 100% { - left: 100%; + .notification-overlay { + left: 10px; + right: 10px; } } diff --git a/frontend/src/views/lobby/RoomPageNew.vue b/frontend/src/views/lobby/RoomPageNew.vue new file mode 100644 index 0000000000000000000000000000000000000000..642cd1719fbed57f09efc56909db98288bfe8c55 --- /dev/null +++ b/frontend/src/views/lobby/RoomPageNew.vue @@ -0,0 +1,1589 @@ + + + + +