package roomclient import ( "context" "fmt" "strings" "time" "hyapp-admin-server/internal/appctx" roomv1 "hyapp.local/api/proto/room/v1" "google.golang.org/grpc" ) type Client interface { ListRooms(ctx context.Context, req ListRoomsRequest) (*ListRoomsResult, error) GetRoom(ctx context.Context, req GetRoomRequest) (*Room, error) UpdateRoom(ctx context.Context, req UpdateRoomRequest) (*CloseRoomResult, error) DeleteRoom(ctx context.Context, req DeleteRoomRequest) (*CloseRoomResult, error) GetRoomTreasureConfig(ctx context.Context) (RoomTreasureConfig, error) UpdateRoomTreasureConfig(ctx context.Context, req UpdateRoomTreasureConfigRequest) (RoomTreasureConfig, error) GetRoomSeatConfig(ctx context.Context) (RoomSeatConfig, error) UpdateRoomSeatConfig(ctx context.Context, req UpdateRoomSeatConfigRequest) (RoomSeatConfig, error) ListRoomPins(ctx context.Context, req ListRoomPinsRequest) (ListRoomPinsResult, error) CreateRoomPin(ctx context.Context, req CreateRoomPinRequest) (RoomPin, error) CancelRoomPin(ctx context.Context, req CancelRoomPinRequest) (RoomPin, error) CloseRoom(ctx context.Context, req CloseRoomRequest) (*CloseRoomResult, error) ReopenRoom(ctx context.Context, req ReopenRoomRequest) (*CloseRoomResult, error) } type ListRoomsRequest struct { Page int PageSize int Keyword string Status string RegionID int64 SortBy string SortDirection string } type ListRoomsResult struct { Rooms []Room Total int64 ServerTimeMs int64 } type GetRoomRequest struct { RoomID string } type UpdateRoomRequest struct { RequestID string RoomID string ActorUserID int64 Title *string CoverURL *string Description *string Mode *string Status *string VisibleRegionID *int64 CloseReason string AdminID uint64 AdminName string } type DeleteRoomRequest struct { RequestID string RoomID string ActorUserID int64 AdminID uint64 AdminName string } type Room struct { RoomID string RoomShortID string VisibleRegionID int64 OwnerUserID int64 Title string CoverURL string Mode string Status string CloseReason string ClosedByAdminID uint64 ClosedByAdminName string ClosedAtMS int64 Heat int64 OnlineCount int32 SeatCount int32 OccupiedSeatCount int32 SortScore int64 CreatedAtMS int64 UpdatedAtMS int64 } type RoomTreasureConfig struct { AppCode string Enabled bool ConfigVersion int64 EnergySource string OpenDelayMS int64 BroadcastEnabled bool BroadcastScope string BroadcastDelayMS int64 RewardStackPolicy string Levels []RoomTreasureLevelConfig GiftEnergyRules []GiftEnergyRuleConfig UpdatedByAdminID int64 CreatedAtMS int64 UpdatedAtMS int64 } type RoomTreasureLevelConfig struct { Level int32 EnergyThreshold int64 CoverURL string AnimationURL string OpeningAnimationURL string OpenedImageURL string InRoomRewards []RoomTreasureRewardItem Top1Rewards []RoomTreasureRewardItem IgniterRewards []RoomTreasureRewardItem } type RoomTreasureRewardItem struct { RewardItemID string ResourceGroupID int64 Weight int64 DisplayName string IconURL string } type GiftEnergyRuleConfig struct { RuleID string GiftID string GiftTypeCode string MultiplierPPM int64 FixedEnergy int64 Excluded bool } type UpdateRoomTreasureConfigRequest struct { Config RoomTreasureConfig AdminID int64 } type RoomSeatConfig struct { CandidateSeatCounts []int32 AllowedSeatCounts []int32 DefaultSeatCount int32 UpdatedAtMS int64 } type UpdateRoomSeatConfigRequest struct { AllowedSeatCounts []int32 DefaultSeatCount int32 } type RoomPinRoom struct { RoomID string RoomShortID string VisibleRegionID int64 OwnerUserID int64 Title string CoverURL string Status string } type RoomPin struct { ID int64 VisibleRegionID int64 RoomID string Weight int64 Status string PinnedAtMS int64 ExpiresAtMS int64 CancelledAtMS int64 CreatedByAdminID uint64 CancelledByAdminID uint64 CreatedAtMS int64 UpdatedAtMS int64 Room RoomPinRoom } type ListRoomPinsRequest struct { Page int PageSize int Keyword string Status string RegionID int64 } type ListRoomPinsResult struct { Pins []RoomPin Total int64 } type CreateRoomPinRequest struct { RoomID string Weight int64 DurationDays int64 AdminID uint64 } type CancelRoomPinRequest struct { PinID int64 AdminID uint64 } type CloseRoomRequest struct { RequestID string RoomID string ActorUserID int64 Reason string } type CloseRoomResult struct { Applied bool RoomVersion int64 ServerTimeMs int64 RoomStatus string OnlineUserNum int } type ReopenRoomRequest struct { RequestID string RoomID string ActorUserID int64 } type GRPCClient struct { client roomv1.RoomCommandServiceClient queryClient roomv1.RoomQueryServiceClient } func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient { return &GRPCClient{client: roomv1.NewRoomCommandServiceClient(conn), queryClient: roomv1.NewRoomQueryServiceClient(conn)} } func (c *GRPCClient) ListRooms(ctx context.Context, req ListRoomsRequest) (*ListRoomsResult, error) { resp, err := c.queryClient.AdminListRooms(ctx, &roomv1.AdminListRoomsRequest{ Meta: requestMeta(ctx, "", 0, "admin-list-rooms"), Page: int32(req.Page), PageSize: int32(req.PageSize), Keyword: strings.TrimSpace(req.Keyword), Status: strings.TrimSpace(req.Status), VisibleRegionId: req.RegionID, SortBy: strings.TrimSpace(req.SortBy), SortDirection: strings.TrimSpace(req.SortDirection), }) if err != nil { return nil, err } rooms := make([]Room, 0, len(resp.GetRooms())) for _, item := range resp.GetRooms() { rooms = append(rooms, roomFromProto(item)) } return &ListRoomsResult{Rooms: rooms, Total: resp.GetTotal(), ServerTimeMs: resp.GetServerTimeMs()}, nil } func (c *GRPCClient) GetRoom(ctx context.Context, req GetRoomRequest) (*Room, error) { resp, err := c.queryClient.AdminGetRoom(ctx, &roomv1.AdminGetRoomRequest{ Meta: requestMeta(ctx, strings.TrimSpace(req.RoomID), 0, "admin-get-room"), RoomId: strings.TrimSpace(req.RoomID), }) if err != nil { return nil, err } room := roomFromProto(resp.GetRoom()) return &room, nil } func (c *GRPCClient) UpdateRoom(ctx context.Context, req UpdateRoomRequest) (*CloseRoomResult, error) { resp, err := c.client.AdminUpdateRoom(ctx, &roomv1.AdminUpdateRoomRequest{ Meta: requestMeta(ctx, req.RoomID, req.ActorUserID, firstNonEmpty(req.RequestID, "admin-update-room")), Title: req.Title, CoverUrl: req.CoverURL, Description: req.Description, Mode: req.Mode, Status: req.Status, VisibleRegionId: req.VisibleRegionID, CloseReason: strings.TrimSpace(req.CloseReason), AdminId: req.AdminID, AdminName: strings.TrimSpace(req.AdminName), }) if err != nil { return nil, err } return closeRoomResultFromCommand(resp.GetResult(), resp.GetRoom()), nil } func (c *GRPCClient) DeleteRoom(ctx context.Context, req DeleteRoomRequest) (*CloseRoomResult, error) { resp, err := c.client.AdminDeleteRoom(ctx, &roomv1.AdminDeleteRoomRequest{ Meta: requestMeta(ctx, req.RoomID, req.ActorUserID, firstNonEmpty(req.RequestID, "admin-delete-room")), AdminId: req.AdminID, AdminName: strings.TrimSpace(req.AdminName), }) if err != nil { return nil, err } return closeRoomResultFromCommand(resp.GetResult(), resp.GetRoom()), nil } func (c *GRPCClient) GetRoomTreasureConfig(ctx context.Context) (RoomTreasureConfig, error) { resp, err := c.queryClient.AdminGetRoomTreasureConfig(ctx, &roomv1.AdminGetRoomTreasureConfigRequest{ Meta: requestMeta(ctx, "", 0, "admin-get-room-treasure-config"), }) if err != nil { return RoomTreasureConfig{}, err } return roomTreasureConfigFromProto(resp.GetConfig()), nil } func (c *GRPCClient) UpdateRoomTreasureConfig(ctx context.Context, req UpdateRoomTreasureConfigRequest) (RoomTreasureConfig, error) { resp, err := c.client.AdminUpdateRoomTreasureConfig(ctx, &roomv1.AdminUpdateRoomTreasureConfigRequest{ Meta: requestMeta(ctx, "", req.AdminID, "admin-update-room-treasure-config"), Config: roomTreasureConfigToProto(req.Config), AdminId: req.AdminID, }) if err != nil { return RoomTreasureConfig{}, err } return roomTreasureConfigFromProto(resp.GetConfig()), nil } func (c *GRPCClient) GetRoomSeatConfig(ctx context.Context) (RoomSeatConfig, error) { resp, err := c.queryClient.AdminGetRoomSeatConfig(ctx, &roomv1.AdminGetRoomSeatConfigRequest{Meta: requestMeta(ctx, "", 0, "admin-get-room-seat-config")}) if err != nil { return RoomSeatConfig{}, err } return roomSeatConfigFromProto(resp.GetConfig()), nil } func (c *GRPCClient) UpdateRoomSeatConfig(ctx context.Context, req UpdateRoomSeatConfigRequest) (RoomSeatConfig, error) { resp, err := c.client.AdminUpdateRoomSeatConfig(ctx, &roomv1.AdminUpdateRoomSeatConfigRequest{ Meta: requestMeta(ctx, "", 0, "admin-update-room-seat-config"), AllowedSeatCounts: req.AllowedSeatCounts, DefaultSeatCount: req.DefaultSeatCount, }) if err != nil { return RoomSeatConfig{}, err } return roomSeatConfigFromProto(resp.GetConfig()), nil } func (c *GRPCClient) ListRoomPins(ctx context.Context, req ListRoomPinsRequest) (ListRoomPinsResult, error) { resp, err := c.queryClient.AdminListRoomPins(ctx, &roomv1.AdminListRoomPinsRequest{ Meta: requestMeta(ctx, "", 0, "admin-list-room-pins"), Page: int32(req.Page), PageSize: int32(req.PageSize), Keyword: strings.TrimSpace(req.Keyword), Status: strings.TrimSpace(req.Status), VisibleRegionId: req.RegionID, }) if err != nil { return ListRoomPinsResult{}, err } pins := make([]RoomPin, 0, len(resp.GetPins())) for _, pin := range resp.GetPins() { pins = append(pins, roomPinFromProto(pin)) } return ListRoomPinsResult{Pins: pins, Total: resp.GetTotal()}, nil } func (c *GRPCClient) CreateRoomPin(ctx context.Context, req CreateRoomPinRequest) (RoomPin, error) { resp, err := c.client.AdminCreateRoomPin(ctx, &roomv1.AdminCreateRoomPinRequest{ Meta: requestMeta(ctx, strings.TrimSpace(req.RoomID), int64(req.AdminID), "admin-create-room-pin"), RoomId: strings.TrimSpace(req.RoomID), Weight: req.Weight, DurationDays: req.DurationDays, AdminId: req.AdminID, }) if err != nil { return RoomPin{}, err } return roomPinFromProto(resp.GetPin()), nil } func (c *GRPCClient) CancelRoomPin(ctx context.Context, req CancelRoomPinRequest) (RoomPin, error) { resp, err := c.client.AdminCancelRoomPin(ctx, &roomv1.AdminCancelRoomPinRequest{ Meta: requestMeta(ctx, "", int64(req.AdminID), "admin-cancel-room-pin"), PinId: req.PinID, AdminId: req.AdminID, }) if err != nil { return RoomPin{}, err } return roomPinFromProto(resp.GetPin()), nil } func (c *GRPCClient) CloseRoom(ctx context.Context, req CloseRoomRequest) (*CloseRoomResult, error) { return c.setRoomLifecycle(ctx, req.RequestID, req.RoomID, req.ActorUserID, firstNonEmpty(req.Reason, "admin_closed")) } func (c *GRPCClient) ReopenRoom(ctx context.Context, req ReopenRoomRequest) (*CloseRoomResult, error) { return c.setRoomLifecycle(ctx, req.RequestID, req.RoomID, req.ActorUserID, "admin_reopen") } func (c *GRPCClient) setRoomLifecycle(ctx context.Context, rawRequestID string, rawRoomID string, actorUserID int64, rawReason string) (*CloseRoomResult, error) { if c == nil || c.client == nil { return nil, fmt.Errorf("room service client is not configured") } roomID := strings.TrimSpace(rawRoomID) reason := strings.TrimSpace(rawReason) if roomID == "" || actorUserID <= 0 || reason == "" { return nil, fmt.Errorf("room lifecycle request is incomplete") } requestID := strings.TrimSpace(rawRequestID) if requestID == "" { requestID = fmt.Sprintf("admin-room-lifecycle-%s-%d", roomID, time.Now().UnixMilli()) } resp, err := c.client.CloseRoom(ctx, &roomv1.CloseRoomRequest{ Meta: requestMeta(ctx, roomID, actorUserID, requestID+":"+reason), Reason: reason, }) if err != nil { return nil, err } return closeRoomResultFromCommand(resp.GetResult(), resp.GetRoom()), nil } func closeRoomResultFromCommand(result *roomv1.CommandResult, room *roomv1.RoomSnapshot) *CloseRoomResult { return &CloseRoomResult{ Applied: result.GetApplied(), RoomVersion: result.GetRoomVersion(), ServerTimeMs: result.GetServerTimeMs(), RoomStatus: room.GetStatus(), OnlineUserNum: len(room.GetOnlineUsers()), } } func commandID(requestID string, reason string, roomID string) string { base := strings.TrimSpace(requestID) if base == "" { base = fmt.Sprintf("admin-room-lifecycle-%d", time.Now().UnixMilli()) } value := base + ":" + strings.TrimSpace(reason) + ":" + strings.TrimSpace(roomID) if len(value) <= 128 { return value } return value[:128] } func requestMeta(ctx context.Context, roomID string, actorUserID int64, seed string) *roomv1.RequestMeta { requestID := strings.TrimSpace(seed) if requestID == "" { requestID = fmt.Sprintf("admin-room-%d", time.Now().UnixMilli()) } return &roomv1.RequestMeta{ RequestId: requestID, CommandId: commandID(requestID, "admin", roomID), ActorUserId: actorUserID, RoomId: strings.TrimSpace(roomID), AppCode: appctx.FromContext(ctx), SentAtMs: time.Now().UnixMilli(), } } func roomFromProto(item *roomv1.AdminRoomListItem) Room { if item == nil { return Room{} } return Room{ RoomID: item.GetRoomId(), RoomShortID: item.GetRoomShortId(), VisibleRegionID: item.GetVisibleRegionId(), OwnerUserID: item.GetOwnerUserId(), Title: item.GetTitle(), CoverURL: item.GetCoverUrl(), Mode: item.GetMode(), Status: item.GetStatus(), CloseReason: item.GetCloseReason(), ClosedByAdminID: item.GetClosedByAdminId(), ClosedByAdminName: item.GetClosedByAdminName(), ClosedAtMS: item.GetClosedAtMs(), Heat: item.GetHeat(), OnlineCount: item.GetOnlineCount(), SeatCount: item.GetSeatCount(), OccupiedSeatCount: item.GetOccupiedSeatCount(), SortScore: item.GetSortScore(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), } } func roomTreasureConfigFromProto(input *roomv1.AdminRoomTreasureConfig) RoomTreasureConfig { if input == nil { return RoomTreasureConfig{} } return RoomTreasureConfig{ AppCode: input.GetAppCode(), Enabled: input.GetEnabled(), ConfigVersion: input.GetConfigVersion(), EnergySource: input.GetEnergySource(), OpenDelayMS: input.GetOpenDelayMs(), BroadcastEnabled: input.GetBroadcastEnabled(), BroadcastScope: input.GetBroadcastScope(), BroadcastDelayMS: input.GetBroadcastDelayMs(), RewardStackPolicy: input.GetRewardStackPolicy(), Levels: roomTreasureLevelsFromProto(input.GetLevels()), GiftEnergyRules: roomTreasureEnergyRulesFromProto(input.GetGiftEnergyRules()), UpdatedByAdminID: input.GetUpdatedByAdminId(), CreatedAtMS: input.GetCreatedAtMs(), UpdatedAtMS: input.GetUpdatedAtMs(), } } func roomSeatConfigFromProto(input *roomv1.AdminRoomSeatConfig) RoomSeatConfig { if input == nil { return RoomSeatConfig{} } return RoomSeatConfig{ CandidateSeatCounts: input.GetCandidateSeatCounts(), AllowedSeatCounts: input.GetAllowedSeatCounts(), DefaultSeatCount: input.GetDefaultSeatCount(), UpdatedAtMS: input.GetUpdatedAtMs(), } } func roomPinFromProto(input *roomv1.AdminRoomPin) RoomPin { if input == nil { return RoomPin{} } room := input.GetRoom() return RoomPin{ ID: input.GetId(), VisibleRegionID: input.GetVisibleRegionId(), RoomID: input.GetRoomId(), Weight: input.GetWeight(), Status: input.GetStatus(), PinnedAtMS: input.GetPinnedAtMs(), ExpiresAtMS: input.GetExpiresAtMs(), CancelledAtMS: input.GetCancelledAtMs(), CreatedByAdminID: input.GetCreatedByAdminId(), CancelledByAdminID: input.GetCancelledByAdminId(), CreatedAtMS: input.GetCreatedAtMs(), UpdatedAtMS: input.GetUpdatedAtMs(), Room: RoomPinRoom{ RoomID: room.GetRoomId(), RoomShortID: room.GetRoomShortId(), VisibleRegionID: room.GetVisibleRegionId(), OwnerUserID: room.GetOwnerUserId(), Title: room.GetTitle(), CoverURL: room.GetCoverUrl(), Status: room.GetStatus(), }, } } func roomTreasureConfigToProto(input RoomTreasureConfig) *roomv1.AdminRoomTreasureConfig { return &roomv1.AdminRoomTreasureConfig{ Enabled: input.Enabled, EnergySource: input.EnergySource, OpenDelayMs: input.OpenDelayMS, BroadcastEnabled: input.BroadcastEnabled, BroadcastScope: input.BroadcastScope, BroadcastDelayMs: input.BroadcastDelayMS, RewardStackPolicy: input.RewardStackPolicy, Levels: roomTreasureLevelsToProto(input.Levels), GiftEnergyRules: roomTreasureEnergyRulesToProto(input.GiftEnergyRules), } } func roomTreasureLevelsFromProto(input []*roomv1.RoomTreasureLevel) []RoomTreasureLevelConfig { out := make([]RoomTreasureLevelConfig, 0, len(input)) for _, level := range input { if level == nil { continue } out = append(out, RoomTreasureLevelConfig{ Level: level.GetLevel(), EnergyThreshold: level.GetEnergyThreshold(), CoverURL: level.GetCoverUrl(), AnimationURL: level.GetAnimationUrl(), OpeningAnimationURL: level.GetOpeningAnimationUrl(), OpenedImageURL: level.GetOpenedImageUrl(), InRoomRewards: roomTreasureRewardsFromProto(level.GetInRoomRewards()), Top1Rewards: roomTreasureRewardsFromProto(level.GetTop1Rewards()), IgniterRewards: roomTreasureRewardsFromProto(level.GetIgniterRewards()), }) } return out } func roomTreasureLevelsToProto(input []RoomTreasureLevelConfig) []*roomv1.RoomTreasureLevel { out := make([]*roomv1.RoomTreasureLevel, 0, len(input)) for _, level := range input { out = append(out, &roomv1.RoomTreasureLevel{ Level: level.Level, EnergyThreshold: level.EnergyThreshold, CoverUrl: level.CoverURL, AnimationUrl: level.AnimationURL, OpeningAnimationUrl: level.OpeningAnimationURL, OpenedImageUrl: level.OpenedImageURL, InRoomRewards: roomTreasureRewardsToProto(level.InRoomRewards), Top1Rewards: roomTreasureRewardsToProto(level.Top1Rewards), IgniterRewards: roomTreasureRewardsToProto(level.IgniterRewards), }) } return out } func roomTreasureRewardsFromProto(input []*roomv1.RoomTreasureRewardItem) []RoomTreasureRewardItem { out := make([]RoomTreasureRewardItem, 0, len(input)) for _, reward := range input { if reward == nil { continue } out = append(out, RoomTreasureRewardItem{ RewardItemID: reward.GetRewardItemId(), ResourceGroupID: reward.GetResourceGroupId(), Weight: reward.GetWeight(), DisplayName: reward.GetDisplayName(), IconURL: reward.GetIconUrl(), }) } return out } func roomTreasureRewardsToProto(input []RoomTreasureRewardItem) []*roomv1.RoomTreasureRewardItem { out := make([]*roomv1.RoomTreasureRewardItem, 0, len(input)) for _, reward := range input { out = append(out, &roomv1.RoomTreasureRewardItem{ RewardItemId: reward.RewardItemID, ResourceGroupId: reward.ResourceGroupID, Weight: reward.Weight, DisplayName: reward.DisplayName, IconUrl: reward.IconURL, }) } return out } func roomTreasureEnergyRulesFromProto(input []*roomv1.RoomTreasureGiftEnergyRule) []GiftEnergyRuleConfig { out := make([]GiftEnergyRuleConfig, 0, len(input)) for _, rule := range input { if rule == nil { continue } out = append(out, GiftEnergyRuleConfig{ RuleID: rule.GetRuleId(), GiftID: rule.GetGiftId(), GiftTypeCode: rule.GetGiftTypeCode(), MultiplierPPM: rule.GetMultiplierPpm(), FixedEnergy: rule.GetFixedEnergy(), Excluded: rule.GetExcluded(), }) } return out } func roomTreasureEnergyRulesToProto(input []GiftEnergyRuleConfig) []*roomv1.RoomTreasureGiftEnergyRule { out := make([]*roomv1.RoomTreasureGiftEnergyRule, 0, len(input)) for _, rule := range input { out = append(out, &roomv1.RoomTreasureGiftEnergyRule{ RuleId: rule.RuleID, GiftId: rule.GiftID, GiftTypeCode: rule.GiftTypeCode, MultiplierPpm: rule.MultiplierPPM, FixedEnergy: rule.FixedEnergy, Excluded: rule.Excluded, }) } return out } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return value } } return "" }