2026-05-28 17:57:29 +08:00

252 lines
8.6 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/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
}