280 lines
9.9 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/appcode"
"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"
)
func (s *Service) AdminListRooms(ctx context.Context, req *roomv1.AdminListRoomsRequest) (*roomv1.AdminListRoomsResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
rooms, total, err := s.repository.AdminListRooms(ctx, AdminRoomListQuery{
AppCode: appcode.FromContext(ctx),
Page: int(req.GetPage()),
PageSize: int(req.GetPageSize()),
Keyword: strings.TrimSpace(req.GetKeyword()),
Status: strings.TrimSpace(req.GetStatus()),
RegionID: req.GetVisibleRegionId(),
OwnerUserID: req.GetOwnerUserId(),
SortBy: strings.TrimSpace(req.GetSortBy()),
SortDirection: strings.TrimSpace(req.GetSortDirection()),
})
if err != nil {
return nil, err
}
items := make([]*roomv1.AdminRoomListItem, 0, len(rooms))
for _, room := range rooms {
items = append(items, adminRoomListItemProto(room))
}
return &roomv1.AdminListRoomsResponse{Rooms: items, Total: total, ServerTimeMs: s.clock.Now().UnixMilli()}, nil
}
func (s *Service) AdminGetRoom(ctx context.Context, req *roomv1.AdminGetRoomRequest) (*roomv1.AdminGetRoomResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
room, exists, err := s.repository.AdminGetRoom(ctx, req.GetRoomId())
if err != nil {
return nil, err
}
if !exists {
return nil, xerr.New(xerr.NotFound, "room not found")
}
return &roomv1.AdminGetRoomResponse{Room: adminRoomListItemProto(room), ServerTimeMs: s.clock.Now().UnixMilli()}, nil
}
func (s *Service) AdminUpdateRoom(ctx context.Context, req *roomv1.AdminUpdateRoomRequest) (*roomv1.AdminUpdateRoomResponse, error) {
ctx = contextFromMeta(ctx, req.GetMeta())
cmd, err := s.adminUpdateCommand(req)
if err != nil {
return nil, err
}
result, err := s.applyAdminRoomMutation(ctx, cmd, cmd)
if err != nil {
return nil, err
}
return &roomv1.AdminUpdateRoomResponse{
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
Room: result.snapshot,
}, nil
}
func (s *Service) AdminDeleteRoom(ctx context.Context, req *roomv1.AdminDeleteRoomRequest) (*roomv1.AdminDeleteRoomResponse, error) {
ctx = contextFromMeta(ctx, req.GetMeta())
if req.GetMeta() == nil || strings.TrimSpace(req.GetMeta().GetRoomId()) == "" {
return nil, xerr.New(xerr.InvalidArgument, "room_id is required")
}
cmd := command.AdminDeleteRoom{
Base: baseFromMeta(req.GetMeta()),
AdminID: req.GetAdminId(),
AdminName: strings.TrimSpace(req.GetAdminName()),
}
patch := command.AdminUpdateRoom{
Base: cmd.Base,
Status: stringPtr(state.RoomStatusDeleted),
CloseReason: "admin_deleted",
AdminID: cmd.AdminID,
AdminName: cmd.AdminName,
}
result, err := s.applyAdminRoomMutation(ctx, cmd, patch)
if err != nil {
return nil, err
}
return &roomv1.AdminDeleteRoomResponse{
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
Room: result.snapshot,
}, nil
}
func (s *Service) adminUpdateCommand(req *roomv1.AdminUpdateRoomRequest) (command.AdminUpdateRoom, error) {
if req == nil || req.GetMeta() == nil || strings.TrimSpace(req.GetMeta().GetRoomId()) == "" {
return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "room_id is required")
}
cmd := command.AdminUpdateRoom{
Base: baseFromMeta(req.GetMeta()),
CloseReason: strings.TrimSpace(req.GetCloseReason()),
AdminID: req.GetAdminId(),
AdminName: strings.TrimSpace(req.GetAdminName()),
}
if req.Title != nil {
value := strings.TrimSpace(req.GetTitle())
if value == "" {
return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "room title is required")
}
if utf8.RuneCountInString(value) > maxRoomNameRunes {
return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "room title is too long")
}
cmd.Title = &value
}
if req.CoverUrl != nil {
value := strings.TrimSpace(req.GetCoverUrl())
if utf8.RuneCountInString(value) > maxRoomAvatarRunes {
return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "room cover is too long")
}
if value == "" {
value = defaultRoomAvatar
}
cmd.CoverURL = &value
}
if req.Description != nil {
value := strings.TrimSpace(req.GetDescription())
if utf8.RuneCountInString(value) > maxRoomDescriptionRunes {
return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "room description is too long")
}
cmd.Description = &value
}
if req.Mode != nil {
value := strings.TrimSpace(req.GetMode())
if value == "" || utf8.RuneCountInString(value) > 64 {
return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "room mode is invalid")
}
cmd.Mode = &value
}
if req.Status != nil {
value := strings.TrimSpace(req.GetStatus())
switch value {
case state.RoomStatusActive, state.RoomStatusClosed, state.RoomStatusDeleted:
cmd.Status = &value
default:
return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "room status is invalid")
}
if value != state.RoomStatusActive && strings.TrimSpace(cmd.CloseReason) == "" {
return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "close reason is required")
}
}
if req.VisibleRegionId != nil {
value := req.GetVisibleRegionId()
if value < 0 {
return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "visible_region_id is invalid")
}
cmd.VisibleRegionID = &value
}
return cmd, nil
}
func (s *Service) applyAdminRoomMutation(ctx context.Context, logCommand command.Command, cmd command.AdminUpdateRoom) (mutationResult, error) {
return s.mutateRoom(ctx, logCommand, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
if current == nil {
return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found")
}
changed := applyAdminRoomPatch(current, cmd)
records := make([]outbox.Record, 0, 2)
result := mutationResult{}
if cmd.Status != nil {
switch *cmd.Status {
case state.RoomStatusClosed, state.RoomStatusDeleted:
kickTargets := closeRoomKickTargets(current)
current.OnlineUsers = map[int64]*state.RoomUserState{}
for index := range current.MicSeats {
current.ClearMicSession(index)
}
closedEvent, err := outbox.Build(current.RoomID, "RoomClosed", current.Version+1, now, &roomeventsv1.RoomClosed{
ActorUserId: cmd.ActorUserID(),
Reason: cmd.CloseReason,
})
if err != nil {
return mutationResult{}, nil, err
}
records = append(records, closedEvent)
for _, targetUserID := range kickTargets {
kickEvent, err := outbox.Build(current.RoomID, "RoomUserKicked", current.Version+1, now, &roomeventsv1.RoomUserKicked{
ActorUserId: cmd.ActorUserID(),
TargetUserId: targetUserID,
})
if err != nil {
return mutationResult{}, nil, err
}
records = append(records, kickEvent)
}
result.roomStatus = *cmd.Status
result.closeInfo = &RoomCloseInfo{Reason: cmd.CloseReason, AdminID: cmd.AdminID, AdminName: cmd.AdminName, ClosedAtMS: now.UnixMilli()}
result.syncEvent = &tencentim.RoomEvent{EventID: closedEvent.EventID, RoomID: current.RoomID, EventType: "room_closed", ActorUserID: cmd.ActorUserID(), RoomVersion: current.Version + 1}
case state.RoomStatusActive:
result.roomStatus = state.RoomStatusActive
result.closeInfo = &RoomCloseInfo{ClearOnOpen: true}
}
}
if cmd.Mode != nil {
result.roomMode = cmd.Mode
}
if cmd.VisibleRegionID != nil {
result.visibleRegionID = cmd.VisibleRegionID
}
if !changed && len(records) == 0 && result.roomMode == nil && result.visibleRegionID == nil && result.roomStatus == "" {
return mutationResult{snapshot: current.ToProto()}, nil, nil
}
current.Version++
result.snapshot = current.ToProto()
return result, records, nil
})
}
func applyAdminRoomPatch(current *state.RoomState, cmd command.AdminUpdateRoom) bool {
changed := false
if current.RoomExt == nil {
current.RoomExt = map[string]string{}
}
if cmd.Title != nil && current.RoomExt[roomExtTitleKey] != *cmd.Title {
current.RoomExt[roomExtTitleKey] = *cmd.Title
changed = true
}
if cmd.CoverURL != nil && current.RoomExt[roomExtCoverURLKey] != *cmd.CoverURL {
current.RoomExt[roomExtCoverURLKey] = *cmd.CoverURL
changed = true
}
if cmd.Description != nil && current.RoomExt[roomExtDescriptionKey] != *cmd.Description {
current.RoomExt[roomExtDescriptionKey] = *cmd.Description
changed = true
}
if cmd.Mode != nil && current.Mode != *cmd.Mode {
current.Mode = *cmd.Mode
changed = true
}
if cmd.VisibleRegionID != nil && current.VisibleRegionID != *cmd.VisibleRegionID {
current.VisibleRegionID = *cmd.VisibleRegionID
changed = true
}
if cmd.Status != nil && current.Status != *cmd.Status {
current.Status = *cmd.Status
changed = true
}
return changed
}
func adminRoomListItemProto(item AdminRoomListEntry) *roomv1.AdminRoomListItem {
return &roomv1.AdminRoomListItem{
RoomId: item.RoomID,
RoomShortId: item.RoomShortID,
VisibleRegionId: item.VisibleRegionID,
OwnerUserId: item.OwnerUserID,
Title: item.Title,
CoverUrl: item.CoverURL,
Mode: item.Mode,
Status: item.Status,
CloseReason: item.CloseReason,
ClosedByAdminId: item.ClosedByAdminID,
ClosedByAdminName: item.ClosedByAdminName,
ClosedAtMs: item.ClosedAtMS,
Heat: item.Heat,
OnlineCount: item.OnlineCount,
SeatCount: item.SeatCount,
OccupiedSeatCount: item.OccupiedSeatCount,
SortScore: item.SortScore,
CreatedAtMs: item.CreatedAtMS,
UpdatedAtMs: item.UpdatedAtMS,
}
}
func stringPtr(value string) *string {
return &value
}