1131 lines
36 KiB
Go
1131 lines
36 KiB
Go
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"
|
||
resourceTypeAvatarFrame = "avatar_frame"
|
||
resourceTypeBadge = "badge"
|
||
resourceTypeEmojiPack = "emoji_pack"
|
||
resourceTypeProfileCard = "profile_card"
|
||
)
|
||
|
||
const (
|
||
resourceAnimationFormatMP4 = "mp4"
|
||
)
|
||
|
||
const (
|
||
mp4AlphaLayoutNormal = "normal"
|
||
mp4AlphaLayoutAlphaLeftRGBRight = "alpha_left_rgb_right"
|
||
mp4AlphaLayoutRGBLeftAlphaRight = "rgb_left_alpha_right"
|
||
mp4AlphaLayoutAlphaTopRGBBottom = "alpha_top_rgb_bottom"
|
||
mp4AlphaLayoutRGBTopAlphaBottom = "rgb_top_alpha_bottom"
|
||
mp4AlphaLayoutCustom = "custom"
|
||
mp4AlphaLayoutDetectVersion int64 = 1
|
||
)
|
||
|
||
const (
|
||
resourcePriceTypeCoin = "coin"
|
||
resourcePriceTypeFree = "free"
|
||
)
|
||
|
||
const (
|
||
badgeFormStrip = "strip"
|
||
badgeFormTile = "tile"
|
||
)
|
||
|
||
const (
|
||
badgeKindNormal = "normal"
|
||
badgeKindLevel = "level"
|
||
)
|
||
|
||
const (
|
||
avatarFrameKindNormal = "normal"
|
||
avatarFrameKindLevel = "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"`
|
||
AvatarFrameKind string `json:"avatarFrameKind"`
|
||
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 resourceMP4LayoutBatchRequest struct {
|
||
Items []resourceMP4LayoutUpdateRequest `json:"items"`
|
||
}
|
||
|
||
type resourceMP4LayoutUpdateRequest struct {
|
||
ResourceID int64 `json:"resourceId"`
|
||
MetadataJSON string `json:"metadataJson"`
|
||
}
|
||
|
||
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 resourceMetadataPayload struct {
|
||
AnimationFormat string `json:"animation_format,omitempty"`
|
||
AvatarFrameKind string `json:"avatar_frame_kind,omitempty"`
|
||
LevelTrack string `json:"level_track,omitempty"`
|
||
MP4AlphaLayout *mp4AlphaLayoutMetadataPayload `json:"mp4_alpha_layout,omitempty"`
|
||
ProfileCardLayout *profileCardLayoutMetadataPayload `json:"profile_card_layout,omitempty"`
|
||
}
|
||
|
||
type mp4AlphaLayoutMetadataPayload struct {
|
||
AlphaLayout string `json:"alpha_layout"`
|
||
VideoWidth int64 `json:"video_w"`
|
||
VideoHeight int64 `json:"video_h"`
|
||
RGBFrame []int64 `json:"rgb_frame"`
|
||
AlphaFrame []int64 `json:"alpha_frame"`
|
||
Confirmed bool `json:"confirmed"`
|
||
Confidence float64 `json:"confidence,omitempty"`
|
||
DetectVersion int64 `json:"detect_version"`
|
||
}
|
||
|
||
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"`
|
||
CPRelationType string `json:"cpRelationType"`
|
||
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, r.AvatarFrameKind)
|
||
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, r.AvatarFrameKind)
|
||
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),
|
||
CpRelationType: r.cpRelationType(),
|
||
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),
|
||
CpRelationType: r.cpRelationType(),
|
||
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) cpRelationType() string {
|
||
if strings.TrimSpace(r.GiftTypeCode) != "cp" {
|
||
return ""
|
||
}
|
||
// CP 关系类型最终由 wallet-service 写回 presentation_json;兼容旧前端只提交 JSON 的请求,避免保存时把兄弟/姐妹关系清掉。
|
||
if value := strings.TrimSpace(r.CPRelationType); value != "" {
|
||
return value
|
||
}
|
||
var payload map[string]any
|
||
if err := json.Unmarshal([]byte(strings.TrimSpace(r.PresentationJSON)), &payload); err != nil {
|
||
return ""
|
||
}
|
||
if value, ok := payload["cp_relation_type"].(string); ok {
|
||
return strings.TrimSpace(value)
|
||
}
|
||
if value, ok := payload["cpRelationType"].(string); ok {
|
||
return strings.TrimSpace(value)
|
||
}
|
||
return ""
|
||
}
|
||
|
||
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 := false
|
||
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 validateResourceAvatarFrameKind(req resourceRequest) error {
|
||
if normalizeResourceType(req.ResourceType) != resourceTypeAvatarFrame {
|
||
return nil
|
||
}
|
||
avatarFrameKind := normalizeAvatarFrameKind(req.AvatarFrameKind)
|
||
if avatarFrameKind == "" {
|
||
return fmt.Errorf("请选择头像框类型")
|
||
}
|
||
if avatarFrameKind == avatarFrameKindLevel && !validLevelTrack(req.LevelTrack) {
|
||
return fmt.Errorf("请选择等级头像框所属等级")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateResourceMetadata(req resourceRequest) error {
|
||
resourceType := normalizeResourceType(req.ResourceType)
|
||
payload, fields, err := parseResourceMetadataPayload(req.MetadataJSON)
|
||
if err != nil {
|
||
return fmt.Errorf("资源元数据格式不正确")
|
||
}
|
||
if payload == nil {
|
||
return nil
|
||
}
|
||
if rawFormat, ok := fields["animation_format"]; ok && len(rawFormat) > 0 {
|
||
animationFormat := strings.ToLower(strings.TrimSpace(payload.AnimationFormat))
|
||
if animationFormat != "" && animationFormat != resourceAnimationFormatMP4 {
|
||
return fmt.Errorf("资源动效格式不支持")
|
||
}
|
||
}
|
||
if _, ok := fields["mp4_alpha_layout"]; !ok {
|
||
return nil
|
||
}
|
||
if !resourceAllowsAnimationMetadata(resourceType) {
|
||
return fmt.Errorf("当前资源类型不支持 MP4 透明布局")
|
||
}
|
||
layout, err := sanitizeMP4AlphaLayoutMetadata(payload.MP4AlphaLayout)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// MP4 透明布局是客户端合成协议,后台只能保存人工确认后的显式矩形,
|
||
// 否则 Flutter 会重新落回不稳定的自动识别路径,黑场或弱 mask 素材仍会展示错误。
|
||
if !layout.Confirmed {
|
||
return fmt.Errorf("请确认 MP4 透明布局")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func resourceGrantStrategy(resourceType string) string {
|
||
switch resourceType {
|
||
case resourceTypeCoin:
|
||
return "wallet_credit"
|
||
case resourceTypeAvatarFrame, "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, avatarFrameKind string) string {
|
||
if resourceType == resourceTypeBadge {
|
||
return badgeMetadataJSON(badgeForm, badgeKind, levelTrack)
|
||
}
|
||
|
||
rawPayload, _, err := parseResourceMetadataPayload(metadataJSON)
|
||
if err != nil {
|
||
return "{}"
|
||
}
|
||
if rawPayload == nil {
|
||
rawPayload = &resourceMetadataPayload{}
|
||
}
|
||
payload := resourceMetadataPayload{}
|
||
if resourceType == resourceTypeProfileCard {
|
||
payload.ProfileCardLayout = sanitizeProfileCardLayoutMetadata(rawPayload.ProfileCardLayout)
|
||
}
|
||
if resourceType == resourceTypeAvatarFrame {
|
||
kind := normalizeAvatarFrameKind(avatarFrameKind)
|
||
payload.AvatarFrameKind = kind
|
||
if kind == avatarFrameKindLevel {
|
||
// 等级头像框会被等级奖励任务按轨道过滤并发放;普通头像框不写 level_track,避免商店、经理赠送等入口误判。
|
||
payload.LevelTrack = normalizeLevelTrack(levelTrack)
|
||
}
|
||
}
|
||
if resourceAllowsAnimationMetadata(resourceType) {
|
||
if layout, err := sanitizeMP4AlphaLayoutMetadata(rawPayload.MP4AlphaLayout); err == nil && layout != nil && layout.Confirmed {
|
||
payload.AnimationFormat = resourceAnimationFormatMP4
|
||
payload.MP4AlphaLayout = layout
|
||
}
|
||
}
|
||
return marshalResourceMetadataPayload(payload)
|
||
}
|
||
|
||
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 := resourceMetadataPayload{}
|
||
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 parseResourceMetadataPayload(metadataJSON string) (*resourceMetadataPayload, map[string]json.RawMessage, error) {
|
||
raw := strings.TrimSpace(metadataJSON)
|
||
if raw == "" {
|
||
return nil, nil, nil
|
||
}
|
||
fields := map[string]json.RawMessage{}
|
||
if err := json.Unmarshal([]byte(raw), &fields); err != nil {
|
||
return nil, nil, err
|
||
}
|
||
payload := resourceMetadataPayload{}
|
||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||
return nil, nil, err
|
||
}
|
||
return &payload, fields, nil
|
||
}
|
||
|
||
func marshalResourceMetadataPayload(payload resourceMetadataPayload) string {
|
||
if payload.AnimationFormat == "" && payload.AvatarFrameKind == "" && payload.LevelTrack == "" && payload.MP4AlphaLayout == nil && payload.ProfileCardLayout == nil {
|
||
return "{}"
|
||
}
|
||
body, err := json.Marshal(payload)
|
||
if err != nil {
|
||
return "{}"
|
||
}
|
||
return string(body)
|
||
}
|
||
|
||
func sanitizeMP4AlphaLayoutMetadata(layout *mp4AlphaLayoutMetadataPayload) (*mp4AlphaLayoutMetadataPayload, error) {
|
||
if layout == nil {
|
||
return nil, fmt.Errorf("MP4 透明布局参数不完整")
|
||
}
|
||
videoWidth := layout.VideoWidth
|
||
videoHeight := layout.VideoHeight
|
||
if videoWidth <= 0 || videoHeight <= 0 {
|
||
return nil, fmt.Errorf("MP4 视频宽高不正确")
|
||
}
|
||
alphaLayout := normalizeMP4AlphaLayout(layout.AlphaLayout)
|
||
if alphaLayout == "" {
|
||
return nil, fmt.Errorf("MP4 透明布局类型不支持")
|
||
}
|
||
detectVersion := layout.DetectVersion
|
||
if detectVersion <= 0 {
|
||
detectVersion = mp4AlphaLayoutDetectVersion
|
||
}
|
||
confidence := layout.Confidence
|
||
if confidence < 0 {
|
||
confidence = 0
|
||
}
|
||
if confidence > 1 {
|
||
confidence = 1
|
||
}
|
||
|
||
if alphaLayout == mp4AlphaLayoutNormal {
|
||
// normal 也是显式布局:RGB 使用完整视频帧,alpha_frame 固定为 null,
|
||
// 这样 native 层能明确跳过 split-alpha 猜测,普通 MP4 不再被误判。
|
||
return &mp4AlphaLayoutMetadataPayload{
|
||
AlphaLayout: mp4AlphaLayoutNormal,
|
||
VideoWidth: videoWidth,
|
||
VideoHeight: videoHeight,
|
||
RGBFrame: []int64{0, 0, videoWidth, videoHeight},
|
||
AlphaFrame: nil,
|
||
Confirmed: layout.Confirmed,
|
||
Confidence: confidence,
|
||
DetectVersion: detectVersion,
|
||
}, nil
|
||
}
|
||
if !validMP4Frame(layout.RGBFrame, videoWidth, videoHeight) || !validMP4Frame(layout.AlphaFrame, videoWidth, videoHeight) {
|
||
return nil, fmt.Errorf("MP4 透明布局矩形不正确")
|
||
}
|
||
// 透明 MP4 的 RGB 与 alpha 矩形直接影响客户端 shader 取样,后端只做边界归一化,
|
||
// 不把字段重算成左右或上下默认值,避免覆盖前端人工确认过的 custom 细节。
|
||
return &mp4AlphaLayoutMetadataPayload{
|
||
AlphaLayout: alphaLayout,
|
||
VideoWidth: videoWidth,
|
||
VideoHeight: videoHeight,
|
||
RGBFrame: append([]int64(nil), layout.RGBFrame[:4]...),
|
||
AlphaFrame: append([]int64(nil), layout.AlphaFrame[:4]...),
|
||
Confirmed: layout.Confirmed,
|
||
Confidence: confidence,
|
||
DetectVersion: detectVersion,
|
||
}, nil
|
||
}
|
||
|
||
func validMP4Frame(frame []int64, videoWidth int64, videoHeight int64) bool {
|
||
if len(frame) < 4 {
|
||
return false
|
||
}
|
||
left, top, width, height := frame[0], frame[1], frame[2], frame[3]
|
||
if left < 0 || top < 0 || width <= 0 || height <= 0 {
|
||
return false
|
||
}
|
||
return left+width <= videoWidth && top+height <= videoHeight
|
||
}
|
||
|
||
func normalizeMP4AlphaLayout(value string) string {
|
||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||
case mp4AlphaLayoutNormal:
|
||
return mp4AlphaLayoutNormal
|
||
case mp4AlphaLayoutAlphaLeftRGBRight:
|
||
return mp4AlphaLayoutAlphaLeftRGBRight
|
||
case mp4AlphaLayoutRGBLeftAlphaRight:
|
||
return mp4AlphaLayoutRGBLeftAlphaRight
|
||
case mp4AlphaLayoutAlphaTopRGBBottom:
|
||
return mp4AlphaLayoutAlphaTopRGBBottom
|
||
case mp4AlphaLayoutRGBTopAlphaBottom:
|
||
return mp4AlphaLayoutRGBTopAlphaBottom
|
||
case mp4AlphaLayoutCustom:
|
||
return mp4AlphaLayoutCustom
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func resourceAllowsAnimationMetadata(resourceType string) bool {
|
||
switch resourceType {
|
||
case "gift", "vehicle", "avatar_frame", "chat_bubble", "floating_screen", resourceTypeProfileCard:
|
||
return true
|
||
default:
|
||
return strings.HasPrefix(resourceType, "mic_seat_")
|
||
}
|
||
}
|
||
|
||
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 normalizeAvatarFrameKind(value string) string {
|
||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||
case "", avatarFrameKindNormal:
|
||
return avatarFrameKindNormal
|
||
case avatarFrameKindLevel:
|
||
return avatarFrameKindLevel
|
||
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 avatarFrameKindFromMetadata(metadataJSON string) string {
|
||
payload := resourceMetadataPayload{}
|
||
if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil {
|
||
return avatarFrameKindNormal
|
||
}
|
||
kind := normalizeAvatarFrameKind(payload.AvatarFrameKind)
|
||
if kind == "" {
|
||
return avatarFrameKindNormal
|
||
}
|
||
return kind
|
||
}
|
||
|
||
func avatarFrameLevelTrackFromMetadata(metadataJSON string) string {
|
||
payload := resourceMetadataPayload{}
|
||
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 avatarFrameKindForResource(resourceType string, metadataJSON string) string {
|
||
if normalizeResourceType(resourceType) != resourceTypeAvatarFrame {
|
||
return ""
|
||
}
|
||
return avatarFrameKindFromMetadata(metadataJSON)
|
||
}
|
||
|
||
func avatarFrameLevelTrackForResource(resourceType string, metadataJSON string) string {
|
||
if normalizeResourceType(resourceType) != resourceTypeAvatarFrame {
|
||
return ""
|
||
}
|
||
return avatarFrameLevelTrackFromMetadata(metadataJSON)
|
||
}
|
||
|
||
func levelTrackForResource(resourceType string, metadataJSON string) string {
|
||
switch normalizeResourceType(resourceType) {
|
||
case resourceTypeBadge:
|
||
return badgeLevelTrackFromMetadata(metadataJSON)
|
||
case resourceTypeAvatarFrame:
|
||
return avatarFrameLevelTrackFromMetadata(metadataJSON)
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|