package resourceapi import ( "encoding/json" "errors" "hyapp/services/gateway-service/internal/transport/http/httpkit" "io" "net/http" "strconv" "strings" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/appcode" "hyapp/services/gateway-service/internal/auth" ) const resourceTypeEmojiPack = "emoji_pack" const ( defaultEmojiPackCategory = "默认" emojiPackPricingFree = "free" emojiPackPricingPaid = "paid" ) type resourceData struct { ResourceID int64 `json:"resource_id"` ResourceCode string `json:"resource_code"` ResourceType string `json:"resource_type"` Name string `json:"name"` Status string `json:"status"` Grantable bool `json:"grantable"` GrantStrategy string `json:"grant_strategy"` WalletAssetType string `json:"wallet_asset_type,omitempty"` WalletAssetAmount int64 `json:"wallet_asset_amount,omitempty"` PriceType string `json:"price_type,omitempty"` CoinPrice int64 `json:"coin_price,omitempty"` GiftPointAmount int64 `json:"gift_point_amount,omitempty"` UsageScopes []string `json:"usage_scopes"` AssetURL string `json:"asset_url"` PreviewURL string `json:"preview_url"` AnimationURL string `json:"animation_url"` MetadataJSON string `json:"metadata_json"` SortOrder int32 `json:"sort_order"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type resourceGroupItemData struct { GroupItemID int64 `json:"group_item_id"` ItemType string `json:"item_type"` ResourceID int64 `json:"resource_id"` Resource *resourceData `json:"resource,omitempty"` WalletAssetType string `json:"wallet_asset_type,omitempty"` WalletAssetAmount int64 `json:"wallet_asset_amount,omitempty"` Quantity int64 `json:"quantity"` DurationMS int64 `json:"duration_ms"` SortOrder int32 `json:"sort_order"` } type resourceGroupData struct { GroupID int64 `json:"group_id"` GroupCode string `json:"group_code"` Name string `json:"name"` Status string `json:"status"` Description string `json:"description"` SortOrder int32 `json:"sort_order"` Items []resourceGroupItemData `json:"items"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type resourceShopItemData struct { ShopItemID int64 `json:"shop_item_id"` ResourceID int64 `json:"resource_id"` Resource resourceData `json:"resource"` Status string `json:"status"` DurationDays int32 `json:"duration_days"` PriceType string `json:"price_type"` CoinPrice int64 `json:"coin_price"` EffectiveFromMS int64 `json:"effective_from_ms"` EffectiveToMS int64 `json:"effective_to_ms"` SortOrder int32 `json:"sort_order"` UpdatedAtMS int64 `json:"updated_at_ms"` } type resourceShopPurchaseRequestBody struct { CommandID string `json:"command_id"` CommandIDAlt string `json:"commandId"` } type resourceShopPurchaseData struct { OrderID string `json:"order_id"` TransactionID string `json:"transaction_id"` ResourceGrantID string `json:"resource_grant_id"` CoinSpent int64 `json:"coin_spent"` Balance resourceAssetBalanceData `json:"balance"` ShopItem resourceShopItemData `json:"shop_item"` Resource userResourceData `json:"resource"` } type resourceAssetBalanceData struct { AssetType string `json:"asset_type"` AvailableAmount int64 `json:"available_amount"` FrozenAmount int64 `json:"frozen_amount"` Version int64 `json:"version"` } type giftConfigData struct { GiftID string `json:"gift_id"` ResourceID int64 `json:"resource_id"` Resource resourceData `json:"resource"` Status string `json:"status"` Name string `json:"name"` SortOrder int32 `json:"sort_order"` PresentationJSON string `json:"presentation_json"` PriceVersion string `json:"price_version"` GiftTypeCode string `json:"gift_type_code"` ChargeAssetType string `json:"charge_asset_type"` CoinPrice int64 `json:"coin_price"` GiftPointAmount int64 `json:"gift_point_amount"` HeatValue int64 `json:"heat_value"` EffectiveFromMS int64 `json:"effective_from_ms"` EffectiveToMS int64 `json:"effective_to_ms"` EffectTypes []string `json:"effect_types"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` RegionIDs []int64 `json:"region_ids"` } type giftTabData struct { Key string `json:"key"` GiftTypeCode string `json:"gift_type_code"` Name string `json:"name"` Label string `json:"label"` TabKey string `json:"tab_key"` Status string `json:"status"` Order int32 `json:"order"` SortOrder int32 `json:"sort_order"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` Gifts []giftConfigData `json:"gifts"` } type emojiPackData struct { ResourceID int64 `json:"resource_id"` ResourceCode string `json:"resource_code"` Name string `json:"name"` Category string `json:"category"` PricingType string `json:"pricing_type"` CoverURL string `json:"cover_url"` AnimationURL string `json:"animation_url"` RegionIDs []int64 `json:"region_ids"` IsGlobal bool `json:"is_global"` SortOrder int32 `json:"sort_order"` UpdatedAtMS int64 `json:"updated_at_ms"` } type emojiPackMetadata struct { Category string `json:"category"` PricingType string `json:"pricing_type"` RegionIDs []int64 `json:"region_ids"` } type userResourceData struct { EntitlementID string `json:"entitlement_id"` ResourceID int64 `json:"resource_id"` Resource resourceData `json:"resource"` Status string `json:"status"` Quantity int64 `json:"quantity"` RemainingQuantity int64 `json:"remaining_quantity"` EffectiveAtMS int64 `json:"effective_at_ms"` ExpiresAtMS int64 `json:"expires_at_ms"` SourceGrantID string `json:"source_grant_id"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` Equipped bool `json:"equipped"` } func (h *Handler) listResources(writer http.ResponseWriter, request *http.Request) { if h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } page, pageSize, ok := resourcePage(request) if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.walletClient.ListResources(request.Context(), &walletv1.ListResourcesRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), ResourceType: strings.TrimSpace(request.URL.Query().Get("resource_type")), Keyword: strings.TrimSpace(request.URL.Query().Get("keyword")), Page: page, PageSize: pageSize, ActiveOnly: true, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } items := make([]resourceData, 0, len(resp.GetResources())) for _, item := range resp.GetResources() { items = append(items, resourceFromProto(item)) } httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize}) } func (h *Handler) getResourceGroup(writer http.ResponseWriter, request *http.Request) { if h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } groupID, ok := httpkit.PositiveInt64PathValue(request, "group_id") if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.walletClient.GetResourceGroup(request.Context(), &walletv1.GetResourceGroupRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), GroupId: groupID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } group := resourceGroupFromProto(resp.GetGroup()) if group.Status != "active" { httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found") return } httpkit.WriteOK(writer, request, group) } func (h *Handler) listResourceShopItems(writer http.ResponseWriter, request *http.Request) { if h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } page, pageSize, ok := resourcePage(request) if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.walletClient.ListResourceShopItems(request.Context(), &walletv1.ListResourceShopItemsRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), ResourceType: strings.TrimSpace(request.URL.Query().Get("resource_type")), Keyword: strings.TrimSpace(request.URL.Query().Get("keyword")), Page: page, PageSize: pageSize, ActiveOnly: true, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } items := make([]resourceShopItemData, 0, len(resp.GetItems())) for _, item := range resp.GetItems() { items = append(items, resourceShopItemFromProto(item)) } httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize}) } func (h *Handler) purchaseResourceShopItem(writer http.ResponseWriter, request *http.Request) { if h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } shopItemID, ok := httpkit.PositiveInt64PathValue(request, "shop_item_id") if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } var body resourceShopPurchaseRequestBody if !httpkit.Decode(writer, request, &body) { return } commandID := strings.TrimSpace(body.CommandID) if commandID == "" { commandID = strings.TrimSpace(body.CommandIDAlt) } if commandID == "" { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.walletClient.PurchaseResourceShopItem(request.Context(), &walletv1.PurchaseResourceShopItemRequest{ CommandId: commandID, AppCode: appcode.FromContext(request.Context()), UserId: auth.UserIDFromContext(request.Context()), ShopItemId: shopItemID, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, resourceShopPurchaseFromProto(resp)) } func (h *Handler) listGifts(writer http.ResponseWriter, request *http.Request) { if h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } page, pageSize, ok := resourcePage(request) if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } regionID, _, ok := optionalNonNegativeInt64Query(request, "region_id") if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.walletClient.ListGiftConfigs(request.Context(), &walletv1.ListGiftConfigsRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), Keyword: strings.TrimSpace(request.URL.Query().Get("keyword")), Page: page, PageSize: pageSize, ActiveOnly: true, RegionId: regionID, FilterRegion: true, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } items := make([]giftConfigData, 0, len(resp.GetGifts())) for _, item := range resp.GetGifts() { items = append(items, giftFromProto(item)) } httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize}) } func (h *Handler) listGiftTabs(writer http.ResponseWriter, request *http.Request) { if h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } regionID, filterRegion, ok := optionalNonNegativeInt64Query(request, "region_id") if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.walletClient.ListGiftTypeConfigs(request.Context(), &walletv1.ListGiftTypeConfigsRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), ActiveOnly: true, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } gifts, err := h.listAllActiveGifts(request, regionID, filterRegion) if err != nil { httpkit.WriteRPCError(writer, request, err) return } giftsByType := make(map[string][]giftConfigData, len(gifts)) for _, item := range gifts { giftsByType[item.GiftTypeCode] = append(giftsByType[item.GiftTypeCode], item) } items := make([]giftTabData, 0, len(resp.GetGiftTypes())) for _, item := range resp.GetGiftTypes() { tab := giftTabFromProto(item) tab.Gifts = giftsByType[tab.GiftTypeCode] if tab.Gifts == nil { tab.Gifts = []giftConfigData{} } items = append(items, tab) } httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)}) } func (h *Handler) listAllActiveGifts(request *http.Request, regionID int64, filterRegion bool) ([]giftConfigData, error) { const pageSize int32 = 100 items := make([]giftConfigData, 0, pageSize) for page := int32(1); ; page++ { resp, err := h.walletClient.ListGiftConfigs(request.Context(), &walletv1.ListGiftConfigsRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), Page: page, PageSize: pageSize, ActiveOnly: true, RegionId: regionID, FilterRegion: filterRegion, }) if err != nil { return nil, err } for _, gift := range resp.GetGifts() { items = append(items, giftFromProto(gift)) } if len(resp.GetGifts()) == 0 || int64(len(items)) >= resp.GetTotal() { return items, nil } } } func (h *Handler) listEmojiPacks(writer http.ResponseWriter, request *http.Request) { if h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } page, pageSize, ok := resourcePage(request) if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } regionID, filterRegion, ok := optionalNonNegativeInt64Query(request, "region_id") if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.walletClient.ListResources(request.Context(), &walletv1.ListResourcesRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), ResourceType: resourceTypeEmojiPack, Keyword: strings.TrimSpace(request.URL.Query().Get("keyword")), Page: 1, PageSize: 1000, ActiveOnly: true, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } all := make([]emojiPackData, 0, len(resp.GetResources())) for _, item := range resp.GetResources() { pack := emojiPackFromProto(item) if filterRegion && !emojiPackMatchesRegion(pack, regionID) { continue } all = append(all, pack) } httpkit.WriteOK(writer, request, map[string]any{"items": paginateEmojiPacks(all, page, pageSize), "total": int64(len(all)), "page": page, "page_size": pageSize}) } func (h *Handler) listMyResources(writer http.ResponseWriter, request *http.Request) { if h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resp, err := h.walletClient.ListUserResources(request.Context(), &walletv1.ListUserResourcesRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), UserId: auth.UserIDFromContext(request.Context()), ResourceType: strings.TrimSpace(request.URL.Query().Get("resource_type")), ActiveOnly: true, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } items := make([]userResourceData, 0, len(resp.GetResources())) for _, item := range resp.GetResources() { items = append(items, userResourceFromProto(item)) } httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)}) } func (h *Handler) equipMyResource(writer http.ResponseWriter, request *http.Request) { if request.Method != http.MethodPost { httpkit.WriteError(writer, request, http.StatusMethodNotAllowed, httpkit.CodeInvalidArgument, "invalid argument") return } if h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resourceID, ok := httpkit.PositiveInt64PathValue(request, "resource_id") if !ok { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } var body struct { EntitlementID string `json:"entitlement_id"` } if request.Body != nil && request.Body != http.NoBody { if err := httpkit.DecodeJSON(request.Body, &body); err != nil && !errors.Is(err, io.EOF) { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidJSON, "invalid request body") return } } resp, err := h.walletClient.EquipUserResource(request.Context(), &walletv1.EquipUserResourceRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), UserId: auth.UserIDFromContext(request.Context()), ResourceId: resourceID, EntitlementId: strings.TrimSpace(body.EntitlementID), }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, userResourceFromProto(resp.GetResource())) } func (h *Handler) unequipMyResource(writer http.ResponseWriter, request *http.Request) { if request.Method != http.MethodDelete { httpkit.WriteError(writer, request, http.StatusMethodNotAllowed, httpkit.CodeInvalidArgument, "invalid argument") return } if h.walletClient == nil { httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error") return } resourceType := strings.TrimSpace(request.PathValue("resource_type")) if resourceType == "" { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } resp, err := h.walletClient.UnequipUserResource(request.Context(), &walletv1.UnequipUserResourceRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()), UserId: auth.UserIDFromContext(request.Context()), ResourceType: resourceType, }) if err != nil { httpkit.WriteRPCError(writer, request, err) return } httpkit.WriteOK(writer, request, map[string]any{ "resource_type": resp.GetResourceType(), "unequipped": resp.GetUnequipped(), "updated_at_ms": resp.GetUpdatedAtMs(), }) } func resourceFromProto(item *walletv1.Resource) resourceData { if item == nil { return resourceData{} } return resourceData{ ResourceID: item.GetResourceId(), ResourceCode: item.GetResourceCode(), ResourceType: item.GetResourceType(), Name: item.GetName(), Status: item.GetStatus(), Grantable: item.GetGrantable(), GrantStrategy: item.GetGrantStrategy(), WalletAssetType: item.GetWalletAssetType(), WalletAssetAmount: item.GetWalletAssetAmount(), PriceType: item.GetPriceType(), CoinPrice: item.GetCoinPrice(), GiftPointAmount: item.GetGiftPointAmount(), UsageScopes: item.GetUsageScopes(), AssetURL: item.GetAssetUrl(), PreviewURL: item.GetPreviewUrl(), AnimationURL: item.GetAnimationUrl(), MetadataJSON: item.GetMetadataJson(), SortOrder: item.GetSortOrder(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), } } func resourceGroupFromProto(group *walletv1.ResourceGroup) resourceGroupData { if group == nil { return resourceGroupData{} } items := make([]resourceGroupItemData, 0, len(group.GetItems())) for _, item := range group.GetItems() { items = append(items, resourceGroupItemData{ GroupItemID: item.GetGroupItemId(), ItemType: item.GetItemType(), ResourceID: item.GetResourceId(), Resource: resourcePointerFromProto(item.GetResource()), WalletAssetType: item.GetWalletAssetType(), WalletAssetAmount: item.GetWalletAssetAmount(), Quantity: item.GetQuantity(), DurationMS: item.GetDurationMs(), SortOrder: item.GetSortOrder(), }) } return resourceGroupData{ GroupID: group.GetGroupId(), GroupCode: group.GetGroupCode(), Name: group.GetName(), Status: group.GetStatus(), Description: group.GetDescription(), SortOrder: group.GetSortOrder(), Items: items, CreatedAtMS: group.GetCreatedAtMs(), UpdatedAtMS: group.GetUpdatedAtMs(), } } func resourcePointerFromProto(item *walletv1.Resource) *resourceData { if item == nil || item.GetResourceId() == 0 { return nil } data := resourceFromProto(item) return &data } func resourceShopItemFromProto(item *walletv1.ResourceShopItem) resourceShopItemData { if item == nil { return resourceShopItemData{} } return resourceShopItemData{ ShopItemID: item.GetShopItemId(), ResourceID: item.GetResourceId(), Resource: resourceFromProto(item.GetResource()), Status: item.GetStatus(), DurationDays: item.GetDurationDays(), PriceType: item.GetPriceType(), CoinPrice: item.GetCoinPrice(), EffectiveFromMS: item.GetEffectiveFromMs(), EffectiveToMS: item.GetEffectiveToMs(), SortOrder: item.GetSortOrder(), UpdatedAtMS: item.GetUpdatedAtMs(), } } func resourceShopPurchaseFromProto(resp *walletv1.PurchaseResourceShopItemResponse) resourceShopPurchaseData { if resp == nil { return resourceShopPurchaseData{} } return resourceShopPurchaseData{ OrderID: resp.GetOrderId(), TransactionID: resp.GetTransactionId(), ResourceGrantID: resp.GetResourceGrantId(), CoinSpent: resp.GetCoinSpent(), Balance: resourceBalanceFromProto(resp.GetBalance()), ShopItem: resourceShopItemFromProto(resp.GetShopItem()), Resource: userResourceFromProto(resp.GetResource()), } } func resourceBalanceFromProto(balance *walletv1.AssetBalance) resourceAssetBalanceData { if balance == nil { return resourceAssetBalanceData{} } return resourceAssetBalanceData{ AssetType: balance.GetAssetType(), AvailableAmount: balance.GetAvailableAmount(), FrozenAmount: balance.GetFrozenAmount(), Version: balance.GetVersion(), } } func giftFromProto(gift *walletv1.GiftConfig) giftConfigData { if gift == nil { return giftConfigData{} } return giftConfigData{ GiftID: gift.GetGiftId(), ResourceID: gift.GetResourceId(), Resource: resourceFromProto(gift.GetResource()), Status: gift.GetStatus(), Name: gift.GetName(), SortOrder: gift.GetSortOrder(), PresentationJSON: gift.GetPresentationJson(), PriceVersion: gift.GetPriceVersion(), GiftTypeCode: gift.GetGiftTypeCode(), ChargeAssetType: gift.GetChargeAssetType(), CoinPrice: gift.GetCoinPrice(), GiftPointAmount: gift.GetGiftPointAmount(), HeatValue: gift.GetHeatValue(), EffectiveFromMS: gift.GetEffectiveFromMs(), EffectiveToMS: gift.GetEffectiveToMs(), EffectTypes: gift.GetEffectTypes(), CreatedAtMS: gift.GetCreatedAtMs(), UpdatedAtMS: gift.GetUpdatedAtMs(), RegionIDs: gift.GetRegionIds(), } } func giftTabFromProto(item *walletv1.GiftTypeConfig) giftTabData { if item == nil { return giftTabData{} } return giftTabData{ Key: item.GetTypeCode(), GiftTypeCode: item.GetTypeCode(), Name: item.GetName(), Label: item.GetTabKey(), TabKey: item.GetTabKey(), Status: item.GetStatus(), Order: item.GetSortOrder(), SortOrder: item.GetSortOrder(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), Gifts: []giftConfigData{}, } } func userResourceFromProto(item *walletv1.UserResourceEntitlement) userResourceData { if item == nil { return userResourceData{} } return userResourceData{ EntitlementID: item.GetEntitlementId(), ResourceID: item.GetResourceId(), Resource: resourceFromProto(item.GetResource()), Status: item.GetStatus(), Quantity: item.GetQuantity(), RemainingQuantity: item.GetRemainingQuantity(), EffectiveAtMS: item.GetEffectiveAtMs(), ExpiresAtMS: item.GetExpiresAtMs(), SourceGrantID: item.GetSourceGrantId(), CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), Equipped: item.GetEquipped(), } } func emojiPackFromProto(item *walletv1.Resource) emojiPackData { if item == nil { return emojiPackData{} } metadata := emojiPackMetadataFromJSON(item.GetMetadataJson()) return emojiPackData{ ResourceID: item.GetResourceId(), ResourceCode: item.GetResourceCode(), Name: item.GetName(), Category: metadata.Category, PricingType: metadata.PricingType, CoverURL: firstNonEmpty(item.GetPreviewUrl(), item.GetAssetUrl()), AnimationURL: item.GetAnimationUrl(), RegionIDs: metadata.RegionIDs, IsGlobal: len(metadata.RegionIDs) == 0, SortOrder: item.GetSortOrder(), UpdatedAtMS: item.GetUpdatedAtMs(), } } func emojiPackMetadataFromJSON(metadataJSON string) emojiPackMetadata { payload := emojiPackMetadata{ Category: defaultEmojiPackCategory, PricingType: emojiPackPricingFree, } if err := json.Unmarshal([]byte(metadataJSON), &payload); err != nil { return payload } payload.Category = normalizeEmojiPackCategory(payload.Category) payload.PricingType = normalizeEmojiPackPricingType(payload.PricingType) payload.RegionIDs = normalizeEmojiPackRegionIDs(payload.RegionIDs) return payload } func normalizeEmojiPackCategory(value string) string { category := strings.TrimSpace(value) if category == "" { return defaultEmojiPackCategory } return category } func normalizeEmojiPackPricingType(value string) string { if strings.ToLower(strings.TrimSpace(value)) == emojiPackPricingPaid { return emojiPackPricingPaid } return emojiPackPricingFree } func normalizeEmojiPackRegionIDs(regionIDs []int64) []int64 { out := make([]int64, 0, len(regionIDs)) seen := make(map[int64]struct{}, len(regionIDs)) for _, regionID := range regionIDs { if regionID <= 0 { return nil } if _, ok := seen[regionID]; ok { continue } seen[regionID] = struct{}{} out = append(out, regionID) } return out } func emojiPackMatchesRegion(pack emojiPackData, regionID int64) bool { if pack.IsGlobal || regionID == 0 { return true } for _, candidate := range pack.RegionIDs { if candidate == regionID { return true } } return false } func paginateEmojiPacks(items []emojiPackData, page int32, pageSize int32) []emojiPackData { start := int((page - 1) * pageSize) if start >= len(items) { return []emojiPackData{} } end := start + int(pageSize) if end > len(items) { end = len(items) } return items[start:end] } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return value } } return "" } func resourcePage(request *http.Request) (int32, int32, bool) { page, ok := httpkit.PositiveInt32Query(request, "page", 1) if !ok { return 0, 0, false } pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20) if !ok { return 0, 0, false } if raw := strings.TrimSpace(request.URL.Query().Get("pageSize")); raw != "" { parsed, err := strconv.Atoi(raw) if err != nil || parsed <= 0 { return 0, 0, false } pageSize = int32(parsed) } return page, pageSize, true } func optionalNonNegativeInt64Query(request *http.Request, name string) (int64, bool, bool) { raw := strings.TrimSpace(request.URL.Query().Get(name)) if raw == "" { return 0, false, true } value, err := strconv.ParseInt(raw, 10, 64) if err != nil || value < 0 { return 0, false, false } return value, true, true }