811 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}