2026-06-08 19:00:23 +08:00

288 lines
10 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/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.OwnerCountryCode != nil {
// 房主国家只作为发现页分类投影字段保存在 RoomExt后台 HTTP 不开放直改入口,只有 user-service 国家事实同步会写入。
ownerCountryCode := normalizeRoomCountryCode(*cmd.OwnerCountryCode)
if current.RoomExt[roomExtOwnerCountryKey] != ownerCountryCode {
current.RoomExt[roomExtOwnerCountryKey] = ownerCountryCode
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
}