861 lines
30 KiB
Go
861 lines
30 KiB
Go
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
|
||
}
|