260 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
if err := s.requireCustomRoomBackgroundBenefit(ctx, req.GetMeta().GetRequestId(), actorUserID); err != nil {
// 保存素材本身就会形成持久数据P1 App 必须在写库前校验当前 effective benefit。
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 err := s.requireCustomRoomBackgroundBenefit(ctx, req.GetMeta().GetRequestId(), cmd.ActorUserID()); err != nil {
// 放在 mutate 回调内可让同一 command_id 的已提交重试先命中幂等,不会因 VIP 后续到期改写旧命令结果。
return mutationResult{}, nil, err
}
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
}