564 lines
18 KiB
Go
564 lines
18 KiB
Go
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"
|
|
resourceTypeBadge = "badge"
|
|
resourceTypeEmojiPack = "emoji_pack"
|
|
)
|
|
|
|
const (
|
|
resourcePriceTypeCoin = "coin"
|
|
resourcePriceTypeFree = "free"
|
|
)
|
|
|
|
const (
|
|
badgeFormStrip = "strip"
|
|
badgeFormTile = "tile"
|
|
)
|
|
|
|
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"`
|
|
GiftPointAmount int64 `json:"giftPointAmount"`
|
|
BadgeForm string `json:"badgeForm"`
|
|
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"`
|
|
DefaultSlot string `json:"default_slot,omitempty"`
|
|
}
|
|
|
|
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 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 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)
|
|
priceType, coinPrice, giftPointAmount := resourcePricing(r.PriceType, r.CoinPrice)
|
|
metadataJSON := resourceMetadataJSON(resourceType, r.BadgeForm)
|
|
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 (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)
|
|
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: 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 (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 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 resourcePricing(priceType string, coinPrice int64) (string, int64, int64) {
|
|
priceType = strings.ToLower(strings.TrimSpace(priceType))
|
|
if priceType == resourcePriceTypeFree {
|
|
return resourcePriceTypeFree, 0, 0
|
|
}
|
|
return resourcePriceTypeCoin, coinPrice, coinPrice
|
|
}
|
|
|
|
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("请选择徽章属性")
|
|
}
|
|
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) string {
|
|
if resourceType != resourceTypeBadge {
|
|
return "{}"
|
|
}
|
|
payload := badgeMetadataPayload{BadgeForm: normalizeBadgeForm(badgeForm)}
|
|
switch payload.BadgeForm {
|
|
case badgeFormStrip:
|
|
payload.DefaultSlot = "profile_strip"
|
|
case badgeFormTile:
|
|
payload.DefaultSlot = "honor_wall"
|
|
default:
|
|
return "{}"
|
|
}
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return "{}"
|
|
}
|
|
return string(body)
|
|
}
|
|
|
|
func normalizeBadgeForm(value string) string {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case badgeFormStrip, "long":
|
|
return badgeFormStrip
|
|
case badgeFormTile, "short":
|
|
return badgeFormTile
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func badgeFormFromMetadata(metadataJSON string) string {
|
|
payload := badgeMetadataPayload{}
|
|
if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil {
|
|
return ""
|
|
}
|
|
return normalizeBadgeForm(payload.BadgeForm)
|
|
}
|
|
|
|
func badgeFormForResource(resourceType string, metadataJSON string) string {
|
|
if normalizeResourceType(resourceType) != resourceTypeBadge {
|
|
return ""
|
|
}
|
|
return badgeFormFromMetadata(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
|
|
}
|