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/roomid" "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" ) // SaveRoomBackground 把房主上传后的背景图 URL 保存到房间维度素材列表。 // 该动作只维护低频素材表,不改变 Room Cell 当前背景;真正生效必须调用 SetRoomBackground。 func (s *Service) SaveRoomBackground(ctx context.Context, req *roomv1.SaveRoomBackgroundRequest) (*roomv1.SaveRoomBackgroundResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) roomID, actorUserID, imageURL, err := normalizeSaveRoomBackground(req) if err != nil { return nil, err } if err := s.requireRoomOwnerMeta(ctx, roomID, actorUserID); err != nil { return nil, err } nowMS := s.clock.Now().UnixMilli() background, err := s.repository.SaveRoomBackground(ctx, RoomBackgroundImage{ AppCode: req.GetMeta().GetAppCode(), RoomID: roomID, ImageURL: imageURL, CreatedByUserID: actorUserID, CreatedAtMS: nowMS, UpdatedAtMS: nowMS, }) if err != nil { return nil, err } return &roomv1.SaveRoomBackgroundResponse{ Background: roomBackgroundToProto(background), ServerTimeMs: s.clock.Now().UnixMilli(), }, nil } // ListRoomBackgrounds 返回房主保存过的背景图列表和当前生效背景。 // 列表属于房间编辑面板数据,因此只允许房主读取。 func (s *Service) ListRoomBackgrounds(ctx context.Context, req *roomv1.ListRoomBackgroundsRequest) (*roomv1.ListRoomBackgroundsResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) roomID, viewerUserID, err := normalizeListRoomBackgrounds(req) if err != nil { return nil, err } if err := s.requireRoomOwnerMeta(ctx, roomID, viewerUserID); err != nil { return nil, err } backgrounds, err := s.repository.ListRoomBackgrounds(ctx, roomID) if err != nil { return nil, err } snapshot, err := s.currentSnapshot(ctx, roomID) if err != nil { return nil, err } return &roomv1.ListRoomBackgroundsResponse{ Backgrounds: roomBackgroundsToProto(backgrounds), SelectedBackgroundUrl: snapshot.GetRoomExt()[roomExtBackgroundKey], ServerTimeMs: s.clock.Now().UnixMilli(), }, nil } // SetRoomBackground 把已保存的背景图设置为当前房间背景。 // 当前背景属于房间展示状态,必须经 Room Cell 命令链路、command log 和快照恢复。 func (s *Service) SetRoomBackground(ctx context.Context, req *roomv1.SetRoomBackgroundRequest) (*roomv1.SetRoomBackgroundResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) roomID, actorUserID, backgroundID, err := normalizeSetRoomBackground(req) if err != nil { return nil, err } if err := s.requireRoomOwnerMeta(ctx, roomID, actorUserID); err != nil { return nil, err } background, exists, err := s.repository.GetRoomBackground(ctx, roomID, backgroundID) if err != nil { return nil, err } if !exists { return nil, xerr.New(xerr.NotFound, "room background not found") } cmd := command.SetRoomBackground{ Base: baseFromMeta(req.GetMeta()), BackgroundID: background.BackgroundID, BackgroundURL: background.ImageURL, } 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 current.OwnerUserID != cmd.ActorUserID() { return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "permission denied") } if current.RoomExt == nil { current.RoomExt = make(map[string]string) } if current.RoomExt[roomExtBackgroundKey] == cmd.BackgroundURL { return mutationResult{snapshot: current.ToProto()}, nil, nil } current.RoomExt[roomExtBackgroundKey] = cmd.BackgroundURL current.Version++ backgroundEvent, err := outbox.Build(current.RoomID, "RoomBackgroundChanged", current.Version, now, &roomeventsv1.RoomBackgroundChanged{ ActorUserId: cmd.ActorUserID(), BackgroundId: cmd.BackgroundID, RoomBackgroundUrl: current.RoomExt[roomExtBackgroundKey], }) if err != nil { return mutationResult{}, nil, err } return mutationResult{ snapshot: current.ToProto(), syncEvent: &tencentim.RoomEvent{ EventID: backgroundEvent.EventID, RoomID: current.RoomID, EventType: "room_background_changed", ActorUserID: cmd.ActorUserID(), RoomVersion: current.Version, Attributes: map[string]string{ "background_id": int64String(cmd.BackgroundID), "room_background_url": current.RoomExt[roomExtBackgroundKey], }, }, }, []outbox.Record{backgroundEvent}, nil }) if err != nil { return nil, err } return &roomv1.SetRoomBackgroundResponse{ Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()), Room: result.snapshot, Background: roomBackgroundToProto(background), }, nil } func normalizeSaveRoomBackground(req *roomv1.SaveRoomBackgroundRequest) (string, int64, string, error) { if req == nil || req.GetMeta() == nil { return "", 0, "", xerr.New(xerr.InvalidArgument, "request is required") } roomID := strings.TrimSpace(req.GetRoomId()) if roomID == "" { roomID = strings.TrimSpace(req.GetMeta().GetRoomId()) } if !roomid.ValidStringID(roomID) { return "", 0, "", xerr.New(xerr.InvalidArgument, "room_id is invalid") } actorUserID := req.GetMeta().GetActorUserId() if actorUserID <= 0 { return "", 0, "", xerr.New(xerr.InvalidArgument, "actor_user_id is required") } imageURL := strings.TrimSpace(req.GetImageUrl()) if imageURL == "" { return "", 0, "", xerr.New(xerr.InvalidArgument, "image_url is required") } if utf8.RuneCountInString(imageURL) > maxRoomBackgroundRunes { return "", 0, "", xerr.New(xerr.InvalidArgument, "image_url is too long") } return roomID, actorUserID, imageURL, nil } func normalizeListRoomBackgrounds(req *roomv1.ListRoomBackgroundsRequest) (string, int64, error) { if req == nil || req.GetMeta() == nil { return "", 0, xerr.New(xerr.InvalidArgument, "request is required") } roomID := strings.TrimSpace(req.GetRoomId()) if !roomid.ValidStringID(roomID) { return "", 0, xerr.New(xerr.InvalidArgument, "room_id is invalid") } viewerUserID := req.GetViewerUserId() if viewerUserID <= 0 { return "", 0, xerr.New(xerr.InvalidArgument, "viewer_user_id is required") } return roomID, viewerUserID, nil } func normalizeSetRoomBackground(req *roomv1.SetRoomBackgroundRequest) (string, int64, int64, error) { if req == nil || req.GetMeta() == nil { return "", 0, 0, xerr.New(xerr.InvalidArgument, "request is required") } roomID := strings.TrimSpace(req.GetMeta().GetRoomId()) if !roomid.ValidStringID(roomID) { return "", 0, 0, xerr.New(xerr.InvalidArgument, "room_id is invalid") } actorUserID := req.GetMeta().GetActorUserId() if actorUserID <= 0 { return "", 0, 0, xerr.New(xerr.InvalidArgument, "actor_user_id is required") } if strings.TrimSpace(req.GetMeta().GetCommandId()) == "" { return "", 0, 0, xerr.New(xerr.InvalidArgument, "command_id is required") } if req.GetBackgroundId() <= 0 { return "", 0, 0, xerr.New(xerr.InvalidArgument, "background_id is required") } return roomID, actorUserID, req.GetBackgroundId(), nil } func (s *Service) requireRoomOwnerMeta(ctx context.Context, roomID string, actorUserID int64) error { meta, exists, err := s.repository.GetRoomMeta(ctx, roomID) if err != nil { return err } if !exists { return xerr.New(xerr.NotFound, "room not found") } if meta.OwnerUserID != actorUserID { return xerr.New(xerr.PermissionDenied, "permission denied") } if meta.Status != state.RoomStatusActive { return xerr.New(xerr.Conflict, "room is not active") } return nil } func roomBackgroundToProto(background RoomBackgroundImage) *roomv1.RoomBackgroundImage { if background.BackgroundID == 0 { return nil } return &roomv1.RoomBackgroundImage{ BackgroundId: background.BackgroundID, RoomId: background.RoomID, ImageUrl: background.ImageURL, CreatedByUserId: background.CreatedByUserID, CreatedAtMs: background.CreatedAtMS, UpdatedAtMs: background.UpdatedAtMS, } } func roomBackgroundsToProto(backgrounds []RoomBackgroundImage) []*roomv1.RoomBackgroundImage { out := make([]*roomv1.RoomBackgroundImage, 0, len(backgrounds)) for _, background := range backgrounds { out = append(out, roomBackgroundToProto(background)) } return out }