From b83c3f38d45399e017c6a56264d8a47c768bf24c Mon Sep 17 00:00:00 2001 From: zhx Date: Fri, 3 Jul 2026 22:55:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8A=E9=BA=A6=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../room-service/internal/room/service/mic.go | 13 ++++-- .../internal/room/service/mic_test.go | 45 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/services/room-service/internal/room/service/mic.go b/services/room-service/internal/room/service/mic.go index 4d2c8357..a6280a09 100644 --- a/services/room-service/internal/room/service/mic.go +++ b/services/room-service/internal/room/service/mic.go @@ -38,9 +38,16 @@ func (s *Service) MicUp(ctx context.Context, req *roomv1.MicUpRequest) (*roomv1. return mutationResult{}, nil, xerr.New(xerr.NotFound, "user not in room") } - if _, exists := current.SeatByUser(cmd.ActorUserID()); exists { - // 一个用户同一时间只能占一个麦位。 - return mutationResult{}, nil, xerr.New(xerr.Conflict, "user already on seat") + if seat, exists := current.SeatByUser(cmd.ActorUserID()); exists { + // MicUp 是客户端“确保自己在麦上”的入口;重复点击或网络重试如果换了 command_id, + // 当前业务目标已经满足,直接返回现有麦位会话,避免把首次成功覆盖成用户可见失败。 + // 真正换麦必须继续走 ChangeMicSeat,不能让重复 MicUp 隐式迁移麦位。 + return mutationResult{ + snapshot: current.ToProto(), + seatNo: seat.SeatNo, + micSessionID: seat.MicSessionID, + publishDeadlineMS: seat.PublishDeadlineMS, + }, nil, nil } index := current.SeatIndex(cmd.SeatNo) diff --git a/services/room-service/internal/room/service/mic_test.go b/services/room-service/internal/room/service/mic_test.go index 238a022c..4cfb40e9 100644 --- a/services/room-service/internal/room/service/mic_test.go +++ b/services/room-service/internal/room/service/mic_test.go @@ -9,6 +9,51 @@ import ( "hyapp/services/room-service/internal/testutil/mysqltest" ) +func TestMicUpReturnsExistingSeatForDuplicateUserCommand(t *testing.T) { + ctx := context.Background() + repository := mysqltest.NewRepository(t) + now := &fixedRoomRocketClock{now: time.Date(2026, 6, 4, 8, 0, 0, 0, time.UTC)} + svc := newRocketTestService(t, repository, &rocketTestWallet{}, now) + + roomID := "room-mic-up-duplicate-user" + ownerID := int64(8501) + speakerID := int64(8502) + createRocketRoom(t, ctx, svc, roomID, ownerID, 9101) + joinRocketRoom(t, ctx, svc, roomID, speakerID) + + upResp, err := svc.MicUp(ctx, &roomv1.MicUpRequest{ + Meta: rocketMeta(roomID, speakerID, "mic-up-duplicate-user-first"), + SeatNo: 2, + }) + if err != nil { + t.Fatalf("first mic up failed: %v", err) + } + + duplicateResp, err := svc.MicUp(ctx, &roomv1.MicUpRequest{ + Meta: rocketMeta(roomID, speakerID, "mic-up-duplicate-user-second"), + SeatNo: 3, + }) + if err != nil { + t.Fatalf("duplicate mic up must return current session instead of conflict: %v", err) + } + if duplicateResp.GetResult().GetApplied() { + t.Fatalf("duplicate mic up must be a no-op mutation: %+v", duplicateResp.GetResult()) + } + if duplicateResp.GetRoom().GetVersion() != upResp.GetRoom().GetVersion() { + t.Fatalf("duplicate mic up must not advance room version: got=%d want=%d", duplicateResp.GetRoom().GetVersion(), upResp.GetRoom().GetVersion()) + } + if duplicateResp.GetSeatNo() != 2 || duplicateResp.GetMicSessionId() != upResp.GetMicSessionId() || duplicateResp.GetPublishDeadlineMs() != upResp.GetPublishDeadlineMs() { + t.Fatalf("duplicate mic up must return original mic session: duplicate=%+v first=%+v", duplicateResp, upResp) + } + if originalSeat := seatByNo(duplicateResp.GetRoom(), 2); originalSeat == nil || originalSeat.GetUserId() != speakerID || originalSeat.GetMicSessionId() != upResp.GetMicSessionId() { + t.Fatalf("original seat must remain occupied by speaker: %+v", originalSeat) + } + if requestedSeat := seatByNo(duplicateResp.GetRoom(), 3); requestedSeat == nil || requestedSeat.GetUserId() != 0 { + // 重复 MicUp 只能表达“我已经在麦上”的幂等结果;换到 seat 3 必须走 ChangeMicSeat。 + t.Fatalf("duplicate mic up must not move user to requested seat: %+v", requestedSeat) + } +} + func TestMicHeartbeatRefreshesPublishingSessionAndPresence(t *testing.T) { ctx := context.Background() repository := mysqltest.NewRepository(t)