package service import ( "context" "strings" "time" "unicode/utf8" roomeventsv1 "hyapp.local/api/proto/events/room/v1" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/tencentim" "hyapp/pkg/xerr" "hyapp/services/room-service/internal/room/command" "hyapp/services/room-service/internal/room/outbox" "hyapp/services/room-service/internal/room/rank" "hyapp/services/room-service/internal/room/state" ) // UpdateRoomProfile 修改房间展示资料和麦位数量,所有状态变化仍由 Room Cell 串行提交。 func (s *Service) UpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomProfileRequest) (*roomv1.UpdateRoomProfileResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) patch, err := s.normalizeUpdateRoomProfile(ctx, req) if err != nil { return nil, err } cmd := command.UpdateRoomProfile{ Base: baseFromMeta(req.GetMeta()), RoomName: patch.Name, RoomAvatar: patch.Avatar, RoomDescription: patch.Description, SeatCount: patch.SeatCount, } result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) { if err := requireActiveRoom(current); err != nil { return mutationResult{}, nil, err } if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil { return mutationResult{}, nil, err } profileChanged := applyProfilePatch(current, cmd) seatChanged, err := applySeatCountPatch(current, cmd.SeatCount) if err != nil { return mutationResult{}, nil, err } if !profileChanged && !seatChanged { // 未提交任何字段变化时仍通过 command log 保护 command_id 幂等,但不广播系统消息。 return mutationResult{snapshot: current.ToProto()}, nil, nil } current.Version++ seatCount := int32(len(current.MicSeats)) profileEvent, err := outbox.Build(current.RoomID, "RoomProfileUpdated", current.Version, now, &roomeventsv1.RoomProfileUpdated{ ActorUserId: cmd.ActorUserID(), RoomName: current.RoomExt[roomExtTitleKey], RoomAvatar: current.RoomExt[roomExtCoverURLKey], RoomDescription: current.RoomExt[roomExtDescriptionKey], SeatCount: seatCount, }) if err != nil { return mutationResult{}, nil, err } result := mutationResult{ snapshot: current.ToProto(), syncEvent: &tencentim.RoomEvent{ EventID: profileEvent.EventID, RoomID: current.RoomID, EventType: "room_profile_updated", ActorUserID: cmd.ActorUserID(), RoomVersion: current.Version, Attributes: map[string]string{ "room_name": current.RoomExt[roomExtTitleKey], "room_avatar": current.RoomExt[roomExtCoverURLKey], "room_description": current.RoomExt[roomExtDescriptionKey], "seat_count": int32String(seatCount), }, }, } if seatChanged { result.roomSeatCount = &seatCount } return result, []outbox.Record{profileEvent}, nil }) if err != nil { return nil, err } return &roomv1.UpdateRoomProfileResponse{ Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()), Room: result.snapshot, }, nil } type updateRoomProfilePatch struct { Name *string Avatar *string Description *string SeatCount *int32 } func (s *Service) normalizeUpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomProfileRequest) (updateRoomProfilePatch, error) { var patch updateRoomProfilePatch if req == nil { return patch, xerr.New(xerr.InvalidArgument, "request is required") } if req.RoomName != nil { value := strings.TrimSpace(req.GetRoomName()) if value == "" { return patch, xerr.New(xerr.InvalidArgument, "room_name is required") } if utf8.RuneCountInString(value) > maxRoomNameRunes { return patch, xerr.New(xerr.InvalidArgument, "room_name is too long") } patch.Name = &value } if req.RoomAvatar != nil { value := strings.TrimSpace(req.GetRoomAvatar()) if value == "" { // 房主清空头像时恢复系统默认资源,避免列表卡片落空。 value = defaultRoomAvatar } if utf8.RuneCountInString(value) > maxRoomAvatarRunes { return patch, xerr.New(xerr.InvalidArgument, "room_avatar is too long") } patch.Avatar = &value } if req.RoomDescription != nil { value := strings.TrimSpace(req.GetRoomDescription()) if utf8.RuneCountInString(value) > maxRoomDescriptionRunes { return patch, xerr.New(xerr.InvalidArgument, "room_description is too long") } patch.Description = &value } if req.SeatCount != nil { value, err := s.validateRoomSeatCount(ctx, req.GetSeatCount()) if err != nil { return patch, err } patch.SeatCount = &value } return patch, nil } func applyProfilePatch(current *state.RoomState, cmd command.UpdateRoomProfile) bool { if current.RoomExt == nil { current.RoomExt = make(map[string]string) } changed := false if cmd.RoomName != nil && current.RoomExt[roomExtTitleKey] != *cmd.RoomName { current.RoomExt[roomExtTitleKey] = *cmd.RoomName changed = true } if cmd.RoomAvatar != nil && current.RoomExt[roomExtCoverURLKey] != *cmd.RoomAvatar { current.RoomExt[roomExtCoverURLKey] = *cmd.RoomAvatar changed = true } if cmd.RoomDescription != nil && current.RoomExt[roomExtDescriptionKey] != *cmd.RoomDescription { current.RoomExt[roomExtDescriptionKey] = *cmd.RoomDescription changed = true } return changed } func applySeatCountPatch(current *state.RoomState, seatCount *int32) (bool, error) { if seatCount == nil { return false, nil } target := *seatCount currentCount := int32(len(current.MicSeats)) if target == currentCount { return false, nil } if target > currentCount { for seatNo := currentCount + 1; seatNo <= target; seatNo++ { current.MicSeats = append(current.MicSeats, state.MicSeat{SeatNo: seatNo}) } return true, nil } nextSeats := make([]state.MicSeat, 0, target) for _, seat := range current.MicSeats { if seat.SeatNo <= target { nextSeats = append(nextSeats, seat) continue } if seat.UserID != 0 || seat.Locked || seat.PublishState != "" || seat.MicSessionID != "" { // 缩容不能隐式踢人、清理发流会话或解锁麦位;房主需要先把高编号麦位处理干净。 return false, xerr.New(xerr.Conflict, "removed seats must be empty and unlocked") } } current.MicSeats = nextSeats return true, nil }