2026-05-11 19:26:01 +08:00

196 lines
6.2 KiB
Go

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
}