From d9bfb014d8c0fd49c08c89857388bf6649998340 Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 7 Jul 2026 20:05:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A7=E4=B8=8B=E9=BA=A6?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=E6=81=A2=E5=A4=8D=E9=98=BB=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/room/service/recovery.go | 7 +++- .../internal/room/service/recovery_test.go | 37 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/services/room-service/internal/room/service/recovery.go b/services/room-service/internal/room/service/recovery.go index f1f2dbb8..289d9724 100644 --- a/services/room-service/internal/room/service/recovery.go +++ b/services/room-service/internal/room/service/recovery.go @@ -240,7 +240,12 @@ func replay(current *state.RoomState, cmd command.Command, committedAtMS int64) case *command.MicDown: seat, exists := current.SeatByUser(typed.TargetUserID) if !exists { - return fmt.Errorf("mic_down replay target is not on seat: %d", typed.TargetUserID) + // 历史日志里存在下麦/离房/超时清理重复到达后仍被标记 replayable 的旧下麦命令;当前状态已无麦位时恢复保持 no-op。 + return nil + } + if typed.MicSessionID != "" && seat.MicSessionID != typed.MicSessionID { + // 超时下麦会携带 mic_session_id;如果用户已快速重上麦,旧命令不能释放新会话。 + return nil } index := current.SeatIndex(seat.SeatNo) current.ClearMicSession(index) diff --git a/services/room-service/internal/room/service/recovery_test.go b/services/room-service/internal/room/service/recovery_test.go index a012267f..4b4e60b8 100644 --- a/services/room-service/internal/room/service/recovery_test.go +++ b/services/room-service/internal/room/service/recovery_test.go @@ -42,6 +42,43 @@ func TestReplayMicHeartbeatSkipsStaleSession(t *testing.T) { } } +func TestReplayMicDownSkipsStaleMissingSeat(t *testing.T) { + current := state.NewRoomState("room-replay-stale-down", 7251, 8, "voice") + current.OnlineUsers[7251] = &state.RoomUserState{UserID: 7251, Role: "owner", JoinedAtMS: 1000, LastSeenAtMS: 1000} + + if err := replay(current, &command.MicDown{ + Base: command.Base{ActorID: 7251, Room: current.RoomID, SentAtMS: 2000}, + TargetUserID: 7251, + MicSessionID: "mic-old", + Reason: "publish_timeout", + }, 2000); err != nil { + t.Fatalf("stale mic down without seat must be skipped: %v", err) + } + if current.Version != 0 { + t.Fatalf("stale mic down must not mutate version, got %d", current.Version) + } +} + +func TestReplayMicDownSkipsStaleSession(t *testing.T) { + current := state.NewRoomState("room-replay-stale-down-session", 7261, 8, "voice") + current.OnlineUsers[7261] = &state.RoomUserState{UserID: 7261, Role: "owner", JoinedAtMS: 1000, LastSeenAtMS: 1000} + current.MicSeats[0].UserID = 7261 + current.MicSeats[0].PublishState = state.MicPublishPublishing + current.MicSeats[0].MicSessionID = "mic-new" + + if err := replay(current, &command.MicDown{ + Base: command.Base{ActorID: 7261, Room: current.RoomID, SentAtMS: 2000}, + TargetUserID: 7261, + MicSessionID: "mic-old", + Reason: "publish_timeout", + }, 2000); err != nil { + t.Fatalf("stale mic down for replaced session must be skipped: %v", err) + } + if current.Version != 0 || current.MicSeats[0].UserID != 7261 || current.MicSeats[0].MicSessionID != "mic-new" { + t.Fatalf("stale mic down must not clear replaced session, version=%d seat=%+v", current.Version, current.MicSeats[0]) + } +} + func TestReplayConfirmMicPublishingSkipsStaleMissingSeat(t *testing.T) { current := state.NewRoomState("room-replay-stale-confirm", 7301, 8, "voice") current.OnlineUsers[7301] = &state.RoomUserState{UserID: 7301, Role: "owner", JoinedAtMS: 1000, LastSeenAtMS: 1000}