2026-05-19 19:50:50 +08:00

713 lines
24 KiB
Go

package wheel
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"time"
"chatapp3-golang/internal/model"
"chatapp3-golang/internal/utils"
"gorm.io/gorm"
)
type configBundle struct {
Config *model.WheelConfig
Categories []model.WheelPoolConfig
Rewards []model.WheelRewardConfig
}
// GetConfig returns admin wheel configuration.
func (s *Service) GetConfig(ctx context.Context, sysOrigin string) (*ConfigResponse, error) {
bundle, err := s.loadConfigBundle(ctx, sysOrigin, false)
if err != nil {
return nil, err
}
if bundle.Config == nil {
return defaultConfigResponse(sysOrigin), nil
}
return s.buildConfigResponse(bundle)
}
// GetAppConfig returns enabled wheel configuration for app clients.
func (s *Service) GetAppConfig(ctx context.Context, user AuthUser) (*ConfigResponse, error) {
bundle, err := s.loadConfigBundle(ctx, user.SysOrigin, true)
if err != nil {
return nil, err
}
if bundle.Config == nil {
return &ConfigResponse{
Configured: false,
SysOrigin: normalizeSysOrigin(user.SysOrigin),
Timezone: defaultTimezone,
ProbabilityTotal: probabilityTotal,
Categories: defaultCategoryPayloads(false),
}, nil
}
return s.buildConfigResponse(bundle)
}
// SaveConfig saves admin wheel configuration.
func (s *Service) SaveConfig(ctx context.Context, req SaveConfigRequest) (*ConfigResponse, error) {
normalized, err := normalizeSaveRequest(req)
if err != nil {
return nil, err
}
configEnabled := false
for _, category := range normalized.Categories {
if category.Enabled {
configEnabled = true
break
}
}
now := time.Now()
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var configRow model.WheelConfig
if err := withWriteLock(tx).Where("sys_origin = ?", normalized.SysOrigin).First(&configRow).Error; errors.Is(err, gorm.ErrRecordNotFound) {
nextID, idErr := utils.NextID()
if idErr != nil {
return idErr
}
configRow = model.WheelConfig{
ID: nextID,
SysOrigin: normalized.SysOrigin,
CreateTime: now,
}
} else if err != nil {
return err
}
configRow.Enabled = configEnabled
configRow.Timezone = normalized.Timezone
configRow.UpdateTime = now
if configRow.CreateTime.IsZero() {
configRow.CreateTime = now
}
if err := tx.Save(&configRow).Error; err != nil {
return err
}
existingRewards, err := loadExistingRewardRows(ctx, tx, configRow.ID, "")
if err != nil {
return err
}
if err := tx.Where("config_id = ?", configRow.ID).Delete(&model.WheelPoolConfig{}).Error; err != nil {
return err
}
if err := tx.Where("config_id = ?", configRow.ID).Delete(&model.WheelRewardConfig{}).Error; err != nil {
return err
}
poolRows := make([]model.WheelPoolConfig, 0, len(normalized.Categories))
rewardRows := make([]model.WheelRewardConfig, 0, 28)
for _, category := range normalized.Categories {
meta, _ := categoryMeta(category.Category)
poolID, idErr := utils.NextID()
if idErr != nil {
return idErr
}
poolRows = append(poolRows, model.WheelPoolConfig{
ID: poolID,
ConfigID: configRow.ID,
SysOrigin: normalized.SysOrigin,
Category: category.Category,
Enabled: category.Enabled,
PriceOneGold: category.PriceOneGold,
PriceTenGold: category.PriceTenGold,
PriceFiftyGold: category.PriceFiftyGold,
Sort: meta.Sort,
CreateTime: now,
UpdateTime: now,
})
for index, item := range category.Rewards {
rewardID, issuedCount, rewardIDErr := resolveRewardRowIdentity(existingRewards, item)
if rewardIDErr != nil {
return rewardIDErr
}
sortValue := item.Sort
if sortValue <= 0 {
sortValue = index + 1
}
rewardRows = append(rewardRows, model.WheelRewardConfig{
ID: rewardID,
ConfigID: configRow.ID,
SysOrigin: normalized.SysOrigin,
Category: category.Category,
RewardType: item.RewardType,
ResourceID: resourceIDPtr(item.ResourceID),
ResourceType: item.ResourceType,
ResourceName: strings.TrimSpace(item.ResourceName),
ResourceURL: strings.TrimSpace(item.ResourceURL),
CoverURL: strings.TrimSpace(item.CoverURL),
DurationDays: item.DurationDays,
DisplayGoldAmount: item.DisplayGoldAmount,
GoldAmount: item.GoldAmount,
Probability: item.Probability,
TotalLimit: item.TotalLimit,
UserLimit: item.UserLimit,
IssuedCount: issuedCount,
Sort: sortValue,
Enabled: item.Enabled,
CreateTime: now,
UpdateTime: now,
})
}
}
if len(poolRows) > 0 {
if err := tx.Create(&poolRows).Error; err != nil {
return err
}
}
if len(rewardRows) > 0 {
return tx.Create(&rewardRows).Error
}
return nil
}); err != nil {
return nil, err
}
return s.GetConfig(ctx, normalized.SysOrigin)
}
// SaveCategoryConfig saves one admin wheel category without touching the other categories.
func (s *Service) SaveCategoryConfig(ctx context.Context, req SaveCategoryConfigRequest) (*ConfigResponse, error) {
normalized, err := normalizeSaveCategoryRequest(req)
if err != nil {
return nil, err
}
now := time.Now()
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var configRow model.WheelConfig
if err := withWriteLock(tx).Where("sys_origin = ?", normalized.SysOrigin).First(&configRow).Error; errors.Is(err, gorm.ErrRecordNotFound) {
nextID, idErr := utils.NextID()
if idErr != nil {
return idErr
}
configRow = model.WheelConfig{
ID: nextID,
SysOrigin: normalized.SysOrigin,
CreateTime: now,
}
} else if err != nil {
return err
}
configRow.Timezone = normalized.Timezone
configRow.UpdateTime = now
if configRow.CreateTime.IsZero() {
configRow.CreateTime = now
}
if err := tx.Save(&configRow).Error; err != nil {
return err
}
category := normalized.Category
meta, _ := categoryMeta(category.Category)
existingRewards, err := loadExistingRewardRows(ctx, tx, configRow.ID, category.Category)
if err != nil {
return err
}
if err := tx.Where("config_id = ? AND category = ?", configRow.ID, category.Category).Delete(&model.WheelPoolConfig{}).Error; err != nil {
return err
}
if err := tx.Where("config_id = ? AND category = ?", configRow.ID, category.Category).Delete(&model.WheelRewardConfig{}).Error; err != nil {
return err
}
poolID, idErr := utils.NextID()
if idErr != nil {
return idErr
}
poolRow := model.WheelPoolConfig{
ID: poolID,
ConfigID: configRow.ID,
SysOrigin: normalized.SysOrigin,
Category: category.Category,
Enabled: category.Enabled,
PriceOneGold: category.PriceOneGold,
PriceTenGold: category.PriceTenGold,
PriceFiftyGold: category.PriceFiftyGold,
Sort: meta.Sort,
CreateTime: now,
UpdateTime: now,
}
if err := tx.Create(&poolRow).Error; err != nil {
return err
}
rewardRows := make([]model.WheelRewardConfig, 0, len(category.Rewards))
for index, item := range category.Rewards {
rewardID, issuedCount, rewardIDErr := resolveRewardRowIdentity(existingRewards, item)
if rewardIDErr != nil {
return rewardIDErr
}
sortValue := item.Sort
if sortValue <= 0 {
sortValue = index + 1
}
rewardRows = append(rewardRows, model.WheelRewardConfig{
ID: rewardID,
ConfigID: configRow.ID,
SysOrigin: normalized.SysOrigin,
Category: category.Category,
RewardType: item.RewardType,
ResourceID: resourceIDPtr(item.ResourceID),
ResourceType: item.ResourceType,
ResourceName: strings.TrimSpace(item.ResourceName),
ResourceURL: strings.TrimSpace(item.ResourceURL),
CoverURL: strings.TrimSpace(item.CoverURL),
DurationDays: item.DurationDays,
DisplayGoldAmount: item.DisplayGoldAmount,
GoldAmount: item.GoldAmount,
Probability: item.Probability,
TotalLimit: item.TotalLimit,
UserLimit: item.UserLimit,
IssuedCount: issuedCount,
Sort: sortValue,
Enabled: item.Enabled,
CreateTime: now,
UpdateTime: now,
})
}
if len(rewardRows) > 0 {
if err := tx.Create(&rewardRows).Error; err != nil {
return err
}
}
var enabledPools int64
if err := tx.Model(&model.WheelPoolConfig{}).
Where("config_id = ? AND enabled = ?", configRow.ID, true).
Count(&enabledPools).Error; err != nil {
return err
}
return tx.Model(&model.WheelConfig{}).
Where("id = ?", configRow.ID).
Updates(map[string]any{
"enabled": enabledPools > 0,
"timezone": normalized.Timezone,
"update_time": now,
}).Error
}); err != nil {
return nil, err
}
return s.GetConfig(ctx, normalized.SysOrigin)
}
func normalizeSaveRequest(req SaveConfigRequest) (SaveConfigRequest, error) {
sysOrigin := normalizeSysOrigin(req.SysOrigin)
if sysOrigin == "" {
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "bad_request", "sysOrigin is required")
}
timezone := normalizeTimezone(req.Timezone)
if _, err := time.LoadLocation(timezone); err != nil {
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "invalid_timezone", err.Error())
}
inputByCategory := make(map[string]CategoryConfigInput, len(req.Categories))
for _, category := range req.Categories {
normalizedCategory := normalizeCategory(category.Category)
if _, ok := categoryMeta(normalizedCategory); !ok {
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "invalid_category", "category must be CLASSIC, LUXURY, or ADVANCED")
}
category.Category = normalizedCategory
inputByCategory[normalizedCategory] = category
}
categories := make([]CategoryConfigInput, 0, len(wheelCategories))
for _, meta := range wheelCategories {
category, exists := inputByCategory[meta.Category]
if !exists {
category = defaultCategoryInput(meta)
}
category.Category = meta.Category
if category.PriceOneGold <= 0 || category.PriceTenGold <= 0 || category.PriceFiftyGold <= 0 {
return SaveConfigRequest{}, NewAppError(http.StatusBadRequest, "invalid_price", fmt.Sprintf("%s draw prices must be greater than 0", meta.Category))
}
normalizedRewards, err := normalizeCategoryRewards(meta, category.Rewards, category.Enabled)
if err != nil {
return SaveConfigRequest{}, err
}
category.Rewards = normalizedRewards
categories = append(categories, category)
}
return SaveConfigRequest{
SysOrigin: sysOrigin,
Enabled: req.Enabled,
Timezone: timezone,
Categories: categories,
}, nil
}
func normalizeSaveCategoryRequest(req SaveCategoryConfigRequest) (SaveCategoryConfigRequest, error) {
sysOrigin := normalizeSysOrigin(req.SysOrigin)
if sysOrigin == "" {
return SaveCategoryConfigRequest{}, NewAppError(http.StatusBadRequest, "bad_request", "sysOrigin is required")
}
timezone := normalizeTimezone(req.Timezone)
if _, err := time.LoadLocation(timezone); err != nil {
return SaveCategoryConfigRequest{}, NewAppError(http.StatusBadRequest, "invalid_timezone", err.Error())
}
category := req.Category
category.Category = normalizeCategory(category.Category)
meta, ok := categoryMeta(category.Category)
if !ok {
return SaveCategoryConfigRequest{}, NewAppError(http.StatusBadRequest, "invalid_category", "category must be CLASSIC, LUXURY, or ADVANCED")
}
if category.PriceOneGold <= 0 || category.PriceTenGold <= 0 || category.PriceFiftyGold <= 0 {
return SaveCategoryConfigRequest{}, NewAppError(http.StatusBadRequest, "invalid_price", fmt.Sprintf("%s draw prices must be greater than 0", meta.Category))
}
rewards, err := normalizeCategoryRewards(meta, category.Rewards, category.Enabled)
if err != nil {
return SaveCategoryConfigRequest{}, err
}
category.Rewards = rewards
return SaveCategoryConfigRequest{
SysOrigin: sysOrigin,
Timezone: timezone,
Category: category,
}, nil
}
func normalizeCategoryRewards(meta CategoryMeta, rewards []RewardConfigInput, requireEnabled bool) ([]RewardConfigInput, error) {
if len(rewards) != meta.RewardCount {
return nil, NewAppError(http.StatusBadRequest, "invalid_reward_count", fmt.Sprintf("%s rewards must contain exactly %d items", meta.Category, meta.RewardCount))
}
normalized := append([]RewardConfigInput(nil), rewards...)
sortRewardInputs(normalized)
enabledProbability := 0
enabledCount := 0
for index := range normalized {
item := &normalized[index]
item.RewardType = normalizeRewardType(item.RewardType)
item.ResourceType = strings.ToUpper(strings.TrimSpace(item.ResourceType))
item.ResourceName = strings.TrimSpace(item.ResourceName)
item.ResourceURL = strings.TrimSpace(item.ResourceURL)
item.CoverURL = strings.TrimSpace(item.CoverURL)
if item.Sort <= 0 {
item.Sort = index + 1
}
switch item.RewardType {
case rewardTypeResource:
if requireEnabled && item.ResourceID.Int64() <= 0 {
return nil, NewAppError(http.StatusBadRequest, "invalid_resource", fmt.Sprintf("%s resource reward requires resourceId", meta.Category))
}
if requireEnabled && item.ResourceType == "" {
return nil, NewAppError(http.StatusBadRequest, "invalid_resource_type", fmt.Sprintf("%s resource reward requires resourceType", meta.Category))
}
if requireEnabled && item.DurationDays <= 0 {
return nil, NewAppError(http.StatusBadRequest, "invalid_duration_days", fmt.Sprintf("%s resource reward requires durationDays greater than 0", meta.Category))
}
if requireEnabled && item.DisplayGoldAmount <= 0 {
return nil, NewAppError(http.StatusBadRequest, "invalid_display_gold_amount", fmt.Sprintf("%s resource reward display gold amount must be greater than 0", meta.Category))
}
if item.ResourceID.Int64() > 0 && item.ResourceName == "" {
item.ResourceName = fmt.Sprintf("资源 %d", item.ResourceID.Int64())
}
item.GoldAmount = 0
case rewardTypeGold:
if requireEnabled && item.GoldAmount <= 0 {
return nil, NewAppError(http.StatusBadRequest, "invalid_gold_amount", fmt.Sprintf("%s gold reward requires goldAmount", meta.Category))
}
item.ResourceID = 0
item.ResourceType = ""
item.ResourceName = "金币"
item.ResourceURL = ""
item.CoverURL = ""
item.DurationDays = 0
item.DisplayGoldAmount = 0
default:
return nil, NewAppError(http.StatusBadRequest, "invalid_reward_type", "rewardType must be RESOURCE or GOLD")
}
if item.Probability < 0 {
return nil, NewAppError(http.StatusBadRequest, "invalid_probability", "probability must be greater than or equal to 0")
}
if item.TotalLimit < 0 {
return nil, NewAppError(http.StatusBadRequest, "invalid_total_limit", "totalLimit must be greater than or equal to 0")
}
if item.UserLimit < 0 {
return nil, NewAppError(http.StatusBadRequest, "invalid_user_limit", "userLimit must be greater than or equal to 0")
}
if item.Enabled {
enabledCount++
if requireEnabled && item.Probability <= 0 {
return nil, NewAppError(http.StatusBadRequest, "invalid_probability", fmt.Sprintf("%s enabled reward probability must be greater than 0", meta.Category))
}
enabledProbability += item.Probability
}
}
if requireEnabled {
if enabledCount != meta.RewardCount {
return nil, NewAppError(http.StatusBadRequest, "invalid_enabled_reward_count", fmt.Sprintf("%s must enable exactly %d rewards", meta.Category, meta.RewardCount))
}
if enabledProbability != probabilityTotal {
return nil, NewAppError(http.StatusBadRequest, "invalid_probability_total", fmt.Sprintf("%s enabled reward probability total must equal %d", meta.Category, probabilityTotal))
}
}
return normalized, nil
}
func loadExistingRewardRows(ctx context.Context, tx *gorm.DB, configID int64, category string) (map[int64]model.WheelRewardConfig, error) {
rows := make([]model.WheelRewardConfig, 0)
query := tx.WithContext(ctx).Where("config_id = ?", configID)
if strings.TrimSpace(category) != "" {
query = query.Where("category = ?", normalizeCategory(category))
}
if err := query.Find(&rows).Error; err != nil {
return nil, err
}
result := make(map[int64]model.WheelRewardConfig, len(rows))
for _, row := range rows {
result[row.ID] = row
}
return result, nil
}
func resolveRewardRowIdentity(existing map[int64]model.WheelRewardConfig, item RewardConfigInput) (int64, int64, error) {
if item.ID > 0 {
if row, ok := existing[item.ID]; ok && sameRewardIdentity(row, item) {
return item.ID, row.IssuedCount, nil
}
}
nextID, err := utils.NextID()
if err != nil {
return 0, 0, err
}
return nextID, 0, nil
}
func sameRewardIdentity(row model.WheelRewardConfig, item RewardConfigInput) bool {
if normalizeRewardType(row.RewardType) != normalizeRewardType(item.RewardType) {
return false
}
if normalizeRewardType(item.RewardType) == rewardTypeGold {
return row.GoldAmount == item.GoldAmount
}
return int64PtrValue(row.ResourceID) == item.ResourceID.Int64() &&
strings.EqualFold(strings.TrimSpace(row.ResourceType), strings.TrimSpace(item.ResourceType)) &&
row.DurationDays == item.DurationDays
}
func (s *Service) loadConfigBundle(ctx context.Context, sysOrigin string, onlyEnabledRewards bool) (*configBundle, error) {
var bundle *configBundle
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
loaded, err := s.loadConfigBundleTx(ctx, tx, sysOrigin, onlyEnabledRewards, false)
if err != nil {
return err
}
bundle = loaded
return nil
}); err != nil {
return nil, err
}
return bundle, nil
}
func (s *Service) loadConfigBundleTx(ctx context.Context, tx *gorm.DB, sysOrigin string, onlyEnabledRewards bool, lockRows bool) (*configBundle, error) {
sysOrigin = normalizeSysOrigin(sysOrigin)
if sysOrigin == "" {
return nil, NewAppError(http.StatusBadRequest, "bad_request", "sysOrigin is required")
}
var configRow model.WheelConfig
configQuery := tx.WithContext(ctx).Where("sys_origin = ?", sysOrigin)
if lockRows {
configQuery = withWriteLock(configQuery)
}
err := configQuery.First(&configRow).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return &configBundle{}, nil
}
if err != nil {
return nil, err
}
var categoryRows []model.WheelPoolConfig
categoryQuery := tx.WithContext(ctx).
Where("config_id = ?", configRow.ID).
Order("sort asc, id asc")
if lockRows {
categoryQuery = withWriteLock(categoryQuery)
}
if err := categoryQuery.Find(&categoryRows).Error; err != nil {
return nil, err
}
rewardQuery := tx.WithContext(ctx).
Where("config_id = ?", configRow.ID).
Order("category asc, sort asc, id asc")
if onlyEnabledRewards {
rewardQuery = rewardQuery.Where("enabled = ?", true)
}
if lockRows {
rewardQuery = withWriteLock(rewardQuery)
}
var rewardRows []model.WheelRewardConfig
if err := rewardQuery.Find(&rewardRows).Error; err != nil {
return nil, err
}
return &configBundle{Config: &configRow, Categories: categoryRows, Rewards: rewardRows}, nil
}
func defaultConfigResponse(sysOrigin string) *ConfigResponse {
return &ConfigResponse{
Configured: false,
SysOrigin: normalizeSysOrigin(sysOrigin),
Enabled: false,
Timezone: defaultTimezone,
ProbabilityTotal: probabilityTotal,
Categories: defaultCategoryPayloads(false),
}
}
func defaultCategoryInput(meta CategoryMeta) CategoryConfigInput {
return CategoryConfigInput{
Category: meta.Category,
Enabled: false,
PriceOneGold: meta.DefaultPriceOneGold,
PriceTenGold: meta.DefaultPriceTenGold,
PriceFiftyGold: meta.DefaultPriceFiftyGold,
Rewards: defaultRewardInputs(meta.RewardCount),
}
}
func defaultRewardInputs(count int) []RewardConfigInput {
rewards := make([]RewardConfigInput, 0, count)
for index := 0; index < count; index++ {
rewards = append(rewards, RewardConfigInput{
Enabled: true,
Sort: index + 1,
RewardType: rewardTypeResource,
Probability: 0,
})
}
return rewards
}
func defaultCategoryPayloads(enabled bool) []CategoryConfigPayload {
result := make([]CategoryConfigPayload, 0, len(wheelCategories))
for _, meta := range wheelCategories {
rewards := make([]RewardConfigPayload, 0, meta.RewardCount)
for index := 0; index < meta.RewardCount; index++ {
rewards = append(rewards, RewardConfigPayload{
Enabled: true,
Sort: index + 1,
RewardType: rewardTypeResource,
Probability: 0,
ProbabilityPercent: "0.00",
})
}
result = append(result, CategoryConfigPayload{
Category: meta.Category,
Label: meta.Label,
Enabled: enabled,
RewardCount: meta.RewardCount,
PriceOneGold: meta.DefaultPriceOneGold,
PriceTenGold: meta.DefaultPriceTenGold,
PriceFiftyGold: meta.DefaultPriceFiftyGold,
ProbabilityTotal: probabilityTotal,
Rewards: rewards,
})
}
return result
}
func (s *Service) buildConfigResponse(bundle *configBundle) (*ConfigResponse, error) {
configRow := bundle.Config
categoryByName := make(map[string]model.WheelPoolConfig, len(bundle.Categories))
for _, row := range bundle.Categories {
categoryByName[normalizeCategory(row.Category)] = row
}
rewardsByCategory := make(map[string][]model.WheelRewardConfig, len(wheelCategories))
for _, row := range bundle.Rewards {
category := normalizeCategory(row.Category)
rewardsByCategory[category] = append(rewardsByCategory[category], row)
}
categories := make([]CategoryConfigPayload, 0, len(wheelCategories))
for _, meta := range wheelCategories {
pool, exists := categoryByName[meta.Category]
if !exists {
pool = model.WheelPoolConfig{
Category: meta.Category,
Enabled: false,
PriceOneGold: meta.DefaultPriceOneGold,
PriceTenGold: meta.DefaultPriceTenGold,
PriceFiftyGold: meta.DefaultPriceFiftyGold,
Sort: meta.Sort,
}
}
rewards := make([]RewardConfigPayload, 0, len(rewardsByCategory[meta.Category]))
for _, row := range rewardsByCategory[meta.Category] {
rewards = append(rewards, rewardPayloadFromConfig(row))
}
for len(rewards) < meta.RewardCount {
rewards = append(rewards, RewardConfigPayload{
Enabled: true,
Sort: len(rewards) + 1,
RewardType: rewardTypeResource,
Probability: 0,
ProbabilityPercent: "0.00",
})
}
if len(rewards) > meta.RewardCount {
rewards = rewards[:meta.RewardCount]
}
categories = append(categories, CategoryConfigPayload{
Category: meta.Category,
Label: meta.Label,
Enabled: pool.Enabled,
RewardCount: meta.RewardCount,
PriceOneGold: pool.PriceOneGold,
PriceTenGold: pool.PriceTenGold,
PriceFiftyGold: pool.PriceFiftyGold,
ProbabilityTotal: probabilityTotal,
Rewards: rewards,
})
}
return &ConfigResponse{
Configured: true,
ID: configRow.ID,
SysOrigin: configRow.SysOrigin,
Enabled: configRow.Enabled,
Timezone: normalizeTimezone(configRow.Timezone),
ProbabilityTotal: probabilityTotal,
Categories: categories,
UpdateTime: formatDateTime(configRow.UpdateTime),
}, nil
}
func rewardPayloadFromConfig(row model.WheelRewardConfig) RewardConfigPayload {
return RewardConfigPayload{
ID: row.ID,
Enabled: row.Enabled,
Sort: row.Sort,
RewardType: row.RewardType,
ResourceID: resourceIDValue(row.ResourceID),
ResourceType: row.ResourceType,
ResourceName: row.ResourceName,
ResourceURL: row.ResourceURL,
CoverURL: row.CoverURL,
DurationDays: row.DurationDays,
DisplayGoldAmount: row.DisplayGoldAmount,
GoldAmount: row.GoldAmount,
Probability: row.Probability,
ProbabilityPercent: probabilityPercent(row.Probability),
TotalLimit: row.TotalLimit,
UserLimit: row.UserLimit,
IssuedCount: row.IssuedCount,
}
}