飘屏后端决定
This commit is contained in:
parent
113c5096e0
commit
e7b6ed9fb7
@ -9957,6 +9957,8 @@ type LuckyGiftMeta struct {
|
|||||||
PoolId string `protobuf:"bytes,10,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
|
PoolId string `protobuf:"bytes,10,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
|
||||||
TargetUserId int64 `protobuf:"varint,11,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
|
TargetUserId int64 `protobuf:"varint,11,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
|
||||||
GiftCount int32 `protobuf:"varint,12,opt,name=gift_count,json=giftCount,proto3" json:"gift_count,omitempty"`
|
GiftCount int32 `protobuf:"varint,12,opt,name=gift_count,json=giftCount,proto3" json:"gift_count,omitempty"`
|
||||||
|
// visible_region_id 来自 room-service 的房间可见区域,用于中奖后的区域 IM 飘屏。
|
||||||
|
VisibleRegionId int64 `protobuf:"varint,13,opt,name=visible_region_id,json=visibleRegionId,proto3" json:"visible_region_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -10075,6 +10077,13 @@ func (x *LuckyGiftMeta) GetGiftCount() int32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *LuckyGiftMeta) GetVisibleRegionId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.VisibleRegionId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type LuckyGiftTier struct {
|
type LuckyGiftTier struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Pool string `protobuf:"bytes,1,opt,name=pool,proto3" json:"pool,omitempty"`
|
Pool string `protobuf:"bytes,1,opt,name=pool,proto3" json:"pool,omitempty"`
|
||||||
@ -12615,7 +12624,7 @@ const file_proto_activity_v1_activity_proto_rawDesc = "" +
|
|||||||
"conditions\"\x8b\x01\n" +
|
"conditions\"\x8b\x01\n" +
|
||||||
"#UpsertAchievementDefinitionResponse\x12J\n" +
|
"#UpsertAchievementDefinitionResponse\x12J\n" +
|
||||||
"\vachievement\x18\x01 \x01(\v2(.hyapp.activity.v1.AchievementDefinitionR\vachievement\x12\x18\n" +
|
"\vachievement\x18\x01 \x01(\v2(.hyapp.activity.v1.AchievementDefinitionR\vachievement\x12\x18\n" +
|
||||||
"\acreated\x18\x02 \x01(\bR\acreated\"\x82\x03\n" +
|
"\acreated\x18\x02 \x01(\bR\acreated\"\xae\x03\n" +
|
||||||
"\rLuckyGiftMeta\x122\n" +
|
"\rLuckyGiftMeta\x122\n" +
|
||||||
"\x04meta\x18\x01 \x01(\v2\x1e.hyapp.activity.v1.RequestMetaR\x04meta\x12\x1d\n" +
|
"\x04meta\x18\x01 \x01(\v2\x1e.hyapp.activity.v1.RequestMetaR\x04meta\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
@ -12633,7 +12642,8 @@ const file_proto_activity_v1_activity_proto_rawDesc = "" +
|
|||||||
" \x01(\tR\x06poolId\x12$\n" +
|
" \x01(\tR\x06poolId\x12$\n" +
|
||||||
"\x0etarget_user_id\x18\v \x01(\x03R\ftargetUserId\x12\x1d\n" +
|
"\x0etarget_user_id\x18\v \x01(\x03R\ftargetUserId\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"gift_count\x18\f \x01(\x05R\tgiftCount\"\xe0\x01\n" +
|
"gift_count\x18\f \x01(\x05R\tgiftCount\x12*\n" +
|
||||||
|
"\x11visible_region_id\x18\r \x01(\x03R\x0fvisibleRegionId\"\xe0\x01\n" +
|
||||||
"\rLuckyGiftTier\x12\x12\n" +
|
"\rLuckyGiftTier\x12\x12\n" +
|
||||||
"\x04pool\x18\x01 \x01(\tR\x04pool\x12\x17\n" +
|
"\x04pool\x18\x01 \x01(\tR\x04pool\x12\x17\n" +
|
||||||
"\atier_id\x18\x02 \x01(\tR\x06tierId\x12!\n" +
|
"\atier_id\x18\x02 \x01(\tR\x06tierId\x12!\n" +
|
||||||
|
|||||||
@ -1125,6 +1125,8 @@ message LuckyGiftMeta {
|
|||||||
string pool_id = 10;
|
string pool_id = 10;
|
||||||
int64 target_user_id = 11;
|
int64 target_user_id = 11;
|
||||||
int32 gift_count = 12;
|
int32 gift_count = 12;
|
||||||
|
// visible_region_id 来自 room-service 的房间可见区域,用于中奖后的区域 IM 飘屏。
|
||||||
|
int64 visible_region_id = 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LuckyGiftTier {
|
message LuckyGiftTier {
|
||||||
|
|||||||
@ -121,10 +121,6 @@ func New(cfg config.Config) (*App, error) {
|
|||||||
}
|
}
|
||||||
broadcastPublisher = tencentClient
|
broadcastPublisher = tencentClient
|
||||||
}
|
}
|
||||||
luckyGiftSvc := luckygiftservice.New(repository,
|
|
||||||
luckygiftservice.WithWallet(walletv1.NewWalletServiceClient(walletConn)),
|
|
||||||
luckygiftservice.WithRoomPublisher(tencentClient),
|
|
||||||
)
|
|
||||||
if cfg.Broadcast.Enabled && broadcastPublisher == nil {
|
if cfg.Broadcast.Enabled && broadcastPublisher == nil {
|
||||||
_ = walletConn.Close()
|
_ = walletConn.Close()
|
||||||
_ = userConn.Close()
|
_ = userConn.Close()
|
||||||
@ -141,6 +137,11 @@ func New(cfg config.Config) (*App, error) {
|
|||||||
WorkerPollInterval: cfg.Broadcast.WorkerPollInterval,
|
WorkerPollInterval: cfg.Broadcast.WorkerPollInterval,
|
||||||
EnsureGroupsOnStartup: cfg.Broadcast.EnsureGroupsOnStartup,
|
EnsureGroupsOnStartup: cfg.Broadcast.EnsureGroupsOnStartup,
|
||||||
}, repository, broadcastPublisher, client.NewGRPCRegionSource(userConn))
|
}, repository, broadcastPublisher, client.NewGRPCRegionSource(userConn))
|
||||||
|
luckyGiftSvc := luckygiftservice.New(repository,
|
||||||
|
luckygiftservice.WithWallet(walletv1.NewWalletServiceClient(walletConn)),
|
||||||
|
luckygiftservice.WithRoomPublisher(tencentClient),
|
||||||
|
luckygiftservice.WithRegionBroadcaster(broadcastSvc),
|
||||||
|
)
|
||||||
activityv1.RegisterActivityServiceServer(server, grpcserver.NewServer(svc))
|
activityv1.RegisterActivityServiceServer(server, grpcserver.NewServer(svc))
|
||||||
activityv1.RegisterMessageInboxServiceServer(server, grpcserver.NewMessageServer(messageSvc))
|
activityv1.RegisterMessageInboxServiceServer(server, grpcserver.NewMessageServer(messageSvc))
|
||||||
activityv1.RegisterActivityCronServiceServer(server, grpcserver.NewCronServer(messageSvc, growthSvc, achievementSvc))
|
activityv1.RegisterActivityCronServiceServer(server, grpcserver.NewCronServer(messageSvc, growthSvc, achievementSvc))
|
||||||
|
|||||||
@ -14,6 +14,8 @@ const (
|
|||||||
TypeRedPacket = "red_packet"
|
TypeRedPacket = "red_packet"
|
||||||
// TypeRoomTreasure 是语音房宝箱满能量后的开箱倒计时播报。
|
// TypeRoomTreasure 是语音房宝箱满能量后的开箱倒计时播报。
|
||||||
TypeRoomTreasure = "room_treasure"
|
TypeRoomTreasure = "room_treasure"
|
||||||
|
// TypeLuckyGiftBigWin 是幸运礼物中奖区域飘屏;当前默认 1 倍及以上即进入播报 outbox。
|
||||||
|
TypeLuckyGiftBigWin = "lucky_gift_big_win"
|
||||||
|
|
||||||
// StatusPending 表示消息已持久化,尚未被 worker claim。
|
// StatusPending 表示消息已持久化,尚未被 worker claim。
|
||||||
StatusPending = "pending"
|
StatusPending = "pending"
|
||||||
|
|||||||
@ -95,6 +95,7 @@ type DrawCommand struct {
|
|||||||
GiftCount int32
|
GiftCount int32
|
||||||
CoinSpent int64
|
CoinSpent int64
|
||||||
PaidAtMS int64
|
PaidAtMS int64
|
||||||
|
VisibleRegionID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type CheckResult struct {
|
type CheckResult struct {
|
||||||
|
|||||||
@ -122,8 +122,8 @@ func normalizeMultiplierPPMs(input []int64, tiers []domain.Tier, giftPrice int64
|
|||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
values = defaultMultiplierPPMs()
|
values = defaultMultiplierPPMs()
|
||||||
}
|
}
|
||||||
seen := map[int64]bool{0: true}
|
seen := map[int64]bool{}
|
||||||
out := []int64{0}
|
out := make([]int64, 0, len(values))
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
if value < 0 || seen[value] {
|
if value < 0 || seen[value] {
|
||||||
continue
|
continue
|
||||||
@ -131,6 +131,9 @@ func normalizeMultiplierPPMs(input []int64, tiers []domain.Tier, giftPrice int64
|
|||||||
seen[value] = true
|
seen[value] = true
|
||||||
out = append(out, value)
|
out = append(out, value)
|
||||||
}
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
out = defaultMultiplierPPMs()
|
||||||
|
}
|
||||||
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,6 +41,19 @@ func TestNormalizeTiersBuildsMultiplierDrivenTiers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeTiersDoesNotForceZeroMultiplier(t *testing.T) {
|
||||||
|
tiers, multipliers := normalizeTiers(nil, []int64{1_000_000, 2_000_000}, 500, 100)
|
||||||
|
|
||||||
|
if len(multipliers) != 2 || multipliers[0] != 1_000_000 || multipliers[1] != 2_000_000 {
|
||||||
|
t.Fatalf("expected positive-only multiplier list to stay positive-only, got %#v", multipliers)
|
||||||
|
}
|
||||||
|
for _, tier := range tiers {
|
||||||
|
if tier.MultiplierPPM == 0 || tier.RewardCoins == 0 {
|
||||||
|
t.Fatalf("positive-only test config must not generate none tier: %#v", tier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateConfigRejectsMultiplierListBelowTargetRTP(t *testing.T) {
|
func TestValidateConfigRejectsMultiplierListBelowTargetRTP(t *testing.T) {
|
||||||
config := DefaultConfig("hyapp", "pool_low")
|
config := DefaultConfig("hyapp", "pool_low")
|
||||||
config.MultiplierPPMs = []int64{0, 500_000}
|
config.MultiplierPPMs = []int64{0, 500_000}
|
||||||
|
|||||||
@ -14,7 +14,9 @@ import (
|
|||||||
"hyapp/pkg/logx"
|
"hyapp/pkg/logx"
|
||||||
"hyapp/pkg/tencentim"
|
"hyapp/pkg/tencentim"
|
||||||
"hyapp/pkg/xerr"
|
"hyapp/pkg/xerr"
|
||||||
|
broadcastdomain "hyapp/services/activity-service/internal/domain/broadcast"
|
||||||
domain "hyapp/services/activity-service/internal/domain/luckygift"
|
domain "hyapp/services/activity-service/internal/domain/luckygift"
|
||||||
|
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Repository 是幸运礼物唯一持久化边界;RTP、奖池、风控计数必须在同一个 MySQL 事务内修改。
|
// Repository 是幸运礼物唯一持久化边界;RTP、奖池、风控计数必须在同一个 MySQL 事务内修改。
|
||||||
@ -44,11 +46,17 @@ type RoomPublisher interface {
|
|||||||
PublishGroupCustomMessage(ctx context.Context, message tencentim.CustomGroupMessage) error
|
PublishGroupCustomMessage(ctx context.Context, message tencentim.CustomGroupMessage) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegionBroadcaster 只写 activity-service 自有的区域播报 outbox;真正发腾讯 IM 仍由 broadcast worker 补偿。
|
||||||
|
type RegionBroadcaster interface {
|
||||||
|
PublishRegionBroadcast(ctx context.Context, input broadcastservice.PublishInput) (broadcastdomain.PublishResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
// Service 承载幸运礼物查询、抽奖命令和后台规则配置。
|
// Service 承载幸运礼物查询、抽奖命令和后台规则配置。
|
||||||
type Service struct {
|
type Service struct {
|
||||||
repository Repository
|
repository Repository
|
||||||
wallet WalletClient
|
wallet WalletClient
|
||||||
publisher RoomPublisher
|
publisher RoomPublisher
|
||||||
|
broadcaster RegionBroadcaster
|
||||||
now func() time.Time
|
now func() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,6 +74,12 @@ func WithRoomPublisher(publisher RoomPublisher) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithRegionBroadcaster(broadcaster RegionBroadcaster) Option {
|
||||||
|
return func(s *Service) {
|
||||||
|
s.broadcaster = broadcaster
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func New(repository Repository, options ...Option) *Service {
|
func New(repository Repository, options ...Option) *Service {
|
||||||
service := &Service{repository: repository, now: time.Now}
|
service := &Service{repository: repository, now: time.Now}
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
@ -194,6 +208,14 @@ func (s *Service) processDrawOutbox(ctx context.Context, event domain.DrawOutbox
|
|||||||
if s.publisher == nil {
|
if s.publisher == nil {
|
||||||
return s.markOutboxFailed(eventCtx, event, options, fmt.Errorf("lucky gift room publisher is not configured"), credited)
|
return s.markOutboxFailed(eventCtx, event, options, fmt.Errorf("lucky gift room publisher is not configured"), credited)
|
||||||
}
|
}
|
||||||
|
if shouldPublishLuckyGiftRegionBroadcast(payload) {
|
||||||
|
if s.broadcaster == nil {
|
||||||
|
return s.markOutboxFailed(eventCtx, event, options, fmt.Errorf("lucky gift region broadcaster is not configured"), credited)
|
||||||
|
}
|
||||||
|
if err := s.publishLuckyGiftRegionBroadcast(eventCtx, payload); err != nil {
|
||||||
|
return s.markOutboxFailed(eventCtx, event, options, err, credited)
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := s.publishLuckyGiftDrawn(eventCtx, payload, options); err != nil {
|
if err := s.publishLuckyGiftDrawn(eventCtx, payload, options); err != nil {
|
||||||
return s.markOutboxFailed(eventCtx, event, options, err, credited)
|
return s.markOutboxFailed(eventCtx, event, options, err, credited)
|
||||||
}
|
}
|
||||||
@ -220,6 +242,23 @@ func (s *Service) publishLuckyGiftDrawn(ctx context.Context, payload luckyGiftDr
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) publishLuckyGiftRegionBroadcast(ctx context.Context, payload luckyGiftDrawnPayload) error {
|
||||||
|
if payload.VisibleRegionID <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(luckyGiftRegionBroadcastPayload(payload, s.now().UTC().UnixMilli()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = s.broadcaster.PublishRegionBroadcast(ctx, broadcastservice.PublishInput{
|
||||||
|
EventID: "lucky_gift_big_win:" + payload.DrawID,
|
||||||
|
BroadcastType: broadcastdomain.TypeLuckyGiftBigWin,
|
||||||
|
RegionID: payload.VisibleRegionID,
|
||||||
|
PayloadJSON: string(body),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) markOutboxFailed(ctx context.Context, event domain.DrawOutbox, options WorkerOptions, cause error, alreadyCredited bool) error {
|
func (s *Service) markOutboxFailed(ctx context.Context, event domain.DrawOutbox, options WorkerOptions, cause error, alreadyCredited bool) error {
|
||||||
nowMS := s.now().UTC().UnixMilli()
|
nowMS := s.now().UTC().UnixMilli()
|
||||||
nextRetryCount := event.RetryCount + 1
|
nextRetryCount := event.RetryCount + 1
|
||||||
@ -321,6 +360,7 @@ type luckyGiftDrawnPayload struct {
|
|||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
SenderUserID int64 `json:"sender_user_id"`
|
SenderUserID int64 `json:"sender_user_id"`
|
||||||
TargetUserID int64 `json:"target_user_id"`
|
TargetUserID int64 `json:"target_user_id"`
|
||||||
|
VisibleRegionID int64 `json:"visible_region_id"`
|
||||||
CoinSpent int64 `json:"coin_spent"`
|
CoinSpent int64 `json:"coin_spent"`
|
||||||
RuleVersion int64 `json:"rule_version"`
|
RuleVersion int64 `json:"rule_version"`
|
||||||
ExperiencePool string `json:"experience_pool"`
|
ExperiencePool string `json:"experience_pool"`
|
||||||
@ -335,6 +375,49 @@ type luckyGiftDrawnPayload struct {
|
|||||||
CreatedAtMS int64 `json:"created_at_ms"`
|
CreatedAtMS int64 `json:"created_at_ms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldPublishLuckyGiftRegionBroadcast(payload luckyGiftDrawnPayload) bool {
|
||||||
|
// 当前产品策略先把 1 倍作为默认区域飘屏阈值;后续接后台配置时只替换这里的阈值来源。
|
||||||
|
return payload.VisibleRegionID > 0 && payload.EffectiveRewardCoins > 0 && payload.MultiplierPPM >= 1_000_000
|
||||||
|
}
|
||||||
|
|
||||||
|
func luckyGiftRegionBroadcastPayload(payload luckyGiftDrawnPayload, sentAtMS int64) map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"event_type": "lucky_gift_drawn",
|
||||||
|
"event_id": "lucky_gift_big_win:" + payload.DrawID,
|
||||||
|
"broadcast_type": broadcastdomain.TypeLuckyGiftBigWin,
|
||||||
|
"scope": broadcastdomain.ScopeRegion,
|
||||||
|
"app_code": payload.AppCode,
|
||||||
|
"region_id": payload.VisibleRegionID,
|
||||||
|
"visible_region_id": payload.VisibleRegionID,
|
||||||
|
"draw_id": payload.DrawID,
|
||||||
|
"command_id": payload.CommandID,
|
||||||
|
"pool_id": payload.PoolID,
|
||||||
|
"room_id": payload.RoomID,
|
||||||
|
"gift_id": payload.GiftID,
|
||||||
|
"gift_count": payload.GiftCount,
|
||||||
|
"user_id": payload.UserID,
|
||||||
|
"sender_user_id": payload.SenderUserID,
|
||||||
|
"target_user_id": payload.TargetUserID,
|
||||||
|
"coin_spent": payload.CoinSpent,
|
||||||
|
"rule_version": payload.RuleVersion,
|
||||||
|
"experience_pool": payload.ExperiencePool,
|
||||||
|
"selected_tier_id": payload.SelectedTierID,
|
||||||
|
"multiplier_ppm": payload.MultiplierPPM,
|
||||||
|
"base_reward_coins": payload.BaseRewardCoins,
|
||||||
|
"room_atmosphere_reward_coins": payload.RoomAtmosphereRewardCoins,
|
||||||
|
"activity_subsidy_coins": payload.ActivitySubsidyCoins,
|
||||||
|
"effective_reward_coins": payload.EffectiveRewardCoins,
|
||||||
|
"stage_feedback": payload.StageFeedback,
|
||||||
|
"high_multiplier": payload.HighMultiplier,
|
||||||
|
"created_at_ms": payload.CreatedAtMS,
|
||||||
|
"sent_at_ms": sentAtMS,
|
||||||
|
"action": map[string]any{
|
||||||
|
"type": "enter_room",
|
||||||
|
"room_id": payload.RoomID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func decodeDrawOutboxPayload(payloadJSON string) (luckyGiftDrawnPayload, error) {
|
func decodeDrawOutboxPayload(payloadJSON string) (luckyGiftDrawnPayload, error) {
|
||||||
var payload luckyGiftDrawnPayload
|
var payload luckyGiftDrawnPayload
|
||||||
if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil {
|
if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil {
|
||||||
|
|||||||
@ -583,6 +583,7 @@ func (r *Repository) applyLuckyDraw(ctx context.Context, tx *sql.Tx, input lucky
|
|||||||
"user_id": input.Command.UserID,
|
"user_id": input.Command.UserID,
|
||||||
"sender_user_id": input.Command.UserID,
|
"sender_user_id": input.Command.UserID,
|
||||||
"target_user_id": input.Command.TargetUserID,
|
"target_user_id": input.Command.TargetUserID,
|
||||||
|
"visible_region_id": input.Command.VisibleRegionID,
|
||||||
"room_id": input.Command.RoomID,
|
"room_id": input.Command.RoomID,
|
||||||
"gift_id": input.Command.GiftID,
|
"gift_id": input.Command.GiftID,
|
||||||
"gift_count": input.Command.GiftCount,
|
"gift_count": input.Command.GiftCount,
|
||||||
@ -1332,15 +1333,11 @@ func (r *Repository) selectLuckyCandidate(config domain.Config, pool string, sta
|
|||||||
limited := map[string]bool{}
|
limited := map[string]bool{}
|
||||||
|
|
||||||
baseCandidates := make([]luckyCandidate, 0, len(config.Tiers))
|
baseCandidates := make([]luckyCandidate, 0, len(config.Tiers))
|
||||||
hasZeroCandidate := false
|
|
||||||
var totalWeight int64
|
var totalWeight int64
|
||||||
for _, tier := range config.Tiers {
|
for _, tier := range config.Tiers {
|
||||||
if tier.Pool != pool || !tier.Enabled {
|
if tier.Pool != pool || !tier.Enabled {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if tier.RewardCoins == 0 {
|
|
||||||
hasZeroCandidate = true
|
|
||||||
}
|
|
||||||
highMultiplier := tier.HighWaterOnly || tier.RewardCoins >= config.GiftPrice*config.HighMultiplier
|
highMultiplier := tier.HighWaterOnly || tier.RewardCoins >= config.GiftPrice*config.HighMultiplier
|
||||||
if highMultiplier && !config.LargeTierEnabled {
|
if highMultiplier && !config.LargeTierEnabled {
|
||||||
limited["large_tier_disabled"] = true
|
limited["large_tier_disabled"] = true
|
||||||
@ -1371,9 +1368,6 @@ func (r *Repository) selectLuckyCandidate(config domain.Config, pool string, sta
|
|||||||
HighMultiplier: highMultiplier,
|
HighMultiplier: highMultiplier,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if minRequired == 0 && !hasZeroCandidate {
|
|
||||||
baseCandidates = append(baseCandidates, luckyCandidate{TierID: pool + "_none", Source: domain.SourceBaseRTP, Pool: pool})
|
|
||||||
}
|
|
||||||
candidates := luckyAutoWeightedBaseCandidates(baseCandidates, minRequired, globalWindow, giftWindow)
|
candidates := luckyAutoWeightedBaseCandidates(baseCandidates, minRequired, globalWindow, giftWindow)
|
||||||
for _, candidate := range candidates {
|
for _, candidate := range candidates {
|
||||||
totalWeight += candidate.Weight
|
totalWeight += candidate.Weight
|
||||||
@ -1673,9 +1667,6 @@ func luckyMultiplierPPMsFromTiers(tiers []domain.Tier, giftPrice int64) []int64
|
|||||||
seen[multiplier] = true
|
seen[multiplier] = true
|
||||||
out = append(out, multiplier)
|
out = append(out, multiplier)
|
||||||
}
|
}
|
||||||
if !seen[0] {
|
|
||||||
out = append(out, 0)
|
|
||||||
}
|
|
||||||
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,6 +100,60 @@ func TestLuckyAutoWeightedBaseCandidatesDoesNotDiluteDeficitAcrossLargeWindow(t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSelectLuckyCandidateDoesNotInjectNoneTier(t *testing.T) {
|
||||||
|
config := domain.Config{
|
||||||
|
GiftID: domain.DefaultPoolID,
|
||||||
|
PoolID: domain.DefaultPoolID,
|
||||||
|
GiftPrice: 10,
|
||||||
|
TargetRTPPPM: 950_000,
|
||||||
|
HighMultiplier: 100,
|
||||||
|
LargeTierEnabled: true,
|
||||||
|
PlatformPoolWeightPPM: 200_000,
|
||||||
|
RoomPoolWeightPPM: 300_000,
|
||||||
|
GiftPoolWeightPPM: 500_000,
|
||||||
|
MaxSinglePayout: 10_000,
|
||||||
|
UserHourlyPayoutCap: 10_000,
|
||||||
|
UserDailyPayoutCap: 10_000,
|
||||||
|
DeviceDailyPayoutCap: 10_000,
|
||||||
|
RoomHourlyPayoutCap: 10_000,
|
||||||
|
AnchorDailyPayoutCap: 10_000,
|
||||||
|
Tiers: []domain.Tier{
|
||||||
|
{Pool: domain.PoolAdvanced, TierID: "advanced_1x", RewardCoins: 10, MultiplierPPM: 1_000_000, Enabled: true},
|
||||||
|
{Pool: domain.PoolAdvanced, TierID: "advanced_2x", RewardCoins: 20, MultiplierPPM: 2_000_000, Enabled: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
window := luckyRTPWindow{ControlDraws: 100, TargetRTPPPM: 950_000}
|
||||||
|
pool := luckyPool{Balance: 1_000_000}
|
||||||
|
counters := map[string]luckyCounter{
|
||||||
|
luckyCounterKey("user", "hour"): {},
|
||||||
|
luckyCounterKey("user", "day"): {},
|
||||||
|
luckyCounterKey("device", "day"): {},
|
||||||
|
luckyCounterKey("room", "hour"): {},
|
||||||
|
luckyCounterKey("anchor", "day"): {},
|
||||||
|
}
|
||||||
|
|
||||||
|
candidate, _, err := (&Repository{}).selectLuckyCandidate(
|
||||||
|
config,
|
||||||
|
domain.PoolAdvanced,
|
||||||
|
false,
|
||||||
|
window,
|
||||||
|
window,
|
||||||
|
pool,
|
||||||
|
pool,
|
||||||
|
pool,
|
||||||
|
luckyPool{},
|
||||||
|
0,
|
||||||
|
counters,
|
||||||
|
20,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("select positive-only candidate failed: %v", err)
|
||||||
|
}
|
||||||
|
if candidate.BaseReward <= 0 || candidate.MultiplierPPM <= 0 {
|
||||||
|
t.Fatalf("positive-only config should not draw implicit none tier: %#v", candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLuckyInitialRewardStatusGrantsZeroRewardWithoutOutbox(t *testing.T) {
|
func TestLuckyInitialRewardStatusGrantsZeroRewardWithoutOutbox(t *testing.T) {
|
||||||
if status := luckyInitialRewardStatus(luckyCandidate{TierID: "none"}, false); status != domain.StatusGranted {
|
if status := luckyInitialRewardStatus(luckyCandidate{TierID: "none"}, false); status != domain.StatusGranted {
|
||||||
t.Fatalf("zero reward without presentation should be granted, got %s", status)
|
t.Fatalf("zero reward without presentation should be granted, got %s", status)
|
||||||
|
|||||||
@ -59,6 +59,7 @@ func (s *LuckyGiftServer) ExecuteLuckyGiftDraw(ctx context.Context, req *activit
|
|||||||
GiftCount: meta.GetGiftCount(),
|
GiftCount: meta.GetGiftCount(),
|
||||||
CoinSpent: meta.GetCoinSpent(),
|
CoinSpent: meta.GetCoinSpent(),
|
||||||
PaidAtMS: meta.GetPaidAtMs(),
|
PaidAtMS: meta.GetPaidAtMs(),
|
||||||
|
VisibleRegionID: meta.GetVisibleRegionId(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerr.ToGRPCError(err)
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
|||||||
@ -138,6 +138,7 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
|||||||
CoinSpent: billing.GetCoinSpent(),
|
CoinSpent: billing.GetCoinSpent(),
|
||||||
PaidAtMs: now.UTC().UnixMilli(),
|
PaidAtMs: now.UTC().UnixMilli(),
|
||||||
PoolId: cmd.PoolID,
|
PoolId: cmd.PoolID,
|
||||||
|
VisibleRegionId: roomMeta.VisibleRegionID,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user