400 lines
15 KiB
Go
400 lines
15 KiB
Go
package luckygift
|
|
|
|
import (
|
|
"context"
|
|
"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
|
|
requestTimeout time.Duration
|
|
audit shared.OperationLogger
|
|
}
|
|
|
|
func New(activity activityclient.Client, requestTimeout time.Duration, audit shared.OperationLogger) *Handler {
|
|
if requestTimeout <= 0 {
|
|
requestTimeout = 3 * time.Second
|
|
}
|
|
return &Handler{activity: activity, requestTimeout: requestTimeout, 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"`
|
|
NoviceMaxEquivalentDraws int64 `json:"novice_max_equivalent_draws"`
|
|
NormalMaxEquivalentDraws int64 `json:"normal_max_equivalent_draws"`
|
|
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"`
|
|
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) {
|
|
ctx, cancel := h.activityContext(c)
|
|
defer cancel()
|
|
resp, err := h.activity.GetLuckyGiftConfig(ctx, &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"))
|
|
}
|
|
ctx, cancel := h.activityContext(c)
|
|
defer cancel()
|
|
resp, err := h.activity.UpsertLuckyGiftConfig(ctx, &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_rule_versions", item.PoolID, "success", "")
|
|
response.OK(c, item)
|
|
}
|
|
|
|
func (h *Handler) ListLuckyGiftConfigs(c *gin.Context) {
|
|
ctx, cancel := h.activityContext(c)
|
|
defer cancel()
|
|
resp, err := h.activity.ListLuckyGiftConfigs(ctx, &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)
|
|
ctx, cancel := h.activityContext(c)
|
|
defer cancel()
|
|
resp, err := h.activity.ListLuckyGiftDraws(ctx, &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) {
|
|
ctx, cancel := h.activityContext(c)
|
|
defer cancel()
|
|
resp, err := h.activity.GetLuckyGiftDrawSummary(ctx, &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 (h *Handler) activityContext(c *gin.Context) (context.Context, context.CancelFunc) {
|
|
return context.WithTimeout(c.Request.Context(), h.requestTimeout)
|
|
}
|
|
|
|
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,
|
|
NoviceMaxEquivalentDraws: req.NoviceMaxEquivalentDraws,
|
|
NormalMaxEquivalentDraws: req.NormalMaxEquivalentDraws,
|
|
MaxSinglePayout: req.MaxSinglePayout,
|
|
UserHourlyPayoutCap: req.UserHourlyPayoutCap,
|
|
UserDailyPayoutCap: req.UserDailyPayoutCap,
|
|
DeviceDailyPayoutCap: req.DeviceDailyPayoutCap,
|
|
RoomHourlyPayoutCap: req.RoomHourlyPayoutCap,
|
|
AnchorDailyPayoutCap: req.AnchorDailyPayoutCap,
|
|
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(),
|
|
NoviceMaxEquivalentDraws: config.GetNoviceMaxEquivalentDraws(),
|
|
NormalMaxEquivalentDraws: config.GetNormalMaxEquivalentDraws(),
|
|
MaxSinglePayout: config.GetMaxSinglePayout(),
|
|
UserHourlyPayoutCap: config.GetUserHourlyPayoutCap(),
|
|
UserDailyPayoutCap: config.GetUserDailyPayoutCap(),
|
|
DeviceDailyPayoutCap: config.GetDeviceDailyPayoutCap(),
|
|
RoomHourlyPayoutCap: config.GetRoomHourlyPayoutCap(),
|
|
AnchorDailyPayoutCap: config.GetAnchorDailyPayoutCap(),
|
|
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
|
|
}
|