飘屏后端决定
This commit is contained in:
parent
113c5096e0
commit
e7b6ed9fb7
@ -9944,21 +9944,23 @@ func (x *UpsertAchievementDefinitionResponse) GetCreated() bool {
|
||||
}
|
||||
|
||||
type LuckyGiftMeta struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
CommandId string `protobuf:"bytes,2,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
|
||||
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
DeviceId string `protobuf:"bytes,4,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"`
|
||||
RoomId string `protobuf:"bytes,5,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"`
|
||||
AnchorId string `protobuf:"bytes,6,opt,name=anchor_id,json=anchorId,proto3" json:"anchor_id,omitempty"`
|
||||
GiftId string `protobuf:"bytes,7,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"`
|
||||
CoinSpent int64 `protobuf:"varint,8,opt,name=coin_spent,json=coinSpent,proto3" json:"coin_spent,omitempty"`
|
||||
PaidAtMs int64 `protobuf:"varint,9,opt,name=paid_at_ms,json=paidAtMs,proto3" json:"paid_at_ms,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"`
|
||||
GiftCount int32 `protobuf:"varint,12,opt,name=gift_count,json=giftCount,proto3" json:"gift_count,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
CommandId string `protobuf:"bytes,2,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
|
||||
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
DeviceId string `protobuf:"bytes,4,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"`
|
||||
RoomId string `protobuf:"bytes,5,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"`
|
||||
AnchorId string `protobuf:"bytes,6,opt,name=anchor_id,json=anchorId,proto3" json:"anchor_id,omitempty"`
|
||||
GiftId string `protobuf:"bytes,7,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"`
|
||||
CoinSpent int64 `protobuf:"varint,8,opt,name=coin_spent,json=coinSpent,proto3" json:"coin_spent,omitempty"`
|
||||
PaidAtMs int64 `protobuf:"varint,9,opt,name=paid_at_ms,json=paidAtMs,proto3" json:"paid_at_ms,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"`
|
||||
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
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *LuckyGiftMeta) Reset() {
|
||||
@ -10075,6 +10077,13 @@ func (x *LuckyGiftMeta) GetGiftCount() int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *LuckyGiftMeta) GetVisibleRegionId() int64 {
|
||||
if x != nil {
|
||||
return x.VisibleRegionId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type LuckyGiftTier struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
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" +
|
||||
"#UpsertAchievementDefinitionResponse\x12J\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" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1e.hyapp.activity.v1.RequestMetaR\x04meta\x12\x1d\n" +
|
||||
"\n" +
|
||||
@ -12633,7 +12642,8 @@ const file_proto_activity_v1_activity_proto_rawDesc = "" +
|
||||
" \x01(\tR\x06poolId\x12$\n" +
|
||||
"\x0etarget_user_id\x18\v \x01(\x03R\ftargetUserId\x12\x1d\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" +
|
||||
"\x04pool\x18\x01 \x01(\tR\x04pool\x12\x17\n" +
|
||||
"\atier_id\x18\x02 \x01(\tR\x06tierId\x12!\n" +
|
||||
|
||||
@ -1125,6 +1125,8 @@ message LuckyGiftMeta {
|
||||
string pool_id = 10;
|
||||
int64 target_user_id = 11;
|
||||
int32 gift_count = 12;
|
||||
// visible_region_id 来自 room-service 的房间可见区域,用于中奖后的区域 IM 飘屏。
|
||||
int64 visible_region_id = 13;
|
||||
}
|
||||
|
||||
message LuckyGiftTier {
|
||||
|
||||
@ -121,10 +121,6 @@ func New(cfg config.Config) (*App, error) {
|
||||
}
|
||||
broadcastPublisher = tencentClient
|
||||
}
|
||||
luckyGiftSvc := luckygiftservice.New(repository,
|
||||
luckygiftservice.WithWallet(walletv1.NewWalletServiceClient(walletConn)),
|
||||
luckygiftservice.WithRoomPublisher(tencentClient),
|
||||
)
|
||||
if cfg.Broadcast.Enabled && broadcastPublisher == nil {
|
||||
_ = walletConn.Close()
|
||||
_ = userConn.Close()
|
||||
@ -141,6 +137,11 @@ func New(cfg config.Config) (*App, error) {
|
||||
WorkerPollInterval: cfg.Broadcast.WorkerPollInterval,
|
||||
EnsureGroupsOnStartup: cfg.Broadcast.EnsureGroupsOnStartup,
|
||||
}, 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.RegisterMessageInboxServiceServer(server, grpcserver.NewMessageServer(messageSvc))
|
||||
activityv1.RegisterActivityCronServiceServer(server, grpcserver.NewCronServer(messageSvc, growthSvc, achievementSvc))
|
||||
|
||||
@ -14,6 +14,8 @@ const (
|
||||
TypeRedPacket = "red_packet"
|
||||
// TypeRoomTreasure 是语音房宝箱满能量后的开箱倒计时播报。
|
||||
TypeRoomTreasure = "room_treasure"
|
||||
// TypeLuckyGiftBigWin 是幸运礼物中奖区域飘屏;当前默认 1 倍及以上即进入播报 outbox。
|
||||
TypeLuckyGiftBigWin = "lucky_gift_big_win"
|
||||
|
||||
// StatusPending 表示消息已持久化,尚未被 worker claim。
|
||||
StatusPending = "pending"
|
||||
|
||||
@ -84,17 +84,18 @@ type CheckCommand struct {
|
||||
}
|
||||
|
||||
type DrawCommand struct {
|
||||
CommandID string
|
||||
PoolID string
|
||||
UserID int64
|
||||
TargetUserID int64
|
||||
DeviceID string
|
||||
RoomID string
|
||||
AnchorID string
|
||||
GiftID string
|
||||
GiftCount int32
|
||||
CoinSpent int64
|
||||
PaidAtMS int64
|
||||
CommandID string
|
||||
PoolID string
|
||||
UserID int64
|
||||
TargetUserID int64
|
||||
DeviceID string
|
||||
RoomID string
|
||||
AnchorID string
|
||||
GiftID string
|
||||
GiftCount int32
|
||||
CoinSpent int64
|
||||
PaidAtMS int64
|
||||
VisibleRegionID int64
|
||||
}
|
||||
|
||||
type CheckResult struct {
|
||||
|
||||
@ -122,8 +122,8 @@ func normalizeMultiplierPPMs(input []int64, tiers []domain.Tier, giftPrice int64
|
||||
if len(values) == 0 {
|
||||
values = defaultMultiplierPPMs()
|
||||
}
|
||||
seen := map[int64]bool{0: true}
|
||||
out := []int64{0}
|
||||
seen := map[int64]bool{}
|
||||
out := make([]int64, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value < 0 || seen[value] {
|
||||
continue
|
||||
@ -131,6 +131,9 @@ func normalizeMultiplierPPMs(input []int64, tiers []domain.Tier, giftPrice int64
|
||||
seen[value] = true
|
||||
out = append(out, value)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
out = defaultMultiplierPPMs()
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||
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) {
|
||||
config := DefaultConfig("hyapp", "pool_low")
|
||||
config.MultiplierPPMs = []int64{0, 500_000}
|
||||
|
||||
@ -14,7 +14,9 @@ import (
|
||||
"hyapp/pkg/logx"
|
||||
"hyapp/pkg/tencentim"
|
||||
"hyapp/pkg/xerr"
|
||||
broadcastdomain "hyapp/services/activity-service/internal/domain/broadcast"
|
||||
domain "hyapp/services/activity-service/internal/domain/luckygift"
|
||||
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
|
||||
)
|
||||
|
||||
// Repository 是幸运礼物唯一持久化边界;RTP、奖池、风控计数必须在同一个 MySQL 事务内修改。
|
||||
@ -44,12 +46,18 @@ type RoomPublisher interface {
|
||||
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 承载幸运礼物查询、抽奖命令和后台规则配置。
|
||||
type Service struct {
|
||||
repository Repository
|
||||
wallet WalletClient
|
||||
publisher RoomPublisher
|
||||
now func() time.Time
|
||||
repository Repository
|
||||
wallet WalletClient
|
||||
publisher RoomPublisher
|
||||
broadcaster RegionBroadcaster
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
type Option func(*Service)
|
||||
@ -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 {
|
||||
service := &Service{repository: repository, now: time.Now}
|
||||
for _, option := range options {
|
||||
@ -194,6 +208,14 @@ func (s *Service) processDrawOutbox(ctx context.Context, event domain.DrawOutbox
|
||||
if s.publisher == nil {
|
||||
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 {
|
||||
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 {
|
||||
nowMS := s.now().UTC().UnixMilli()
|
||||
nextRetryCount := event.RetryCount + 1
|
||||
@ -321,6 +360,7 @@ type luckyGiftDrawnPayload struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
SenderUserID int64 `json:"sender_user_id"`
|
||||
TargetUserID int64 `json:"target_user_id"`
|
||||
VisibleRegionID int64 `json:"visible_region_id"`
|
||||
CoinSpent int64 `json:"coin_spent"`
|
||||
RuleVersion int64 `json:"rule_version"`
|
||||
ExperiencePool string `json:"experience_pool"`
|
||||
@ -335,6 +375,49 @@ type luckyGiftDrawnPayload struct {
|
||||
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) {
|
||||
var payload luckyGiftDrawnPayload
|
||||
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,
|
||||
"sender_user_id": input.Command.UserID,
|
||||
"target_user_id": input.Command.TargetUserID,
|
||||
"visible_region_id": input.Command.VisibleRegionID,
|
||||
"room_id": input.Command.RoomID,
|
||||
"gift_id": input.Command.GiftID,
|
||||
"gift_count": input.Command.GiftCount,
|
||||
@ -1332,15 +1333,11 @@ func (r *Repository) selectLuckyCandidate(config domain.Config, pool string, sta
|
||||
limited := map[string]bool{}
|
||||
|
||||
baseCandidates := make([]luckyCandidate, 0, len(config.Tiers))
|
||||
hasZeroCandidate := false
|
||||
var totalWeight int64
|
||||
for _, tier := range config.Tiers {
|
||||
if tier.Pool != pool || !tier.Enabled {
|
||||
continue
|
||||
}
|
||||
if tier.RewardCoins == 0 {
|
||||
hasZeroCandidate = true
|
||||
}
|
||||
highMultiplier := tier.HighWaterOnly || tier.RewardCoins >= config.GiftPrice*config.HighMultiplier
|
||||
if highMultiplier && !config.LargeTierEnabled {
|
||||
limited["large_tier_disabled"] = true
|
||||
@ -1371,9 +1368,6 @@ func (r *Repository) selectLuckyCandidate(config domain.Config, pool string, sta
|
||||
HighMultiplier: highMultiplier,
|
||||
})
|
||||
}
|
||||
if minRequired == 0 && !hasZeroCandidate {
|
||||
baseCandidates = append(baseCandidates, luckyCandidate{TierID: pool + "_none", Source: domain.SourceBaseRTP, Pool: pool})
|
||||
}
|
||||
candidates := luckyAutoWeightedBaseCandidates(baseCandidates, minRequired, globalWindow, giftWindow)
|
||||
for _, candidate := range candidates {
|
||||
totalWeight += candidate.Weight
|
||||
@ -1673,9 +1667,6 @@ func luckyMultiplierPPMsFromTiers(tiers []domain.Tier, giftPrice int64) []int64
|
||||
seen[multiplier] = true
|
||||
out = append(out, multiplier)
|
||||
}
|
||||
if !seen[0] {
|
||||
out = append(out, 0)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||
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) {
|
||||
if status := luckyInitialRewardStatus(luckyCandidate{TierID: "none"}, false); status != domain.StatusGranted {
|
||||
t.Fatalf("zero reward without presentation should be granted, got %s", status)
|
||||
|
||||
@ -48,17 +48,18 @@ func (s *LuckyGiftServer) ExecuteLuckyGiftDraw(ctx context.Context, req *activit
|
||||
meta := req.GetLuckyGift()
|
||||
ctx = appcode.WithContext(ctx, meta.GetMeta().GetAppCode())
|
||||
result, err := s.svc.Draw(ctx, domain.DrawCommand{
|
||||
CommandID: meta.GetCommandId(),
|
||||
PoolID: meta.GetPoolId(),
|
||||
UserID: meta.GetUserId(),
|
||||
TargetUserID: meta.GetTargetUserId(),
|
||||
DeviceID: meta.GetDeviceId(),
|
||||
RoomID: meta.GetRoomId(),
|
||||
AnchorID: meta.GetAnchorId(),
|
||||
GiftID: meta.GetGiftId(),
|
||||
GiftCount: meta.GetGiftCount(),
|
||||
CoinSpent: meta.GetCoinSpent(),
|
||||
PaidAtMS: meta.GetPaidAtMs(),
|
||||
CommandID: meta.GetCommandId(),
|
||||
PoolID: meta.GetPoolId(),
|
||||
UserID: meta.GetUserId(),
|
||||
TargetUserID: meta.GetTargetUserId(),
|
||||
DeviceID: meta.GetDeviceId(),
|
||||
RoomID: meta.GetRoomId(),
|
||||
AnchorID: meta.GetAnchorId(),
|
||||
GiftID: meta.GetGiftId(),
|
||||
GiftCount: meta.GetGiftCount(),
|
||||
CoinSpent: meta.GetCoinSpent(),
|
||||
PaidAtMS: meta.GetPaidAtMs(),
|
||||
VisibleRegionID: meta.GetVisibleRegionId(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
|
||||
@ -126,18 +126,19 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
if luckyEnabled {
|
||||
drawResp, err := s.luckyGift.ExecuteLuckyGiftDraw(ctx, &activityv1.ExecuteLuckyGiftDrawRequest{
|
||||
LuckyGift: &activityv1.LuckyGiftMeta{
|
||||
Meta: activityMetaFromRoom(ctx, req.GetMeta()),
|
||||
CommandId: cmd.ID(),
|
||||
UserId: cmd.ActorUserID(),
|
||||
TargetUserId: cmd.TargetUserID,
|
||||
DeviceId: luckyGiftDeviceID(cmd),
|
||||
RoomId: cmd.RoomID(),
|
||||
AnchorId: luckyGiftAnchorID(roomMeta),
|
||||
GiftId: cmd.GiftID,
|
||||
GiftCount: cmd.GiftCount,
|
||||
CoinSpent: billing.GetCoinSpent(),
|
||||
PaidAtMs: now.UTC().UnixMilli(),
|
||||
PoolId: cmd.PoolID,
|
||||
Meta: activityMetaFromRoom(ctx, req.GetMeta()),
|
||||
CommandId: cmd.ID(),
|
||||
UserId: cmd.ActorUserID(),
|
||||
TargetUserId: cmd.TargetUserID,
|
||||
DeviceId: luckyGiftDeviceID(cmd),
|
||||
RoomId: cmd.RoomID(),
|
||||
AnchorId: luckyGiftAnchorID(roomMeta),
|
||||
GiftId: cmd.GiftID,
|
||||
GiftCount: cmd.GiftCount,
|
||||
CoinSpent: billing.GetCoinSpent(),
|
||||
PaidAtMs: now.UTC().UnixMilli(),
|
||||
PoolId: cmd.PoolID,
|
||||
VisibleRegionId: roomMeta.VisibleRegionID,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user