diff --git a/services/room-service/internal/room/service/recovery.go b/services/room-service/internal/room/service/recovery.go index 289d9724..b39dbddc 100644 --- a/services/room-service/internal/room/service/recovery.go +++ b/services/room-service/internal/room/service/recovery.go @@ -327,7 +327,8 @@ func replay(current *state.RoomState, cmd command.Command, committedAtMS int64) case *command.ChangeMicSeat: from, exists := current.SeatByUser(typed.TargetUserID) if !exists { - return fmt.Errorf("change_mic_seat replay target is not on seat: %d", typed.TargetUserID) + // 换麦位属于麦上用户操作;历史过期命令若晚于下麦/离房恢复到达,当前状态已无源麦位时保持 no-op。 + return nil } fromIndex := current.SeatIndex(from.SeatNo) toIndex := current.SeatIndex(typed.SeatNo) @@ -353,7 +354,8 @@ func replay(current *state.RoomState, cmd command.Command, committedAtMS int64) case *command.SetMicMute: seat, exists := current.SeatByUser(typed.TargetUserID) if !exists { - return fmt.Errorf("set_mic_mute replay target is not on seat: %d", typed.TargetUserID) + // 静音状态只附着在当前麦位会话上;用户已不在麦上时,旧静音命令不能阻断房间恢复。 + return nil } index := current.SeatIndex(seat.SeatNo) current.MicSeats[index].MicMuted = typed.Muted diff --git a/services/room-service/internal/room/service/recovery_test.go b/services/room-service/internal/room/service/recovery_test.go index 4b4e60b8..63b6c4de 100644 --- a/services/room-service/internal/room/service/recovery_test.go +++ b/services/room-service/internal/room/service/recovery_test.go @@ -142,3 +142,35 @@ func TestReplayApplyRTCEventSkipsStaleMissingSeat(t *testing.T) { t.Fatalf("stale rtc events must not mutate version, got %d", current.Version) } } + +func TestReplayChangeMicSeatSkipsStaleMissingSeat(t *testing.T) { + current := state.NewRoomState("room-replay-stale-change-seat", 7601, 8, "voice") + current.OnlineUsers[7601] = &state.RoomUserState{UserID: 7601, Role: "owner", JoinedAtMS: 1000, LastSeenAtMS: 1000} + + if err := replay(current, &command.ChangeMicSeat{ + Base: command.Base{ActorID: 7601, Room: current.RoomID, SentAtMS: 2000}, + TargetUserID: 7601, + SeatNo: 2, + }, 2000); err != nil { + t.Fatalf("stale change seat without source seat must be skipped: %v", err) + } + if current.Version != 0 { + t.Fatalf("stale change seat must not mutate version, got %d", current.Version) + } +} + +func TestReplaySetMicMuteSkipsStaleMissingSeat(t *testing.T) { + current := state.NewRoomState("room-replay-stale-mute", 7701, 8, "voice") + current.OnlineUsers[7701] = &state.RoomUserState{UserID: 7701, Role: "owner", JoinedAtMS: 1000, LastSeenAtMS: 1000} + + if err := replay(current, &command.SetMicMute{ + Base: command.Base{ActorID: 7701, Room: current.RoomID, SentAtMS: 2000}, + TargetUserID: 7701, + Muted: true, + }, 2000); err != nil { + t.Fatalf("stale mute without seat must be skipped: %v", err) + } + if current.Version != 0 { + t.Fatalf("stale mute must not mutate version, got %d", current.Version) + } +}