package resource import ( "encoding/json" "fmt" "strings" "time" "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" ) 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"` CoverURL string `json:"coverUrl"` AnimationURL string `json:"animationUrl"` RegionIDs []int64 `json:"regionIds"` SortOrder int32 `json:"sortOrder"` } 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 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("表情包参数不完整") } metadataJSON, err := emojiPackMetadataJSON(r.RegionIDs) 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 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) (string, error) { normalizedRegionIDs := normalizeRegionIDs(regionIDs) metadata := map[string]any{"region_scope": "all"} if len(normalizedRegionIDs) > 0 { metadata["region_scope"] = "limited" metadata["region_ids"] = normalizedRegionIDs } body, err := json.Marshal(metadata) if err != nil { return "", fmt.Errorf("表情包区域配置不正确") } return string(body), nil } 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 }