724 lines
28 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 luckygift
import (
"context"
"fmt"
"math"
"strings"
"time"
"hyapp-admin-server/internal/integration/luckygiftadmin"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/response"
luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
"github.com/gin-gonic/gin"
"google.golang.org/grpc/codes"
grpcstatus "google.golang.org/grpc/status"
)
type Handler struct {
luckyGift luckygiftadmin.Client
requestTimeout time.Duration
audit shared.OperationLogger
}
func New(luckyGift luckygiftadmin.Client, requestTimeout time.Duration, audit shared.OperationLogger) *Handler {
if requestTimeout <= 0 {
requestTimeout = 3 * time.Second
}
return &Handler{luckyGift: luckyGift, requestTimeout: requestTimeout, audit: audit}
}
type configRequest struct {
AppCode string `json:"app_code"`
PoolID string `json:"pool_id"`
StrategyVersion string `json:"strategy_version"`
Enabled bool `json:"enabled"`
TargetRTPPercent float64 `json:"target_rtp_percent"`
PoolRatePercent float64 `json:"pool_rate_percent"`
ProfitRatePercent float64 `json:"profit_rate_percent"`
AnchorRatePercent float64 `json:"anchor_rate_percent"`
SettlementWindowWager int64 `json:"settlement_window_wager"`
ControlBandPercent float64 `json:"control_band_percent"`
GiftPriceReference int64 `json:"gift_price_reference"`
NoviceMaxEquivalentDraws int64 `json:"novice_max_equivalent_draws"`
NormalMaxEquivalentDraws int64 `json:"normal_max_equivalent_draws"`
EffectiveFromMS int64 `json:"effective_from_ms"`
InitialPoolCoins int64 `json:"initial_pool_coins"`
LossStreakGuarantee int64 `json:"loss_streak_guarantee"`
LowWatermarkCoins int64 `json:"low_watermark_coins"`
LowWaterNonzeroFactorPercent float64 `json:"low_water_nonzero_factor_percent"`
HighWatermarkCoins int64 `json:"high_watermark_coins"`
HighWaterNonzeroFactorPercent float64 `json:"high_water_nonzero_factor_percent"`
RechargeBoostWindowMS int64 `json:"recharge_boost_window_ms"`
RechargeBoostFactorPercent float64 `json:"recharge_boost_factor_percent"`
JackpotMultipliers []float64 `json:"jackpot_multipliers"`
JackpotGlobalRTPMaxPercent float64 `json:"jackpot_global_rtp_max_percent"`
JackpotUserDayRTPMaxPercent float64 `json:"jackpot_user_day_rtp_max_percent"`
JackpotUser72hRTPMaxPercent float64 `json:"jackpot_user_72h_rtp_max_percent"`
JackpotSpendThresholdCoins int64 `json:"jackpot_spend_threshold_coins"`
MaxJackpotHitsPerUserDay int64 `json:"max_jackpot_hits_per_user_day"`
MaxSinglePayout int64 `json:"max_single_payout"`
UserHourlyPayoutCap int64 `json:"user_hourly_payout_cap"`
UserDailyPayoutCap int64 `json:"user_daily_payout_cap"`
DeviceDailyPayoutCap int64 `json:"device_daily_payout_cap"`
RoomHourlyPayoutCap int64 `json:"room_hourly_payout_cap"`
AnchorDailyPayoutCap int64 `json:"anchor_daily_payout_cap"`
Stages []stageDTO `json:"stages"`
}
type configDTO struct {
configRequest
RuleVersion int64 `json:"rule_version"`
CreatedByAdminID int64 `json:"created_by_admin_id"`
CreatedAtMS int64 `json:"created_at_ms"`
}
type stageDTO struct {
Stage string `json:"stage"`
MinRecharge7DCoins int64 `json:"min_recharge_7d_coins"`
MinRecharge30DCoins int64 `json:"min_recharge_30d_coins"`
Tiers []tierDTO `json:"tiers"`
}
type tierDTO struct {
TierID string `json:"tier_id"`
Multiplier float64 `json:"multiplier"`
ProbabilityPercent float64 `json:"probability_percent"`
HighWaterOnly bool `json:"high_water_only"`
Enabled bool `json:"enabled"`
}
type drawDTO struct {
DrawID string `json:"draw_id"`
CommandID string `json:"command_id"`
PoolID string `json:"pool_id"`
GiftID string `json:"gift_id"`
RuleVersion int64 `json:"rule_version"`
ExperiencePool string `json:"experience_pool"`
SelectedTierID string `json:"selected_tier_id"`
MultiplierPPM int64 `json:"multiplier_ppm"`
BaseRewardCoins int64 `json:"base_reward_coins"`
EffectiveRewardCoins int64 `json:"effective_reward_coins"`
RewardStatus string `json:"reward_status"`
RTPWindowIndex int64 `json:"rtp_window_index"`
GiftRTPWindowIndex int64 `json:"gift_rtp_window_index"`
GlobalBaseRTPPPM int64 `json:"global_base_rtp_ppm"`
GiftBaseRTPPPM int64 `json:"gift_base_rtp_ppm"`
StageFeedback bool `json:"stage_feedback"`
HighMultiplier bool `json:"high_multiplier"`
CreatedAtMS int64 `json:"created_at_ms"`
AppCode string `json:"app_code"`
UserID int64 `json:"user_id"`
ExternalUserID string `json:"external_user_id"`
}
type drawSummaryDTO struct {
PoolID string `json:"pool_id"`
TotalDraws int64 `json:"total_draws"`
UniqueUsers int64 `json:"unique_users"`
UniqueRooms int64 `json:"unique_rooms"`
TotalSpentCoins int64 `json:"total_spent_coins"`
TotalRewardCoins int64 `json:"total_reward_coins"`
BaseRewardCoins int64 `json:"base_reward_coins"`
ActualRTPPPM int64 `json:"actual_rtp_ppm"`
PendingDraws int64 `json:"pending_draws"`
GrantedDraws int64 `json:"granted_draws"`
FailedDraws int64 `json:"failed_draws"`
}
type poolBalanceDTO struct {
AppCode string `json:"app_code"`
PoolID string `json:"pool_id"`
StrategyVersion string `json:"strategy_version"`
Balance int64 `json:"balance"`
ReserveFloor int64 `json:"reserve_floor"`
AvailableBalance int64 `json:"available_balance"`
TotalIn int64 `json:"total_in"`
TotalOut int64 `json:"total_out"`
ManualCreditTotal int64 `json:"manual_credit_total"`
ManualDebitTotal int64 `json:"manual_debit_total"`
Materialized bool `json:"materialized"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
type poolAdjustmentRequest struct {
AdjustmentID string `json:"adjustment_id"`
AmountCoins int64 `json:"amount_coins"`
Reason string `json:"reason"`
}
type poolAdjustmentDTO struct {
AdjustmentID string `json:"adjustment_id"`
AppCode string `json:"app_code"`
PoolID string `json:"pool_id"`
StrategyVersion string `json:"strategy_version"`
Direction string `json:"direction"`
AmountCoins int64 `json:"amount_coins"`
Reason string `json:"reason"`
OperatorAdminID int64 `json:"operator_admin_id"`
BalanceBefore int64 `json:"balance_before"`
BalanceAfter int64 `json:"balance_after"`
CreatedAtMS int64 `json:"created_at_ms"`
}
type poolAdjustmentResponseDTO struct {
Adjustment poolAdjustmentDTO `json:"adjustment"`
Pool poolBalanceDTO `json:"pool"`
IdempotentReplay bool `json:"idempotent_replay"`
}
func (h *Handler) GetLuckyGiftConfig(c *gin.Context) {
if strings.TrimSpace(c.Query("app_code")) == "" {
response.BadRequest(c, "app_code is required")
return
}
ctx, cancel := h.luckyGiftContext(c)
defer cancel()
resp, err := h.luckyGift.GetLuckyGiftConfig(ctx, &luckygiftv1.GetLuckyGiftConfigRequest{
Meta: h.meta(c),
PoolId: strings.TrimSpace(c.Query("pool_id")),
})
if err != nil {
response.ServerError(c, "获取幸运礼物配置失败")
return
}
response.OK(c, configFromProto(resp.GetConfig()))
}
func (h *Handler) UpsertLuckyGiftConfig(c *gin.Context) {
var req configRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "幸运礼物配置参数不正确")
return
}
if req.PoolID == "" {
req.PoolID = strings.TrimSpace(c.Query("pool_id"))
}
if req.AppCode == "" {
req.AppCode = strings.TrimSpace(c.Query("app_code"))
}
if strings.TrimSpace(req.AppCode) == "" {
response.BadRequest(c, "app_code is required")
return
}
ctx, cancel := h.luckyGiftContext(c)
defer cancel()
resp, err := h.luckyGift.UpsertLuckyGiftConfig(ctx, &luckygiftv1.UpsertLuckyGiftConfigRequest{
Meta: h.metaForApp(c, req.AppCode),
Config: configToProto(req),
OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
item := configFromProto(resp.GetConfig())
shared.OperationLogWithResourceID(c, h.audit, "upsert-lucky-gift-config", "lucky_gift_rule_versions", item.PoolID, "success", "")
response.OK(c, item)
}
func (h *Handler) ListLuckyGiftConfigs(c *gin.Context) {
if strings.TrimSpace(c.Query("app_code")) == "" {
response.BadRequest(c, "app_code is required")
return
}
ctx, cancel := h.luckyGiftContext(c)
defer cancel()
resp, err := h.luckyGift.ListLuckyGiftConfigs(ctx, &luckygiftv1.ListLuckyGiftConfigsRequest{
Meta: h.meta(c),
})
if err != nil {
response.ServerError(c, "获取幸运礼物奖池列表失败")
return
}
items := make([]configDTO, 0, len(resp.GetConfigs()))
for _, config := range resp.GetConfigs() {
items = append(items, configFromProto(config))
}
response.OK(c, items)
}
func (h *Handler) ListLuckyGiftDraws(c *gin.Context) {
if strings.TrimSpace(c.Query("app_code")) == "" {
response.BadRequest(c, "app_code is required")
return
}
options := shared.ListOptions(c)
ctx, cancel := h.luckyGiftContext(c)
defer cancel()
resp, err := h.luckyGift.ListLuckyGiftDraws(ctx, &luckygiftv1.ListLuckyGiftDrawsRequest{
Meta: h.meta(c),
PoolId: strings.TrimSpace(c.Query("pool_id")),
GiftId: strings.TrimSpace(c.Query("gift_id")),
UserId: parseOptionalInt64(c.Query("user_id")),
RoomId: strings.TrimSpace(c.Query("room_id")),
Status: strings.TrimSpace(options.Status),
Page: int32(options.Page),
PageSize: int32(options.PageSize),
ExternalUserId: strings.TrimSpace(c.Query("external_user_id")),
ExternalOnly: parseOptionalBool(c.Query("external_only")),
})
if err != nil {
response.ServerError(c, "获取幸运礼物抽奖记录失败")
return
}
items := make([]drawDTO, 0, len(resp.GetDraws()))
for _, draw := range resp.GetDraws() {
items = append(items, drawFromProto(draw))
}
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
}
func (h *Handler) GetLuckyGiftDrawSummary(c *gin.Context) {
if strings.TrimSpace(c.Query("app_code")) == "" {
response.BadRequest(c, "app_code is required")
return
}
ctx, cancel := h.luckyGiftContext(c)
defer cancel()
resp, err := h.luckyGift.GetLuckyGiftDrawSummary(ctx, &luckygiftv1.GetLuckyGiftDrawSummaryRequest{
Meta: h.meta(c),
PoolId: strings.TrimSpace(c.Query("pool_id")),
GiftId: strings.TrimSpace(c.Query("gift_id")),
UserId: parseOptionalInt64(c.Query("user_id")),
RoomId: strings.TrimSpace(c.Query("room_id")),
Status: strings.TrimSpace(c.Query("status")),
ExternalUserId: strings.TrimSpace(c.Query("external_user_id")),
ExternalOnly: parseOptionalBool(c.Query("external_only")),
})
if err != nil {
response.ServerError(c, "获取幸运礼物抽奖汇总失败")
return
}
response.OK(c, drawSummaryFromProto(resp.GetSummary()))
}
func (h *Handler) ListLuckyGiftPoolBalances(c *gin.Context) {
if strings.TrimSpace(c.Query("app_code")) == "" {
response.BadRequest(c, "app_code is required")
return
}
ctx, cancel := h.luckyGiftContext(c)
defer cancel()
resp, err := h.luckyGift.ListLuckyGiftPoolBalances(ctx, &luckygiftv1.ListLuckyGiftPoolBalancesRequest{
Meta: h.meta(c),
PoolId: strings.TrimSpace(c.Query("pool_id")),
StrategyVersion: strings.ToLower(strings.TrimSpace(c.Query("strategy_version"))),
})
if err != nil {
response.ServerError(c, "获取幸运礼物奖池余额失败")
return
}
items := make([]poolBalanceDTO, 0, len(resp.GetPools()))
for _, pool := range resp.GetPools() {
items = append(items, poolBalanceFromProto(pool))
}
response.OK(c, gin.H{"items": items, "total": len(items)})
}
func (h *Handler) CreditLuckyGiftPoolBalance(c *gin.Context) {
h.adjustLuckyGiftPoolBalance(c, "in")
}
func (h *Handler) DebitLuckyGiftPoolBalance(c *gin.Context) {
h.adjustLuckyGiftPoolBalance(c, "out")
}
func (h *Handler) adjustLuckyGiftPoolBalance(c *gin.Context, direction string) {
appCode := strings.ToLower(strings.TrimSpace(c.Query("app_code")))
poolID := strings.TrimSpace(c.Query("pool_id"))
strategyVersion := strings.ToLower(strings.TrimSpace(c.Query("strategy_version")))
if appCode == "" || poolID == "" || (strategyVersion != "fixed_v2" && strategyVersion != "dynamic_v3") {
response.BadRequest(c, "app_code, pool_id and strategy_version are required")
return
}
var req poolAdjustmentRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "幸运礼物奖池调整参数不正确")
return
}
req.AdjustmentID = strings.TrimSpace(req.AdjustmentID)
req.Reason = strings.TrimSpace(req.Reason)
if req.AdjustmentID == "" || req.AmountCoins <= 0 || req.Reason == "" {
response.BadRequest(c, "adjustment_id, positive amount_coins and reason are required")
return
}
ctx, cancel := h.luckyGiftContext(c)
defer cancel()
resp, err := h.luckyGift.AdjustLuckyGiftPoolBalance(ctx, &luckygiftv1.AdjustLuckyGiftPoolBalanceRequest{
Meta: h.metaForApp(c, appCode),
PoolId: poolID,
StrategyVersion: strategyVersion,
AdjustmentId: req.AdjustmentID,
Direction: direction,
AmountCoins: req.AmountCoins,
Reason: req.Reason,
OperatorAdminId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
h.writePoolAdjustmentError(c, err)
return
}
item := poolAdjustmentResponseDTO{
Adjustment: poolAdjustmentFromProto(resp.GetAdjustment()),
Pool: poolBalanceFromProto(resp.GetPool()),
IdempotentReplay: resp.GetIdempotentReplay(),
}
shared.OperationLogWithResourceID(
c, h.audit, "adjust-lucky-gift-pool-"+direction, "lucky_pool_adjustments", item.Adjustment.AdjustmentID, "success",
fmt.Sprintf("app_code=%s pool_id=%s strategy_version=%s direction=%s amount_coins=%d reason=%s", appCode, poolID, strategyVersion, direction, req.AmountCoins, req.Reason),
)
response.OK(c, item)
}
func (h *Handler) writePoolAdjustmentError(c *gin.Context, err error) {
status := grpcstatus.Convert(err)
switch status.Code() {
case codes.InvalidArgument:
response.BadRequest(c, status.Message())
case codes.NotFound:
response.NotFound(c, status.Message())
case codes.AlreadyExists, codes.FailedPrecondition, codes.Aborted:
response.Conflict(c, status.Message())
default:
response.ServerError(c, "调整幸运礼物奖池余额失败")
}
}
func (h *Handler) meta(c *gin.Context) *luckygiftv1.RequestMeta {
return h.metaForApp(c, "")
}
func (h *Handler) metaForApp(c *gin.Context, explicitAppCode string) *luckygiftv1.RequestMeta {
appCode := strings.ToLower(strings.TrimSpace(explicitAppCode))
if appCode == "" {
appCode = strings.ToLower(strings.TrimSpace(c.Query("app_code")))
}
return &luckygiftv1.RequestMeta{
RequestId: middleware.CurrentRequestID(c),
Caller: "admin-server",
// ops-center 是 admin 的衍生平台,筛选 app_code 通过 query/body 传入;
// 这里把它放进 Admin RPC meta让 lucky-gift-service 继续作为 app 维度过滤和配置发布的唯一 owner。
AppCode: appCode,
SentAtMs: time.Now().UTC().UnixMilli(),
}
}
func (h *Handler) luckyGiftContext(c *gin.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(c.Request.Context(), h.requestTimeout)
}
func configToProto(req configRequest) *luckygiftv1.LuckyGiftRuleConfig {
strategyVersion := strings.ToLower(strings.TrimSpace(req.StrategyVersion))
initialPoolCoins := req.InitialPoolCoins
if strategyVersion == "dynamic_v3" {
// V3 启动资金已经迁移为独立人工 credit旧页面残留的 1,000,000 不能在再次保存时回写不可变配置。
initialPoolCoins = 0
}
stages := make([]*luckygiftv1.LuckyGiftRuleStage, 0, len(req.Stages))
for _, stage := range req.Stages {
stageName := strings.TrimSpace(stage.Stage)
tierIDCounts := make(map[string]int, len(stage.Tiers))
tiers := make([]*luckygiftv1.LuckyGiftRuleTier, 0, len(stage.Tiers))
for _, tier := range stage.Tiers {
multiplierPPM := multiplierToPPM(tier.Multiplier)
tiers = append(tiers, &luckygiftv1.LuckyGiftRuleTier{
Stage: stageName,
// 奖档 ID 是 lucky-gift-service 持久化主键的一部分不能让运营手工维护admin-server 按阶段和倍率生成稳定 ID
// 同一阶段同倍率出现多行时追加序号,保证进入 lucky-gift-service 前已经满足唯一性。
TierId: generatedLuckyGiftTierID(stageName, multiplierPPM, tierIDCounts),
MultiplierPpm: multiplierPPM,
BaseWeightPpm: percentToPPM(tier.ProbabilityPercent),
HighWaterOnly: tier.HighWaterOnly,
Enabled: tier.Enabled,
})
}
stages = append(stages, &luckygiftv1.LuckyGiftRuleStage{
Stage: stageName,
MinRecharge_7DCoins: stage.MinRecharge7DCoins,
MinRecharge_30DCoins: stage.MinRecharge30DCoins,
Tiers: tiers,
})
}
return &luckygiftv1.LuckyGiftRuleConfig{
AppCode: strings.ToLower(strings.TrimSpace(req.AppCode)),
PoolId: strings.TrimSpace(req.PoolID),
StrategyVersion: strategyVersion,
Enabled: req.Enabled,
TargetRtpPpm: percentToPPM(req.TargetRTPPercent),
PoolRatePpm: percentToPPM(req.PoolRatePercent),
ProfitRatePpm: percentToPPM(req.ProfitRatePercent),
AnchorRatePpm: percentToPPM(req.AnchorRatePercent),
SettlementWindowWager: req.SettlementWindowWager,
ControlBandPpm: percentToPPM(req.ControlBandPercent),
GiftPriceReference: req.GiftPriceReference,
NoviceMaxEquivalentDraws: req.NoviceMaxEquivalentDraws,
NormalMaxEquivalentDraws: req.NormalMaxEquivalentDraws,
EffectiveFromMs: req.EffectiveFromMS,
InitialPoolCoins: initialPoolCoins,
LossStreakGuarantee: req.LossStreakGuarantee,
LowWatermarkCoins: req.LowWatermarkCoins,
LowWaterNonzeroFactorPpm: percentToPPM(req.LowWaterNonzeroFactorPercent),
HighWatermarkCoins: req.HighWatermarkCoins,
HighWaterNonzeroFactorPpm: percentToPPM(req.HighWaterNonzeroFactorPercent),
RechargeBoostWindowMs: req.RechargeBoostWindowMS,
RechargeBoostFactorPpm: percentToPPM(req.RechargeBoostFactorPercent),
JackpotMultiplierPpms: multipliersToPPM(req.JackpotMultipliers),
JackpotGlobalRtpMaxPpm: percentToPPM(req.JackpotGlobalRTPMaxPercent),
JackpotUserDayRtpMaxPpm: percentToPPM(req.JackpotUserDayRTPMaxPercent),
JackpotUser_72HRtpMaxPpm: percentToPPM(req.JackpotUser72hRTPMaxPercent),
JackpotSpendThresholdCoins: req.JackpotSpendThresholdCoins,
MaxJackpotHitsPerUserDay: req.MaxJackpotHitsPerUserDay,
MaxSinglePayout: req.MaxSinglePayout,
UserHourlyPayoutCap: req.UserHourlyPayoutCap,
UserDailyPayoutCap: req.UserDailyPayoutCap,
DeviceDailyPayoutCap: req.DeviceDailyPayoutCap,
RoomHourlyPayoutCap: req.RoomHourlyPayoutCap,
AnchorDailyPayoutCap: req.AnchorDailyPayoutCap,
Stages: stages,
}
}
func configFromProto(config *luckygiftv1.LuckyGiftRuleConfig) configDTO {
if config == nil {
return configDTO{}
}
stages := make([]stageDTO, 0, len(config.GetStages()))
for _, stage := range config.GetStages() {
tiers := make([]tierDTO, 0, len(stage.GetTiers()))
for _, tier := range stage.GetTiers() {
tiers = append(tiers, tierDTO{
TierID: tier.GetTierId(),
Multiplier: ppmToMultiplier(tier.GetMultiplierPpm()),
ProbabilityPercent: ppmToPercent(tier.GetBaseWeightPpm()),
HighWaterOnly: tier.GetHighWaterOnly(),
Enabled: tier.GetEnabled(),
})
}
stages = append(stages, stageDTO{
Stage: stage.GetStage(),
MinRecharge7DCoins: stage.GetMinRecharge_7DCoins(),
MinRecharge30DCoins: stage.GetMinRecharge_30DCoins(),
Tiers: tiers,
})
}
poolID := strings.TrimSpace(config.GetPoolId())
strategyVersion := strings.ToLower(strings.TrimSpace(config.GetStrategyVersion()))
initialPoolCoins := config.GetInitialPoolCoins()
if strategyVersion == "dynamic_v3" {
// 兼容读取迁移前 V3 快照时也展示 0避免运营误以为规则字段仍会自动形成账本资金。
initialPoolCoins = 0
}
return configDTO{
RuleVersion: config.GetRuleVersion(),
CreatedByAdminID: config.GetCreatedByAdminId(),
CreatedAtMS: config.GetCreatedAtMs(),
configRequest: configRequest{
AppCode: config.GetAppCode(),
PoolID: poolID,
StrategyVersion: strategyVersion,
Enabled: config.GetEnabled(),
TargetRTPPercent: ppmToPercent(config.GetTargetRtpPpm()),
PoolRatePercent: ppmToPercent(config.GetPoolRatePpm()),
ProfitRatePercent: ppmToPercent(config.GetProfitRatePpm()),
AnchorRatePercent: ppmToPercent(config.GetAnchorRatePpm()),
SettlementWindowWager: config.GetSettlementWindowWager(),
ControlBandPercent: ppmToPercent(config.GetControlBandPpm()),
GiftPriceReference: config.GetGiftPriceReference(),
NoviceMaxEquivalentDraws: config.GetNoviceMaxEquivalentDraws(),
NormalMaxEquivalentDraws: config.GetNormalMaxEquivalentDraws(),
EffectiveFromMS: config.GetEffectiveFromMs(),
InitialPoolCoins: initialPoolCoins,
LossStreakGuarantee: config.GetLossStreakGuarantee(),
LowWatermarkCoins: config.GetLowWatermarkCoins(),
LowWaterNonzeroFactorPercent: ppmToPercent(config.GetLowWaterNonzeroFactorPpm()),
HighWatermarkCoins: config.GetHighWatermarkCoins(),
HighWaterNonzeroFactorPercent: ppmToPercent(config.GetHighWaterNonzeroFactorPpm()),
RechargeBoostWindowMS: config.GetRechargeBoostWindowMs(),
RechargeBoostFactorPercent: ppmToPercent(config.GetRechargeBoostFactorPpm()),
JackpotMultipliers: ppmsToMultipliers(config.GetJackpotMultiplierPpms()),
JackpotGlobalRTPMaxPercent: ppmToPercent(config.GetJackpotGlobalRtpMaxPpm()),
JackpotUserDayRTPMaxPercent: ppmToPercent(config.GetJackpotUserDayRtpMaxPpm()),
JackpotUser72hRTPMaxPercent: ppmToPercent(config.GetJackpotUser_72HRtpMaxPpm()),
JackpotSpendThresholdCoins: config.GetJackpotSpendThresholdCoins(),
MaxJackpotHitsPerUserDay: config.GetMaxJackpotHitsPerUserDay(),
MaxSinglePayout: config.GetMaxSinglePayout(),
UserHourlyPayoutCap: config.GetUserHourlyPayoutCap(),
UserDailyPayoutCap: config.GetUserDailyPayoutCap(),
DeviceDailyPayoutCap: config.GetDeviceDailyPayoutCap(),
RoomHourlyPayoutCap: config.GetRoomHourlyPayoutCap(),
AnchorDailyPayoutCap: config.GetAnchorDailyPayoutCap(),
Stages: stages,
},
}
}
func generatedLuckyGiftTierID(stage string, multiplierPPM int64, counts map[string]int) string {
baseID := luckyGiftTierIDBase(stage, multiplierPPM)
counts[baseID]++
if counts[baseID] == 1 {
return baseID
}
return fmt.Sprintf("%s_%d", baseID, counts[baseID])
}
func luckyGiftTierIDBase(stage string, multiplierPPM int64) string {
stage = strings.TrimSpace(stage)
if multiplierPPM <= 0 {
return stage + "_none"
}
whole := multiplierPPM / 1_000_000
fraction := multiplierPPM % 1_000_000
if fraction == 0 {
return fmt.Sprintf("%s_%dx", stage, whole)
}
fractionText := strings.TrimRight(fmt.Sprintf("%06d", fraction), "0")
return fmt.Sprintf("%s_%d_%sx", stage, whole, fractionText)
}
func percentToPPM(value float64) int64 {
return int64(math.Round(value * 10_000))
}
func ppmToPercent(value int64) float64 {
return float64(value) / 10_000
}
func multiplierToPPM(value float64) int64 {
return int64(math.Round(value * 1_000_000))
}
func multipliersToPPM(values []float64) []int64 {
out := make([]int64, 0, len(values))
for _, value := range values {
out = append(out, multiplierToPPM(value))
}
return out
}
func ppmToMultiplier(value int64) float64 {
return float64(value) / 1_000_000
}
func ppmsToMultipliers(values []int64) []float64 {
out := make([]float64, 0, len(values))
for _, value := range values {
out = append(out, ppmToMultiplier(value))
}
return out
}
func drawFromProto(draw *luckygiftv1.LuckyGiftDrawResult) drawDTO {
if draw == nil {
return drawDTO{}
}
return drawDTO{
DrawID: draw.GetDrawId(),
CommandID: draw.GetCommandId(),
PoolID: draw.GetPoolId(),
GiftID: draw.GetGiftId(),
RuleVersion: draw.GetRuleVersion(),
ExperiencePool: draw.GetExperiencePool(),
SelectedTierID: draw.GetSelectedTierId(),
MultiplierPPM: draw.GetMultiplierPpm(),
BaseRewardCoins: draw.GetBaseRewardCoins(),
EffectiveRewardCoins: draw.GetEffectiveRewardCoins(),
RewardStatus: draw.GetRewardStatus(),
RTPWindowIndex: draw.GetRtpWindowIndex(),
GiftRTPWindowIndex: draw.GetGiftRtpWindowIndex(),
GlobalBaseRTPPPM: draw.GetGlobalBaseRtpPpm(),
GiftBaseRTPPPM: draw.GetGiftBaseRtpPpm(),
StageFeedback: draw.GetStageFeedback(),
HighMultiplier: draw.GetHighMultiplier(),
CreatedAtMS: draw.GetCreatedAtMs(),
AppCode: draw.GetAppCode(),
UserID: draw.GetUserId(),
ExternalUserID: draw.GetExternalUserId(),
}
}
func drawSummaryFromProto(summary *luckygiftv1.LuckyGiftDrawSummary) drawSummaryDTO {
if summary == nil {
return drawSummaryDTO{}
}
return drawSummaryDTO{
PoolID: summary.GetPoolId(),
TotalDraws: summary.GetTotalDraws(),
UniqueUsers: summary.GetUniqueUsers(),
UniqueRooms: summary.GetUniqueRooms(),
TotalSpentCoins: summary.GetTotalSpentCoins(),
TotalRewardCoins: summary.GetTotalRewardCoins(),
BaseRewardCoins: summary.GetBaseRewardCoins(),
ActualRTPPPM: summary.GetActualRtpPpm(),
PendingDraws: summary.GetPendingDraws(),
GrantedDraws: summary.GetGrantedDraws(),
FailedDraws: summary.GetFailedDraws(),
}
}
func poolBalanceFromProto(pool *luckygiftv1.LuckyGiftPoolBalance) poolBalanceDTO {
if pool == nil {
return poolBalanceDTO{}
}
return poolBalanceDTO{
AppCode: pool.GetAppCode(),
PoolID: pool.GetPoolId(),
StrategyVersion: pool.GetStrategyVersion(),
Balance: pool.GetBalance(),
ReserveFloor: pool.GetReserveFloor(),
AvailableBalance: pool.GetAvailableBalance(),
TotalIn: pool.GetTotalIn(),
TotalOut: pool.GetTotalOut(),
ManualCreditTotal: pool.GetManualCreditTotal(),
ManualDebitTotal: pool.GetManualDebitTotal(),
Materialized: pool.GetMaterialized(),
UpdatedAtMS: pool.GetUpdatedAtMs(),
}
}
func poolAdjustmentFromProto(adjustment *luckygiftv1.LuckyGiftPoolAdjustment) poolAdjustmentDTO {
if adjustment == nil {
return poolAdjustmentDTO{}
}
return poolAdjustmentDTO{
AdjustmentID: adjustment.GetAdjustmentId(),
AppCode: adjustment.GetAppCode(),
PoolID: adjustment.GetPoolId(),
StrategyVersion: adjustment.GetStrategyVersion(),
Direction: adjustment.GetDirection(),
AmountCoins: adjustment.GetAmountCoins(),
Reason: adjustment.GetReason(),
OperatorAdminID: adjustment.GetOperatorAdminId(),
BalanceBefore: adjustment.GetBalanceBefore(),
BalanceAfter: adjustment.GetBalanceAfter(),
CreatedAtMS: adjustment.GetCreatedAtMs(),
}
}
func parseOptionalInt64(value string) int64 {
value = strings.TrimSpace(value)
if value == "" {
return 0
}
var result int64
for _, char := range value {
if char < '0' || char > '9' {
return 0
}
result = result*10 + int64(char-'0')
}
return result
}
func parseOptionalBool(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "1", "true", "yes", "y":
return true
default:
return false
}
}