260 lines
9.1 KiB
Go
260 lines
9.1 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
|
||
}
|
||
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
|
||
}
|