package resource import ( "encoding/json" "fmt" "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" resourceTypeEmojiPack = "emoji_pack" ) 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"` 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 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"` GiftPointAmount int64 `json:"giftPointAmount"` 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 grantResourceRequest struct { CommandID string `json:"commandId"` TargetUserID int64 `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 int64 `json:"targetUserId"` GroupID int64 `json:"groupId"` Reason string `json:"reason"` } func (r resourceRequest) createProto(c *gin.Context) *walletv1.CreateResourceRequest { resourceType := normalizeResourceType(r.ResourceType) walletAssetType, walletAssetAmount := resourceWalletAsset(resourceType, r.Amount) 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, UsageScopes: []string{resourceType}, AssetUrl: strings.TrimSpace(r.AssetURL), PreviewUrl: strings.TrimSpace(r.PreviewURL), AnimationUrl: strings.TrimSpace(r.AnimationURL), MetadataJson: "{}", SortOrder: r.SortOrder, OperatorUserId: actorID(c), } } func (r resourceRequest) updateProto(c *gin.Context, resourceID int64) *walletv1.UpdateResourceRequest { resourceType := normalizeResourceType(r.ResourceType) walletAssetType, walletAssetAmount := resourceWalletAsset(resourceType, r.Amount) 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, UsageScopes: []string{resourceType}, AssetUrl: strings.TrimSpace(r.AssetURL), PreviewUrl: strings.TrimSpace(r.PreviewURL), AnimationUrl: strings.TrimSpace(r.AnimationURL), 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: r.GiftPointAmount, HeatValue: r.HeatValue, 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: r.GiftPointAmount, HeatValue: r.HeatValue, 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 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 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 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 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 }