package luckygift import ( "fmt" "sort" "strings" "hyapp/pkg/xerr" domain "hyapp/services/activity-service/internal/domain/luckygift" ) const ppmScale int64 = 1_000_000 // DefaultConfig 给后台指定奖池提供规则草稿;真正生效必须由后台显式 enabled。 func DefaultConfig(appCode, poolID string) domain.Config { const giftPrice int64 = 10_000 poolID = normalizePoolID(poolID) return domain.Config{ AppCode: appCode, GiftID: poolID, PoolID: poolID, Enabled: false, GiftPrice: giftPrice, TargetRTPPPM: 950_000, PoolRatePPM: 950_000, GlobalWindowDraws: 100_000, GiftWindowDraws: 100_000, NoviceDrawLimit: 2_000, IntermediateDrawLimit: 20_000, HighMultiplier: 100, HighWaterPoolMultiple: 2, PlatformPoolWeightPPM: 200_000, RoomPoolWeightPPM: 300_000, GiftPoolWeightPPM: 500_000, InitialPlatformPool: 9_500_000, InitialGiftPool: 23_750_000, InitialRoomPool: 5_000_000, PlatformReserve: 1_000_000, GiftReserve: 1_000_000, RoomReserve: 100_000, MaxSinglePayout: giftPrice * 500, UserHourlyPayoutCap: 3_420_000, UserDailyPayoutCap: 61_560_000, DeviceDailyPayoutCap: 102_600_000, RoomHourlyPayoutCap: 68_400_000, AnchorDailyPayoutCap: 1_231_200_000, RoomAtmosphereRatePPM: 10_000, RoomAtmosphereInitial: 100_000, RoomAtmosphereReserve: 10_000, ActivityBudget: 0, ActivityDailyLimit: 0, LargeTierEnabled: true, MultiplierPPMs: defaultMultiplierPPMs(), Tiers: defaultTiers(giftPrice), } } func validateConfig(config domain.Config) error { if normalizePoolID(config.PoolID) == "" { return xerr.New(xerr.InvalidArgument, "lucky gift pool id is required") } config.GiftID = normalizePoolID(config.PoolID) if config.GiftPrice <= 0 || config.TargetRTPPPM <= 0 || config.TargetRTPPPM > ppmScale { return xerr.New(xerr.InvalidArgument, "gift price or target RTP is invalid") } if config.PoolRatePPM < config.TargetRTPPPM || config.PoolRatePPM > ppmScale { return xerr.New(xerr.InvalidArgument, "pool rate must be between target RTP and 100%") } if config.GlobalWindowDraws <= 0 || config.GiftWindowDraws <= 0 { return xerr.New(xerr.InvalidArgument, "RTP window draws must be positive") } if config.NoviceDrawLimit < 0 || config.IntermediateDrawLimit < config.NoviceDrawLimit { return xerr.New(xerr.InvalidArgument, "experience pool draw limits are invalid") } if config.HighMultiplier <= 0 || config.HighWaterPoolMultiple <= 0 { return xerr.New(xerr.InvalidArgument, "high multiplier config is invalid") } if config.PlatformPoolWeightPPM < 0 || config.RoomPoolWeightPPM < 0 || config.GiftPoolWeightPPM < 0 || config.PlatformPoolWeightPPM+config.RoomPoolWeightPPM+config.GiftPoolWeightPPM != ppmScale { return xerr.New(xerr.InvalidArgument, "pool weights must sum to 1000000") } if config.MaxSinglePayout <= 0 || config.UserHourlyPayoutCap <= 0 || config.UserDailyPayoutCap <= 0 || config.DeviceDailyPayoutCap <= 0 || config.RoomHourlyPayoutCap <= 0 || config.AnchorDailyPayoutCap <= 0 { return xerr.New(xerr.InvalidArgument, "risk payout caps must be positive") } if len(config.Tiers) == 0 { return xerr.New(xerr.InvalidArgument, "reward tiers are required") } maxMultiplierPPM := int64(0) for _, tier := range config.Tiers { if tier.Pool == "" || tier.TierID == "" || tier.Weight < 0 || tier.RewardCoins < 0 || tier.MultiplierPPM < 0 { return xerr.New(xerr.InvalidArgument, fmt.Sprintf("invalid reward tier: %s", tier.TierID)) } if tier.Enabled && tier.MultiplierPPM > maxMultiplierPPM { maxMultiplierPPM = tier.MultiplierPPM } } if maxMultiplierPPM < config.TargetRTPPPM { return xerr.New(xerr.InvalidArgument, "max lucky gift multiplier must not be lower than target RTP") } return nil } func normalizeTiers(tiers []domain.Tier, multipliers []int64, giftPrice, highMultiplier int64) ([]domain.Tier, []int64) { multiplierPPMs := normalizeMultiplierPPMs(multipliers, tiers, giftPrice) return buildMultiplierTiers(giftPrice, highMultiplier, multiplierPPMs), multiplierPPMs } func normalizeMultiplierPPMs(input []int64, tiers []domain.Tier, giftPrice int64) []int64 { values := append([]int64(nil), input...) if len(values) == 0 { for _, tier := range tiers { if tier.MultiplierPPM > 0 || tier.RewardCoins == 0 { values = append(values, tier.MultiplierPPM) continue } if giftPrice > 0 && tier.RewardCoins > 0 { values = append(values, tier.RewardCoins*ppmScale/giftPrice) } } } if len(values) == 0 { values = defaultMultiplierPPMs() } seen := map[int64]bool{0: true} out := []int64{0} for _, value := range values { if value < 0 || seen[value] { continue } seen[value] = true out = append(out, value) } sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) return out } func buildMultiplierTiers(cost, highMultiplier int64, multipliers []int64) []domain.Tier { out := make([]domain.Tier, 0, len(multipliers)*3) for _, pool := range []string{domain.PoolNovice, domain.PoolIntermediate, domain.PoolAdvanced} { for _, multiplierPPM := range multipliers { out = append(out, domain.Tier{ Pool: pool, TierID: pool + "_" + multiplierTierID(multiplierPPM), RewardCoins: cost * multiplierPPM / ppmScale, MultiplierPPM: multiplierPPM, Weight: 0, HighWaterOnly: highMultiplier > 0 && multiplierPPM >= highMultiplier*ppmScale, Enabled: true, }) } } return out } func defaultMultiplierPPMs() []int64 { return []int64{ 0, 200_000, 500_000, 1_000_000, 2_000_000, 5_000_000, 10_000_000, 20_000_000, 50_000_000, 100_000_000, 500_000_000, } } func defaultTiers(cost int64) []domain.Tier { return buildMultiplierTiers(cost, 100, defaultMultiplierPPMs()) } func multiplierTierID(multiplierPPM int64) string { if multiplierPPM == 0 { return "none" } whole := multiplierPPM / ppmScale fraction := multiplierPPM % ppmScale if fraction == 0 { return fmt.Sprintf("%dx", whole) } text := strings.TrimRight(strings.TrimRight(fmt.Sprintf("%d_%06dx", whole, fraction), "0"), "_") return text } func normalizePoolID(poolID string) string { poolID = strings.TrimSpace(poolID) if poolID == "" { return domain.DefaultPoolID } return poolID }