2026-07-17 16:55:22 +08:00

861 lines
30 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 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"
"hyapp/services/gateway-service/internal/transport/http/resourceview"
)
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"`
Format string `json:"format,omitempty"`
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 resourceEquipRequestBody struct {
EntitlementID string `json:"entitlement_id"`
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 resourceEquipRequestBody
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
}
}
commandID := strings.TrimSpace(body.CommandID)
if commandID == "" {
commandID = strings.TrimSpace(body.CommandIDAlt)
}
appCode := appcode.FromContext(request.Context())
if commandID == "" {
if appCode == "fami" {
// Fami tiered VIP 是本次新契约,不能静默用每次都会变化的 request_id 伪装业务幂等。
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
// Lalu 等旧客户端没有业务 command_id用本次 request_id 保持原有可调用性,但不承诺跨请求重放。
commandID = "legacy_equip:" + httpkit.RequestIDFromContext(request.Context())
}
resp, err := h.walletClient.EquipUserResource(request.Context(), &walletv1.EquipUserResourceRequest{
RequestId: httpkit.RequestIDFromContext(request.Context()),
CommandId: commandID,
AppCode: appCode,
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(),
Format: resourceview.FormatFromMetadata(item.GetMetadataJson()),
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
}