package luckygift import ( "strings" "time" "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/integration/activityclient" "hyapp-admin-server/internal/middleware" "hyapp-admin-server/internal/modules/shared" "hyapp-admin-server/internal/response" activityv1 "hyapp.local/api/proto/activity/v1" "github.com/gin-gonic/gin" ) type Handler struct { activity activityclient.Client audit shared.OperationLogger } func New(activity activityclient.Client, audit shared.OperationLogger) *Handler { return &Handler{activity: activity, audit: audit} } type configRequest struct { PoolID string `json:"pool_id"` Enabled bool `json:"enabled"` GiftPrice int64 `json:"gift_price"` TargetRTPPPM int64 `json:"target_rtp_ppm"` PoolRatePPM int64 `json:"pool_rate_ppm"` GlobalWindowDraws int64 `json:"global_window_draws"` GiftWindowDraws int64 `json:"gift_window_draws"` NoviceDrawLimit int64 `json:"novice_draw_limit"` IntermediateDrawLimit int64 `json:"intermediate_draw_limit"` HighMultiplier int64 `json:"high_multiplier"` HighWaterPoolMultiple int64 `json:"high_water_pool_multiple"` PlatformPoolWeightPPM int64 `json:"platform_pool_weight_ppm"` RoomPoolWeightPPM int64 `json:"room_pool_weight_ppm"` GiftPoolWeightPPM int64 `json:"gift_pool_weight_ppm"` InitialPlatformPool int64 `json:"initial_platform_pool"` InitialGiftPool int64 `json:"initial_gift_pool"` InitialRoomPool int64 `json:"initial_room_pool"` PlatformReserve int64 `json:"platform_reserve"` GiftReserve int64 `json:"gift_reserve"` RoomReserve int64 `json:"room_reserve"` 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"` RoomAtmosphereRatePPM int64 `json:"room_atmosphere_rate_ppm"` RoomAtmosphereInitial int64 `json:"room_atmosphere_initial"` RoomAtmosphereReserve int64 `json:"room_atmosphere_reserve"` ActivityBudget int64 `json:"activity_budget"` ActivityDailyLimit int64 `json:"activity_daily_limit"` LargeTierEnabled bool `json:"large_tier_enabled"` MultiplierPPMs []int64 `json:"multiplier_ppms"` Tiers []tierDTO `json:"tiers"` } type configDTO struct { configRequest AppCode string `json:"app_code"` GiftID string `json:"gift_id"` RuleVersion int64 `json:"rule_version"` UpdatedByAdminID int64 `json:"updated_by_admin_id"` CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` } type tierDTO struct { Pool string `json:"pool"` TierID string `json:"tier_id"` RewardCoins int64 `json:"reward_coins"` MultiplierPPM int64 `json:"multiplier_ppm"` Weight int64 `json:"weight"` 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"` BaseRewardCoins int64 `json:"base_reward_coins"` RoomAtmosphereRewardCoins int64 `json:"room_atmosphere_reward_coins"` ActivitySubsidyCoins int64 `json:"activity_subsidy_coins"` EffectiveRewardCoins int64 `json:"effective_reward_coins"` BudgetSourcesJSON string `json:"budget_sources_json"` 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"` } 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"` RoomAtmosphereRewardCoins int64 `json:"room_atmosphere_reward_coins"` ActivitySubsidyCoins int64 `json:"activity_subsidy_coins"` ActualRTPPPM int64 `json:"actual_rtp_ppm"` PendingDraws int64 `json:"pending_draws"` GrantedDraws int64 `json:"granted_draws"` FailedDraws int64 `json:"failed_draws"` } func (h *Handler) GetLuckyGiftConfig(c *gin.Context) { resp, err := h.activity.GetLuckyGiftConfig(c.Request.Context(), &activityv1.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")) } resp, err := h.activity.UpsertLuckyGiftConfig(c.Request.Context(), &activityv1.UpsertLuckyGiftConfigRequest{ Meta: h.meta(c), 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_rules", item.PoolID, "success", "") response.OK(c, item) } func (h *Handler) ListLuckyGiftConfigs(c *gin.Context) { resp, err := h.activity.ListLuckyGiftConfigs(c.Request.Context(), &activityv1.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) { options := shared.ListOptions(c) resp, err := h.activity.ListLuckyGiftDraws(c.Request.Context(), &activityv1.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), }) 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) { resp, err := h.activity.GetLuckyGiftDrawSummary(c.Request.Context(), &activityv1.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")), }) if err != nil { response.ServerError(c, "获取幸运礼物抽奖汇总失败") return } response.OK(c, drawSummaryFromProto(resp.GetSummary())) } func (h *Handler) meta(c *gin.Context) *activityv1.RequestMeta { return &activityv1.RequestMeta{ RequestId: middleware.CurrentRequestID(c), Caller: "admin-server", AppCode: appctx.FromContext(c.Request.Context()), SentAtMs: time.Now().UTC().UnixMilli(), } } func configToProto(req configRequest) *activityv1.LuckyGiftConfig { tiers := make([]*activityv1.LuckyGiftTier, 0, len(req.Tiers)) for _, tier := range req.Tiers { tiers = append(tiers, &activityv1.LuckyGiftTier{ Pool: tier.Pool, TierId: tier.TierID, RewardCoins: tier.RewardCoins, MultiplierPpm: tier.MultiplierPPM, Weight: tier.Weight, HighWaterOnly: tier.HighWaterOnly, Enabled: tier.Enabled, }) } return &activityv1.LuckyGiftConfig{ PoolId: strings.TrimSpace(req.PoolID), Enabled: req.Enabled, GiftPrice: req.GiftPrice, TargetRtpPpm: req.TargetRTPPPM, PoolRatePpm: req.PoolRatePPM, GlobalWindowDraws: req.GlobalWindowDraws, GiftWindowDraws: req.GiftWindowDraws, NoviceDrawLimit: req.NoviceDrawLimit, IntermediateDrawLimit: req.IntermediateDrawLimit, HighMultiplier: req.HighMultiplier, HighWaterPoolMultiple: req.HighWaterPoolMultiple, PlatformPoolWeightPpm: req.PlatformPoolWeightPPM, RoomPoolWeightPpm: req.RoomPoolWeightPPM, GiftPoolWeightPpm: req.GiftPoolWeightPPM, InitialPlatformPool: req.InitialPlatformPool, InitialGiftPool: req.InitialGiftPool, InitialRoomPool: req.InitialRoomPool, PlatformReserve: req.PlatformReserve, GiftReserve: req.GiftReserve, RoomReserve: req.RoomReserve, MaxSinglePayout: req.MaxSinglePayout, UserHourlyPayoutCap: req.UserHourlyPayoutCap, UserDailyPayoutCap: req.UserDailyPayoutCap, DeviceDailyPayoutCap: req.DeviceDailyPayoutCap, RoomHourlyPayoutCap: req.RoomHourlyPayoutCap, AnchorDailyPayoutCap: req.AnchorDailyPayoutCap, RoomAtmosphereRatePpm: req.RoomAtmosphereRatePPM, RoomAtmosphereInitial: req.RoomAtmosphereInitial, RoomAtmosphereReserve: req.RoomAtmosphereReserve, ActivityBudget: req.ActivityBudget, ActivityDailyLimit: req.ActivityDailyLimit, LargeTierEnabled: req.LargeTierEnabled, MultiplierPpms: append([]int64(nil), req.MultiplierPPMs...), Tiers: tiers, } } func configFromProto(config *activityv1.LuckyGiftConfig) configDTO { if config == nil { return configDTO{} } tiers := make([]tierDTO, 0, len(config.GetTiers())) for _, tier := range config.GetTiers() { tiers = append(tiers, tierDTO{ Pool: tier.GetPool(), TierID: tier.GetTierId(), RewardCoins: tier.GetRewardCoins(), MultiplierPPM: tier.GetMultiplierPpm(), Weight: tier.GetWeight(), HighWaterOnly: tier.GetHighWaterOnly(), Enabled: tier.GetEnabled(), }) } poolID := strings.TrimSpace(config.GetPoolId()) if poolID == "" { poolID = strings.TrimSpace(config.GetGiftId()) } return configDTO{ AppCode: config.GetAppCode(), GiftID: config.GetGiftId(), RuleVersion: config.GetRuleVersion(), UpdatedByAdminID: config.GetUpdatedByAdminId(), CreatedAtMS: config.GetCreatedAtMs(), UpdatedAtMS: config.GetUpdatedAtMs(), configRequest: configRequest{ PoolID: poolID, Enabled: config.GetEnabled(), GiftPrice: config.GetGiftPrice(), TargetRTPPPM: config.GetTargetRtpPpm(), PoolRatePPM: config.GetPoolRatePpm(), GlobalWindowDraws: config.GetGlobalWindowDraws(), GiftWindowDraws: config.GetGiftWindowDraws(), NoviceDrawLimit: config.GetNoviceDrawLimit(), IntermediateDrawLimit: config.GetIntermediateDrawLimit(), HighMultiplier: config.GetHighMultiplier(), HighWaterPoolMultiple: config.GetHighWaterPoolMultiple(), PlatformPoolWeightPPM: config.GetPlatformPoolWeightPpm(), RoomPoolWeightPPM: config.GetRoomPoolWeightPpm(), GiftPoolWeightPPM: config.GetGiftPoolWeightPpm(), InitialPlatformPool: config.GetInitialPlatformPool(), InitialGiftPool: config.GetInitialGiftPool(), InitialRoomPool: config.GetInitialRoomPool(), PlatformReserve: config.GetPlatformReserve(), GiftReserve: config.GetGiftReserve(), RoomReserve: config.GetRoomReserve(), MaxSinglePayout: config.GetMaxSinglePayout(), UserHourlyPayoutCap: config.GetUserHourlyPayoutCap(), UserDailyPayoutCap: config.GetUserDailyPayoutCap(), DeviceDailyPayoutCap: config.GetDeviceDailyPayoutCap(), RoomHourlyPayoutCap: config.GetRoomHourlyPayoutCap(), AnchorDailyPayoutCap: config.GetAnchorDailyPayoutCap(), RoomAtmosphereRatePPM: config.GetRoomAtmosphereRatePpm(), RoomAtmosphereInitial: config.GetRoomAtmosphereInitial(), RoomAtmosphereReserve: config.GetRoomAtmosphereReserve(), ActivityBudget: config.GetActivityBudget(), ActivityDailyLimit: config.GetActivityDailyLimit(), LargeTierEnabled: config.GetLargeTierEnabled(), MultiplierPPMs: append([]int64(nil), config.GetMultiplierPpms()...), Tiers: tiers, }, } } func drawFromProto(draw *activityv1.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(), BaseRewardCoins: draw.GetBaseRewardCoins(), RoomAtmosphereRewardCoins: draw.GetRoomAtmosphereRewardCoins(), ActivitySubsidyCoins: draw.GetActivitySubsidyCoins(), EffectiveRewardCoins: draw.GetEffectiveRewardCoins(), BudgetSourcesJSON: draw.GetBudgetSourcesJson(), RewardStatus: draw.GetRewardStatus(), RTPWindowIndex: draw.GetRtpWindowIndex(), GiftRTPWindowIndex: draw.GetGiftRtpWindowIndex(), GlobalBaseRTPPPM: draw.GetGlobalBaseRtpPpm(), GiftBaseRTPPPM: draw.GetGiftBaseRtpPpm(), StageFeedback: draw.GetStageFeedback(), HighMultiplier: draw.GetHighMultiplier(), CreatedAtMS: draw.GetCreatedAtMs(), } } func drawSummaryFromProto(summary *activityv1.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(), RoomAtmosphereRewardCoins: summary.GetRoomAtmosphereRewardCoins(), ActivitySubsidyCoins: summary.GetActivitySubsidyCoins(), ActualRTPPPM: summary.GetActualRtpPpm(), PendingDraws: summary.GetPendingDraws(), GrantedDraws: summary.GetGrantedDraws(), FailedDraws: summary.GetFailedDraws(), } } 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 }