2026-06-30 10:27:29 +08:00

309 lines
11 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()),
NowMS: s.clock.Now().UnixMilli(),
})
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(), s.clock.Now().UnixMilli())
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()),
}
result, err := s.mutateRoom(ctx, cmd, false, func(_ time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
if current == nil {
return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found")
}
// 删除不是“关闭”的另一种展示状态,而是后台确认移除房间恢复来源;状态只服务本次响应和内存 Cell 收尾。
current.Status = state.RoomStatusDeleted
current.OnlineUsers = map[int64]*state.RoomUserState{}
for index := range current.MicSeats {
current.ClearMicSession(index)
}
current.Version++
return mutationResult{
snapshot: current.ToProto(),
deleteRoom: true,
}, nil, nil
})
if err != nil {
return nil, err
}
if result.applied {
s.deleteRoomListCacheBestEffort(ctx, cmd.RoomID())
s.forgetLoadedRoom(ctx, cmd.RoomID())
}
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:
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.OwnerCountryCode != nil {
value := normalizeRoomCountryCode(req.GetOwnerCountryCode())
if value == "" {
// 国家码只服务内部同步和运维 backfill入口仍按列表筛选同一套规则拒绝非法值。
return command.AdminUpdateRoom{}, xerr.New(xerr.InvalidArgument, "owner country code is invalid")
}
cmd.OwnerCountryCode = &value
}
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{
// 房主国家是列表国家筛选和恢复态的一致性字段,内部 backfill 必须立刻把 RoomExt 写进 snapshot。
forceSnapshot: cmd.OwnerCountryCode != nil,
}
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 == "" {
result.snapshot = current.ToProto()
return result, 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,
}
}