From e7b6ed9fb7ee78fe1ec796db9191f39bedafb822 Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 26 May 2026 05:57:08 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A3=98=E5=B1=8F=E5=90=8E=E7=AB=AF=E5=86=B3?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/proto/activity/v1/activity.pb.go | 44 +++++---- api/proto/activity/v1/activity.proto | 2 + services/activity-service/internal/app/app.go | 9 +- .../internal/domain/broadcast/broadcast.go | 2 + .../internal/domain/luckygift/lucky_gift.go | 23 ++--- .../internal/service/luckygift/config.go | 7 +- .../internal/service/luckygift/config_test.go | 13 +++ .../internal/service/luckygift/service.go | 91 ++++++++++++++++++- .../storage/mysql/lucky_gift_repository.go | 11 +-- .../mysql/lucky_gift_repository_test.go | 54 +++++++++++ .../transport/grpc/lucky_gift_server.go | 23 ++--- .../internal/room/service/gift.go | 25 ++--- 12 files changed, 233 insertions(+), 71 deletions(-) diff --git a/api/proto/activity/v1/activity.pb.go b/api/proto/activity/v1/activity.pb.go index af5b4664..e337adad 100644 --- a/api/proto/activity/v1/activity.pb.go +++ b/api/proto/activity/v1/activity.pb.go @@ -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" + diff --git a/api/proto/activity/v1/activity.proto b/api/proto/activity/v1/activity.proto index 32fbc496..40a3c751 100644 --- a/api/proto/activity/v1/activity.proto +++ b/api/proto/activity/v1/activity.proto @@ -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 { diff --git a/services/activity-service/internal/app/app.go b/services/activity-service/internal/app/app.go index 9c128c09..82694772 100644 --- a/services/activity-service/internal/app/app.go +++ b/services/activity-service/internal/app/app.go @@ -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)) diff --git a/services/activity-service/internal/domain/broadcast/broadcast.go b/services/activity-service/internal/domain/broadcast/broadcast.go index b6ad0932..64eb3f98 100644 --- a/services/activity-service/internal/domain/broadcast/broadcast.go +++ b/services/activity-service/internal/domain/broadcast/broadcast.go @@ -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" diff --git a/services/activity-service/internal/domain/luckygift/lucky_gift.go b/services/activity-service/internal/domain/luckygift/lucky_gift.go index c4e99874..6c1272b6 100644 --- a/services/activity-service/internal/domain/luckygift/lucky_gift.go +++ b/services/activity-service/internal/domain/luckygift/lucky_gift.go @@ -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 { diff --git a/services/activity-service/internal/service/luckygift/config.go b/services/activity-service/internal/service/luckygift/config.go index 40983c91..da790371 100644 --- a/services/activity-service/internal/service/luckygift/config.go +++ b/services/activity-service/internal/service/luckygift/config.go @@ -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 } diff --git a/services/activity-service/internal/service/luckygift/config_test.go b/services/activity-service/internal/service/luckygift/config_test.go index 6e37ccd2..c7deb170 100644 --- a/services/activity-service/internal/service/luckygift/config_test.go +++ b/services/activity-service/internal/service/luckygift/config_test.go @@ -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} diff --git a/services/activity-service/internal/service/luckygift/service.go b/services/activity-service/internal/service/luckygift/service.go index bc1a2f7c..c9e1a6d1 100644 --- a/services/activity-service/internal/service/luckygift/service.go +++ b/services/activity-service/internal/service/luckygift/service.go @@ -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 { diff --git a/services/activity-service/internal/storage/mysql/lucky_gift_repository.go b/services/activity-service/internal/storage/mysql/lucky_gift_repository.go index d76ae280..4292d6c4 100644 --- a/services/activity-service/internal/storage/mysql/lucky_gift_repository.go +++ b/services/activity-service/internal/storage/mysql/lucky_gift_repository.go @@ -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 } diff --git a/services/activity-service/internal/storage/mysql/lucky_gift_repository_test.go b/services/activity-service/internal/storage/mysql/lucky_gift_repository_test.go index c42aba8a..ad106ca0 100644 --- a/services/activity-service/internal/storage/mysql/lucky_gift_repository_test.go +++ b/services/activity-service/internal/storage/mysql/lucky_gift_repository_test.go @@ -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) diff --git a/services/activity-service/internal/transport/grpc/lucky_gift_server.go b/services/activity-service/internal/transport/grpc/lucky_gift_server.go index f54081a5..b7313eda 100644 --- a/services/activity-service/internal/transport/grpc/lucky_gift_server.go +++ b/services/activity-service/internal/transport/grpc/lucky_gift_server.go @@ -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) diff --git a/services/room-service/internal/room/service/gift.go b/services/room-service/internal/room/service/gift.go index 29150e53..ec92c3aa 100644 --- a/services/room-service/internal/room/service/gift.go +++ b/services/room-service/internal/room/service/gift.go @@ -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 {