package resource import ( "encoding/json" "fmt" "strconv" "strings" "time" "unicode/utf8" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/middleware" walletv1 "hyapp.local/api/proto/wallet/v1" "github.com/gin-gonic/gin" ) const dayMillis int64 = 24 * 60 * 60 * 1000 const ( resourceTypeCoin = "coin" resourceTypeBadge = "badge" resourceTypeEmojiPack = "emoji_pack" resourceTypeProfileCard = "profile_card" ) const ( resourcePriceTypeCoin = "coin" resourcePriceTypeFree = "free" ) const ( badgeFormStrip = "strip" badgeFormTile = "tile" ) const ( badgeKindNormal = "normal" badgeKindLevel = "level" ) const ( defaultEmojiPackCategory = "默认" emojiPackPricingFree = "free" emojiPackPricingPaid = "paid" ) type resourceRequest struct { ResourceCode string `json:"resourceCode"` ResourceType string `json:"resourceType"` Name string `json:"name"` Status string `json:"status"` Amount int64 `json:"amount"` PriceType string `json:"priceType"` CoinPrice int64 `json:"coinPrice"` BadgeForm string `json:"badgeForm"` BadgeKind string `json:"badgeKind"` LevelTrack string `json:"levelTrack"` MetadataJSON string `json:"metadataJson"` ManagerGrantEnabled *bool `json:"managerGrantEnabled"` AssetURL string `json:"assetUrl"` PreviewURL string `json:"previewUrl"` AnimationURL string `json:"animationUrl"` SortOrder int32 `json:"sortOrder"` } type emojiPackRequest struct { Name string `json:"name"` Category string `json:"category"` PricingType string `json:"pricingType"` CoverURL string `json:"coverUrl"` AnimationURL string `json:"animationUrl"` RegionIDs []int64 `json:"regionIds"` SortOrder int32 `json:"sortOrder"` } type emojiPackMetadataPayload struct { Category string `json:"category"` PricingType string `json:"pricing_type"` RegionIDs []int64 `json:"region_ids,omitempty"` RegionScope string `json:"region_scope"` } type badgeMetadataPayload struct { BadgeForm string `json:"badge_form"` BadgeKind string `json:"badge_kind,omitempty"` LevelTrack string `json:"level_track,omitempty"` DefaultSlot string `json:"default_slot,omitempty"` } type profileCardMetadataPayload struct { ProfileCardLayout *profileCardLayoutMetadataPayload `json:"profile_card_layout,omitempty"` } type profileCardLayoutMetadataPayload struct { SourceWidth int64 `json:"source_width"` SourceHeight int64 `json:"source_height"` ColorContentWidth int64 `json:"color_content_width"` ContentTop int64 `json:"content_top"` ContentBottom int64 `json:"content_bottom"` ContentHeight int64 `json:"content_height"` ContentTopRatio float64 `json:"content_top_ratio"` ContentHeightRatio float64 `json:"content_height_ratio"` DetectVersion int64 `json:"detect_version"` } type resourceGroupRequest struct { GroupCode string `json:"groupCode"` Name string `json:"name"` Status string `json:"status"` Description string `json:"description"` SortOrder int32 `json:"sortOrder"` Items []resourceGroupItemRequest `json:"items"` } type resourceGroupItemRequest struct { ItemType string `json:"itemType"` ResourceID int64 `json:"resourceId"` AssetType string `json:"assetType"` Amount int64 `json:"amount"` DurationDays int64 `json:"durationDays"` SortOrder int32 `json:"sortOrder"` } type giftRequest struct { GiftID string `json:"giftId"` ResourceID int64 `json:"resourceId"` Status string `json:"status"` Name string `json:"name"` SortOrder int32 `json:"sortOrder"` PresentationJSON string `json:"presentationJson"` PriceVersion string `json:"priceVersion"` GiftTypeCode string `json:"giftTypeCode"` ChargeAssetType string `json:"chargeAssetType"` CoinPrice int64 `json:"coinPrice"` HeatValue int64 `json:"heatValue"` // 兼容旧后台请求;新后台不再展示或提交热度。 EffectiveAtMS int64 `json:"effectiveAtMs"` EffectiveFromMS int64 `json:"effectiveFromMs"` EffectiveToMS int64 `json:"effectiveToMs"` EffectTypes []string `json:"effectTypes"` RegionIDs []int64 `json:"regionIds"` } type giftTypeRequest struct { DisplayName string `json:"displayName"` TabName string `json:"tabName"` Status string `json:"status"` SortOrder int32 `json:"sortOrder"` } type giftTypesRequest struct { Items []giftTypeItemRequest `json:"items"` } type giftTypeItemRequest struct { TabKey string `json:"tabKey"` DisplayName string `json:"displayName"` TabName string `json:"tabName"` Status string `json:"status"` SortOrder int32 `json:"sortOrder"` } type grantResourceRequest struct { CommandID string `json:"commandId"` TargetUserID any `json:"targetUserId"` ResourceID int64 `json:"resourceId"` Quantity int64 `json:"quantity"` DurationMS int64 `json:"durationMs"` Reason string `json:"reason"` } type grantGroupRequest struct { CommandID string `json:"commandId"` TargetUserID any `json:"targetUserId"` GroupID int64 `json:"groupId"` Reason string `json:"reason"` } type resourceShopItemsRequest struct { Items []resourceShopItemRequest `json:"items"` } type resourceShopItemRequest struct { ShopItemID int64 `json:"shopItemId"` ResourceID int64 `json:"resourceId"` Status string `json:"status"` DurationDays int32 `json:"durationDays"` EffectiveFromMS int64 `json:"effectiveFromMs"` EffectiveToMS int64 `json:"effectiveToMs"` SortOrder int32 `json:"sortOrder"` } func (r resourceRequest) createProto(c *gin.Context) *walletv1.CreateResourceRequest { resourceType := normalizeResourceType(r.ResourceType) walletAssetType, walletAssetAmount := resourceWalletAsset(resourceType, r.Amount) priceType, coinPrice, giftPointAmount := resourcePricing(r.PriceType, r.CoinPrice) metadataJSON := resourceMetadataJSON(resourceType, r.BadgeForm, r.MetadataJSON, r.BadgeKind, r.LevelTrack) return &walletv1.CreateResourceRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appctx.FromContext(c.Request.Context()), ResourceCode: strings.TrimSpace(r.ResourceCode), ResourceType: resourceType, Name: strings.TrimSpace(r.Name), Status: strings.TrimSpace(r.Status), Grantable: true, ManagerGrantEnabled: managerGrantEnabledOrDefault(r.ManagerGrantEnabled), GrantStrategy: resourceGrantStrategy(resourceType), WalletAssetType: walletAssetType, WalletAssetAmount: walletAssetAmount, PriceType: priceType, CoinPrice: coinPrice, GiftPointAmount: giftPointAmount, UsageScopes: []string{resourceType}, AssetUrl: strings.TrimSpace(r.AssetURL), PreviewUrl: strings.TrimSpace(r.PreviewURL), AnimationUrl: strings.TrimSpace(r.AnimationURL), MetadataJson: metadataJSON, SortOrder: r.SortOrder, OperatorUserId: actorID(c), } } func parseGrantTargetUserID(value any) (int64, error) { // Admin 前端会先把短号解析成内部 user_id;内部 user_id 已超过 JS 安全整数, // 所以新请求用字符串传递,避免浏览器把 318705991371722752 这类 ID 四舍五入。 switch typed := value.(type) { case string: userID, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64) if err != nil || userID <= 0 { return 0, fmt.Errorf("target_user_id is invalid") } return userID, nil case json.Number: userID, err := typed.Int64() if err != nil || userID <= 0 { return 0, fmt.Errorf("target_user_id is invalid") } return userID, nil case float64: // 兼容旧 admin-platform 对短号或小整数 user_id 传 JSON number 的请求; // 长 ID 必须走字符串,否则到这里前精度已经不可恢复。 userID := int64(typed) if float64(userID) != typed || userID <= 0 { return 0, fmt.Errorf("target_user_id is invalid") } return userID, nil case int64: if typed <= 0 { return 0, fmt.Errorf("target_user_id is invalid") } return typed, nil case int: if typed <= 0 { return 0, fmt.Errorf("target_user_id is invalid") } return int64(typed), nil default: return 0, fmt.Errorf("target_user_id is required") } } func (r resourceRequest) updateProto(c *gin.Context, resourceID int64) *walletv1.UpdateResourceRequest { resourceType := normalizeResourceType(r.ResourceType) walletAssetType, walletAssetAmount := resourceWalletAsset(resourceType, r.Amount) priceType, coinPrice, giftPointAmount := resourcePricing(r.PriceType, r.CoinPrice) metadataJSON := resourceMetadataJSON(resourceType, r.BadgeForm, r.MetadataJSON, r.BadgeKind, r.LevelTrack) return &walletv1.UpdateResourceRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appctx.FromContext(c.Request.Context()), ResourceId: resourceID, ResourceCode: strings.TrimSpace(r.ResourceCode), ResourceType: resourceType, Name: strings.TrimSpace(r.Name), Status: strings.TrimSpace(r.Status), Grantable: true, ManagerGrantEnabled: managerGrantEnabledOrDefault(r.ManagerGrantEnabled), GrantStrategy: resourceGrantStrategy(resourceType), WalletAssetType: walletAssetType, WalletAssetAmount: walletAssetAmount, PriceType: priceType, CoinPrice: coinPrice, GiftPointAmount: giftPointAmount, UsageScopes: []string{resourceType}, AssetUrl: strings.TrimSpace(r.AssetURL), PreviewUrl: strings.TrimSpace(r.PreviewURL), AnimationUrl: strings.TrimSpace(r.AnimationURL), MetadataJson: metadataJSON, SortOrder: r.SortOrder, OperatorUserId: actorID(c), } } func (r emojiPackRequest) createProto(c *gin.Context) (*walletv1.CreateResourceRequest, error) { name := strings.TrimSpace(r.Name) coverURL := strings.TrimSpace(r.CoverURL) animationURL := strings.TrimSpace(r.AnimationURL) if name == "" || coverURL == "" || animationURL == "" { return nil, fmt.Errorf("表情包参数不完整") } category := normalizeEmojiPackCategory(r.Category) if utf8.RuneCountInString(category) > 64 { return nil, fmt.Errorf("表情包分类不能超过 64 个字符") } metadataJSON, err := emojiPackMetadataJSON(r.RegionIDs, category, r.PricingType) if err != nil { return nil, err } return &walletv1.CreateResourceRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appctx.FromContext(c.Request.Context()), ResourceCode: fmt.Sprintf("emoji_pack_%d", time.Now().UTC().UnixNano()), ResourceType: resourceTypeEmojiPack, Name: name, Status: "active", Grantable: false, ManagerGrantEnabled: boolPointer(false), GrantStrategy: "increase_quantity", UsageScopes: []string{resourceTypeEmojiPack}, AssetUrl: coverURL, PreviewUrl: coverURL, AnimationUrl: animationURL, MetadataJson: metadataJSON, SortOrder: r.SortOrder, OperatorUserId: actorID(c), }, nil } func (r resourceGroupRequest) createProto(c *gin.Context) *walletv1.CreateResourceGroupRequest { return &walletv1.CreateResourceGroupRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appctx.FromContext(c.Request.Context()), GroupCode: strings.TrimSpace(r.GroupCode), Name: strings.TrimSpace(r.Name), Status: strings.TrimSpace(r.Status), Description: strings.TrimSpace(r.Description), SortOrder: r.SortOrder, Items: groupItemInputs(r.Items), OperatorUserId: actorID(c), } } func (r resourceGroupRequest) updateProto(c *gin.Context, groupID int64) *walletv1.UpdateResourceGroupRequest { return &walletv1.UpdateResourceGroupRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appctx.FromContext(c.Request.Context()), GroupId: groupID, GroupCode: strings.TrimSpace(r.GroupCode), Name: strings.TrimSpace(r.Name), Status: strings.TrimSpace(r.Status), Description: strings.TrimSpace(r.Description), SortOrder: r.SortOrder, Items: groupItemInputs(r.Items), OperatorUserId: actorID(c), } } func (r giftRequest) createProto(c *gin.Context) *walletv1.CreateGiftConfigRequest { return &walletv1.CreateGiftConfigRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appctx.FromContext(c.Request.Context()), GiftId: strings.TrimSpace(r.GiftID), ResourceId: r.ResourceID, Status: strings.TrimSpace(r.Status), Name: strings.TrimSpace(r.Name), SortOrder: r.SortOrder, PresentationJson: strings.TrimSpace(r.PresentationJSON), PriceVersion: strings.TrimSpace(r.PriceVersion), GiftTypeCode: strings.TrimSpace(r.GiftTypeCode), ChargeAssetType: strings.TrimSpace(r.ChargeAssetType), CoinPrice: r.CoinPrice, GiftPointAmount: 0, HeatValue: r.CoinPrice, EffectiveAtMs: r.EffectiveAtMS, EffectiveFromMs: r.EffectiveFromMS, EffectiveToMs: r.EffectiveToMS, EffectTypes: r.EffectTypes, OperatorUserId: actorID(c), RegionIds: r.RegionIDs, } } func (r giftRequest) updateProto(c *gin.Context, giftID string) *walletv1.UpdateGiftConfigRequest { return &walletv1.UpdateGiftConfigRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appctx.FromContext(c.Request.Context()), GiftId: strings.TrimSpace(giftID), ResourceId: r.ResourceID, Status: strings.TrimSpace(r.Status), Name: strings.TrimSpace(r.Name), SortOrder: r.SortOrder, PresentationJson: strings.TrimSpace(r.PresentationJSON), PriceVersion: strings.TrimSpace(r.PriceVersion), GiftTypeCode: strings.TrimSpace(r.GiftTypeCode), ChargeAssetType: strings.TrimSpace(r.ChargeAssetType), CoinPrice: r.CoinPrice, GiftPointAmount: 0, HeatValue: r.CoinPrice, EffectiveAtMs: r.EffectiveAtMS, EffectiveFromMs: r.EffectiveFromMS, EffectiveToMs: r.EffectiveToMS, EffectTypes: r.EffectTypes, OperatorUserId: actorID(c), RegionIds: r.RegionIDs, } } func (r giftTypeRequest) upsertProto(c *gin.Context, typeCode string) *walletv1.UpsertGiftTypeConfigRequest { return &walletv1.UpsertGiftTypeConfigRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appctx.FromContext(c.Request.Context()), TypeCode: strings.TrimSpace(typeCode), Name: strings.TrimSpace(r.DisplayName), TabKey: strings.TrimSpace(r.TabName), Status: strings.TrimSpace(r.Status), SortOrder: r.SortOrder, OperatorUserId: actorID(c), } } func (r giftTypeItemRequest) upsertProto(c *gin.Context) *walletv1.UpsertGiftTypeConfigRequest { return giftTypeRequest{ DisplayName: r.DisplayName, TabName: r.TabName, Status: r.Status, SortOrder: r.SortOrder, }.upsertProto(c, r.TabKey) } func (r resourceShopItemsRequest) upsertProto(c *gin.Context) *walletv1.UpsertResourceShopItemsRequest { return &walletv1.UpsertResourceShopItemsRequest{ RequestId: middleware.CurrentRequestID(c), AppCode: appctx.FromContext(c.Request.Context()), Items: resourceShopItemInputs(r.Items), OperatorUserId: actorID(c), } } func managerGrantEnabledOrDefault(value *bool) *bool { enabled := true if value != nil { enabled = *value } return &enabled } func groupItemInputs(items []resourceGroupItemRequest) []*walletv1.ResourceGroupItemInput { out := make([]*walletv1.ResourceGroupItemInput, 0, len(items)) for _, item := range items { itemType := strings.ToLower(strings.TrimSpace(item.ItemType)) if itemType == "wallet_asset" { out = append(out, &walletv1.ResourceGroupItemInput{ ItemType: itemType, WalletAssetType: strings.TrimSpace(item.AssetType), WalletAssetAmount: item.Amount, Quantity: 1, SortOrder: item.SortOrder, }) continue } out = append(out, &walletv1.ResourceGroupItemInput{ ItemType: "resource", ResourceId: item.ResourceID, Quantity: 1, DurationMs: item.DurationDays * dayMillis, SortOrder: item.SortOrder, }) } return out } func resourceShopItemInputs(items []resourceShopItemRequest) []*walletv1.ResourceShopItemInput { out := make([]*walletv1.ResourceShopItemInput, 0, len(items)) for _, item := range items { out = append(out, &walletv1.ResourceShopItemInput{ ShopItemId: item.ShopItemID, ResourceId: item.ResourceID, Status: strings.TrimSpace(item.Status), DurationDays: item.DurationDays, EffectiveFromMs: item.EffectiveFromMS, EffectiveToMs: item.EffectiveToMS, SortOrder: item.SortOrder, }) } return out } func normalizeResourceType(value string) string { return strings.ToLower(strings.TrimSpace(value)) } func resourceWalletAsset(resourceType string, amount int64) (string, int64) { if resourceType == resourceTypeCoin { return "COIN", amount } return "", 0 } func resourcePricing(priceType string, coinPrice int64) (string, int64, int64) { priceType = strings.ToLower(strings.TrimSpace(priceType)) if priceType == resourcePriceTypeFree { return resourcePriceTypeFree, 0, 0 } return resourcePriceTypeCoin, coinPrice, 0 } func validateResourcePricing(req resourceRequest) error { priceType := strings.ToLower(strings.TrimSpace(req.PriceType)) switch priceType { case resourcePriceTypeCoin: if req.CoinPrice <= 0 { return fmt.Errorf("请输入资源价格") } case resourcePriceTypeFree: return nil default: return fmt.Errorf("请选择资源价格类型") } return nil } func validateResourceBadgeForm(req resourceRequest) error { if normalizeResourceType(req.ResourceType) != resourceTypeBadge { return nil } if normalizeBadgeForm(req.BadgeForm) == "" { return fmt.Errorf("请选择徽章属性") } badgeKind := normalizeBadgeKind(req.BadgeKind) if badgeKind == "" { return fmt.Errorf("请选择徽章类型") } if badgeKind == badgeKindLevel && !validLevelTrack(req.LevelTrack) { return fmt.Errorf("请选择等级徽章所属等级") } return nil } func resourceGrantStrategy(resourceType string) string { switch resourceType { case resourceTypeCoin: return "wallet_credit" case "avatar_frame", "profile_card", "vehicle", "chat_bubble": return "extend_expiry" case "badge": return "set_active_flag" default: return "increase_quantity" } } func resourceMetadataJSON(resourceType string, badgeForm string, metadataJSON string, badgeKind string, levelTrack string) string { switch resourceType { case resourceTypeBadge: return badgeMetadataJSON(badgeForm, badgeKind, levelTrack) case resourceTypeProfileCard: return profileCardMetadataJSON(metadataJSON) default: return "{}" } } func badgeMetadataJSON(badgeForm string, badgeKind string, levelTrack string) string { payload := badgeMetadataPayload{BadgeForm: normalizeBadgeForm(badgeForm), BadgeKind: normalizeBadgeKind(badgeKind)} switch payload.BadgeForm { case badgeFormStrip: payload.DefaultSlot = "profile_strip" case badgeFormTile: payload.DefaultSlot = "honor_wall" default: return "{}" } if payload.BadgeKind == badgeKindLevel { payload.LevelTrack = normalizeLevelTrack(levelTrack) if payload.LevelTrack == "" { return "{}" } } body, err := json.Marshal(payload) if err != nil { return "{}" } return string(body) } func profileCardMetadataJSON(metadataJSON string) string { raw := strings.TrimSpace(metadataJSON) if raw == "" { return "{}" } payload := profileCardMetadataPayload{} if err := json.Unmarshal([]byte(raw), &payload); err != nil { return "{}" } layout := sanitizeProfileCardLayoutMetadata(payload.ProfileCardLayout) if layout == nil { return "{}" } // 资料卡上传解析只允许写入布局字段;不要把后台临时调试数据透给 app。 body, err := json.Marshal(profileCardMetadataPayload{ProfileCardLayout: layout}) if err != nil { return "{}" } return string(body) } func sanitizeProfileCardLayoutMetadata(layout *profileCardLayoutMetadataPayload) *profileCardLayoutMetadataPayload { if layout == nil { return nil } if layout.SourceWidth <= 0 || layout.SourceHeight <= 0 { return nil } if layout.ContentTop < 0 || layout.ContentBottom < layout.ContentTop || layout.ContentBottom >= layout.SourceHeight { return nil } contentHeight := layout.ContentBottom - layout.ContentTop + 1 if layout.ContentHeight != contentHeight { return nil } colorContentWidth := layout.ColorContentWidth if colorContentWidth <= 0 || colorContentWidth > layout.SourceWidth { colorContentWidth = layout.SourceWidth } // 比例字段由整数边界重新计算,避免前端浮点误差或脏数据进入客户端展示协议。 return &profileCardLayoutMetadataPayload{ SourceWidth: layout.SourceWidth, SourceHeight: layout.SourceHeight, ColorContentWidth: colorContentWidth, ContentTop: layout.ContentTop, ContentBottom: layout.ContentBottom, ContentHeight: contentHeight, ContentTopRatio: profileCardRatio(layout.ContentTop, layout.SourceHeight), ContentHeightRatio: profileCardRatio(contentHeight, layout.SourceHeight), DetectVersion: 1, } } func profileCardRatio(value int64, total int64) float64 { if total <= 0 { return 0 } return float64(value) / float64(total) } func normalizeBadgeForm(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case badgeFormStrip, "long": return badgeFormStrip case badgeFormTile, "short": return badgeFormTile default: return "" } } func normalizeBadgeKind(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "", badgeKindNormal: return badgeKindNormal case badgeKindLevel: return badgeKindLevel default: return "" } } func normalizeLevelTrack(value string) string { value = strings.ToLower(strings.TrimSpace(value)) if validLevelTrack(value) { return value } return "" } func validLevelTrack(value string) bool { switch strings.ToLower(strings.TrimSpace(value)) { case "wealth", "game", "charm": return true default: return false } } func badgeFormFromMetadata(metadataJSON string) string { payload := badgeMetadataPayload{} if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil { return "" } return normalizeBadgeForm(payload.BadgeForm) } func badgeKindFromMetadata(metadataJSON string) string { payload := badgeMetadataPayload{} if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil { return badgeKindNormal } return normalizeBadgeKind(payload.BadgeKind) } func badgeLevelTrackFromMetadata(metadataJSON string) string { payload := badgeMetadataPayload{} if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil { return "" } return normalizeLevelTrack(payload.LevelTrack) } func badgeFormForResource(resourceType string, metadataJSON string) string { if normalizeResourceType(resourceType) != resourceTypeBadge { return "" } return badgeFormFromMetadata(metadataJSON) } func badgeKindForResource(resourceType string, metadataJSON string) string { if normalizeResourceType(resourceType) != resourceTypeBadge { return "" } kind := badgeKindFromMetadata(metadataJSON) if kind == "" { return badgeKindNormal } return kind } func badgeLevelTrackForResource(resourceType string, metadataJSON string) string { if normalizeResourceType(resourceType) != resourceTypeBadge { return "" } return badgeLevelTrackFromMetadata(metadataJSON) } func emojiPackMetadataJSON(regionIDs []int64, category string, pricingType string) (string, error) { normalizedRegionIDs := normalizeRegionIDs(regionIDs) metadata := emojiPackMetadataPayload{ Category: normalizeEmojiPackCategory(category), PricingType: normalizeEmojiPackPricingType(pricingType), RegionScope: "all", } if utf8.RuneCountInString(metadata.Category) > 64 { return "", fmt.Errorf("表情包分类不能超过 64 个字符") } if len(normalizedRegionIDs) > 0 { metadata.RegionScope = "limited" metadata.RegionIDs = normalizedRegionIDs } body, err := json.Marshal(metadata) if err != nil { return "", fmt.Errorf("表情包配置不正确") } return string(body), nil } func emojiPackMetadataFromJSON(metadataJSON string) emojiPackMetadataPayload { payload := emojiPackMetadataPayload{ Category: defaultEmojiPackCategory, PricingType: emojiPackPricingFree, RegionScope: "all", } if err := json.Unmarshal([]byte(metadataJSON), &payload); err != nil { return payload } payload.Category = normalizeEmojiPackCategory(payload.Category) payload.PricingType = normalizeEmojiPackPricingType(payload.PricingType) payload.RegionIDs = normalizeRegionIDs(payload.RegionIDs) if len(payload.RegionIDs) > 0 { payload.RegionScope = "limited" } else { payload.RegionScope = "all" } 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 normalizeRegionIDs(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 regionID < 0 { continue } if _, ok := seen[regionID]; ok { continue } seen[regionID] = struct{}{} out = append(out, regionID) } return out } func boolPointer(value bool) *bool { return &value }