2026-05-26 01:02:12 +08:00

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
}