357 lines
13 KiB
Go

package luckygift
import (
"math"
"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"`
TargetRTPPercent float64 `json:"target_rtp_percent"`
PoolRatePercent float64 `json:"pool_rate_percent"`
SettlementWindowWager int64 `json:"settlement_window_wager"`
ControlBandPercent float64 `json:"control_band_percent"`
GiftPriceReference int64 `json:"gift_price_reference"`
EffectiveFromMS int64 `json:"effective_from_ms"`
Stages []stageDTO `json:"stages"`
}
type configDTO struct {
configRequest
AppCode string `json:"app_code"`
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"`
Tiers []tierDTO `json:"tiers"`
}
type tierDTO struct {
TierID string `json:"tier_id"`
Multiplier float64 `json:"multiplier"`
ProbabilityPercent float64 `json:"probability_percent"`
RewardSource string `json:"reward_source"`
HighWaterOnly bool `json:"high_water_only"`
BroadcastLevel string `json:"broadcast_level"`
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"`
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.LuckyGiftRuleConfig {
stages := make([]*activityv1.LuckyGiftRuleStage, 0, len(req.Stages))
for _, stage := range req.Stages {
tiers := make([]*activityv1.LuckyGiftRuleTier, 0, len(stage.Tiers))
for _, tier := range stage.Tiers {
tiers = append(tiers, &activityv1.LuckyGiftRuleTier{
Stage: strings.TrimSpace(stage.Stage),
TierId: strings.TrimSpace(tier.TierID),
MultiplierPpm: multiplierToPPM(tier.Multiplier),
BaseWeightPpm: percentToPPM(tier.ProbabilityPercent),
RewardSource: strings.TrimSpace(tier.RewardSource),
HighWaterOnly: tier.HighWaterOnly,
BroadcastLevel: strings.TrimSpace(tier.BroadcastLevel),
Enabled: tier.Enabled,
})
}
stages = append(stages, &activityv1.LuckyGiftRuleStage{
Stage: strings.TrimSpace(stage.Stage),
Tiers: tiers,
})
}
return &activityv1.LuckyGiftRuleConfig{
PoolId: strings.TrimSpace(req.PoolID),
Enabled: req.Enabled,
TargetRtpPpm: percentToPPM(req.TargetRTPPercent),
PoolRatePpm: percentToPPM(req.PoolRatePercent),
SettlementWindowWager: req.SettlementWindowWager,
ControlBandPpm: percentToPPM(req.ControlBandPercent),
GiftPriceReference: req.GiftPriceReference,
EffectiveFromMs: req.EffectiveFromMS,
Stages: stages,
}
}
func configFromProto(config *activityv1.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()),
RewardSource: tier.GetRewardSource(),
HighWaterOnly: tier.GetHighWaterOnly(),
BroadcastLevel: tier.GetBroadcastLevel(),
Enabled: tier.GetEnabled(),
})
}
stages = append(stages, stageDTO{
Stage: stage.GetStage(),
Tiers: tiers,
})
}
poolID := strings.TrimSpace(config.GetPoolId())
return configDTO{
AppCode: config.GetAppCode(),
RuleVersion: config.GetRuleVersion(),
CreatedByAdminID: config.GetCreatedByAdminId(),
CreatedAtMS: config.GetCreatedAtMs(),
configRequest: configRequest{
PoolID: poolID,
Enabled: config.GetEnabled(),
TargetRTPPercent: ppmToPercent(config.GetTargetRtpPpm()),
PoolRatePercent: ppmToPercent(config.GetPoolRatePpm()),
SettlementWindowWager: config.GetSettlementWindowWager(),
ControlBandPercent: ppmToPercent(config.GetControlBandPpm()),
GiftPriceReference: config.GetGiftPriceReference(),
EffectiveFromMS: config.GetEffectiveFromMs(),
Stages: stages,
},
}
}
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 ppmToMultiplier(value int64) float64 {
return float64(value) / 1_000_000
}
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(),
MultiplierPPM: draw.GetMultiplierPpm(),
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
}