From 6b16f8c96ea4ab5d20e96827d1c4e79647a0c6ee Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 9 Jun 2026 19:16:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=9B=B8=E5=85=B3=E7=81=AB?= =?UTF-8?q?=E7=AE=AD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/flutter对接/语音房火箭Flutter对接.md | 2 +- docs/语音房火箭功能架构.md | 13 +-- .../internal/room/service/room_rocket.go | 53 ++++++------ .../internal/room/service/room_rocket_test.go | 81 +++++++++++++++++++ 4 files changed, 114 insertions(+), 35 deletions(-) diff --git a/docs/flutter对接/语音房火箭Flutter对接.md b/docs/flutter对接/语音房火箭Flutter对接.md index 629c00d1..4d687a6b 100644 --- a/docs/flutter对接/语音房火箭Flutter对接.md +++ b/docs/flutter对接/语音房火箭Flutter对接.md @@ -274,7 +274,7 @@ | `rewards[].grant_id` | 发奖回执 | | `rewards[].status` | 发奖状态 | -奖励 IM 用于通知本次哪些人中奖。需要渲染金币数量、奖励天数、聊天气泡或麦位特效素材时,收到 IM 后调用 `GET /api/v1/rooms/{room_id}/rocket`,读取 `state.last_rewards[].resource_group`。 +奖励 IM 用于通知本次哪些人中奖。top1 和点火人命中后会发完整配置池,同一用户可能同时收到多条 `top1`、`igniter` 和 1 条在房随机奖励。需要渲染金币数量、奖励天数、聊天气泡或麦位特效素材时,收到 IM 后调用 `GET /api/v1/rooms/{room_id}/rocket`,读取 `state.last_rewards[].resource_group`。 ## 客户端处理 diff --git a/docs/语音房火箭功能架构.md b/docs/语音房火箭功能架构.md index 31d55d52..dcd06124 100644 --- a/docs/语音房火箭功能架构.md +++ b/docs/语音房火箭功能架构.md @@ -74,12 +74,15 @@ | 贡献第一奖励 | `pending_launches[].top1_user_id` | | 在房奖励 | 发射命令执行时 Room Cell `OnlineUsers` | -`reward_stack_policy`: +发放规则: -| 值 | 说明 | +| 奖励 | 规则 | | --- | --- | -| `allow_stack` | 同一用户可同时拿在房、top1、点火人奖励 | -| `priority_only` | 先点火人,再 top1,再在房;同一用户只拿一份 | +| 在房奖励 | 每个在线用户从 `inRoomRewards` 按权重随机抽 1 个 | +| 贡献第一奖励 | top1 用户获得 `top1Rewards` 配置池内全部奖励 | +| 点火人奖励 | 点火用户获得 `igniterRewards` 配置池内全部奖励 | + +同一用户同时是 top1 和点火人时,top1 池和点火人池继续叠加;如果该用户发射瞬间仍在房间,也会再拿 1 个在房随机奖励。 ## IM @@ -119,7 +122,7 @@ | `launch_delay_ms` | 点火到发射延迟 | | `broadcast_enabled` | 是否发飘屏 | | `broadcast_scope` | `region`、`global`、`none` | -| `reward_stack_policy` | 奖励叠加策略 | +| `reward_stack_policy` | 历史兼容字段;当前特殊身份奖励按上方规则叠加 | | `levels_json` | 5 级阈值、物料和奖励池 | | `gift_fuel_rules_json` | 礼物燃料规则 | diff --git a/services/room-service/internal/room/service/room_rocket.go b/services/room-service/internal/room/service/room_rocket.go index c7373ca1..f120986f 100644 --- a/services/room-service/internal/room/service/room_rocket.go +++ b/services/room-service/internal/room/service/room_rocket.go @@ -913,21 +913,16 @@ func sortedRocketOnlineUserIDs(users map[int64]*state.RoomUserState) []int64 { return userIDs } -func (s *Service) settleRoomRocketRewards(ctx context.Context, now time.Time, current *state.RoomState, launch state.RocketPendingLaunch, cfg RoomRocketConfig, levelCfg RoomRocketLevelConfig, inRoomUserIDs []int64) ([]state.RocketRewardGrant, error) { +func (s *Service) settleRoomRocketRewards(ctx context.Context, now time.Time, current *state.RoomState, launch state.RocketPendingLaunch, _ RoomRocketConfig, levelCfg RoomRocketLevelConfig, inRoomUserIDs []int64) ([]state.RocketRewardGrant, error) { if current == nil { return nil, nil } - rewards := make([]state.RocketRewardGrant, 0, len(inRoomUserIDs)+2) - claimed := make(map[int64]bool, len(inRoomUserIDs)+2) - addReward := func(role string, userID int64, pool []RoomRocketRewardItem) error { + rewards := make([]state.RocketRewardGrant, 0, len(inRoomUserIDs)+len(levelCfg.Top1Rewards)+len(levelCfg.IgniterRewards)) + addRewardItem := func(role string, userID int64, item RoomRocketRewardItem) error { if userID <= 0 { return nil } - if cfg.RewardStackPolicy == roomRocketRewardStackPriority && claimed[userID] { - return nil - } - item, ok := selectRoomRocketReward(launch.RocketID, role, userID, pool) - if !ok { + if item.ResourceGroupID <= 0 || item.Weight <= 0 { return nil } grantID, err := s.grantRoomRocketReward(ctx, now, current, launch.RocketID, role, userID, item) @@ -944,35 +939,35 @@ func (s *Service) settleRoomRocketRewards(ctx context.Context, now time.Time, cu GrantID: grantID, Status: roomRocketGrantStatus, }) - claimed[userID] = true + return nil + } + addRandomReward := func(role string, userID int64, pool []RoomRocketRewardItem) error { + item, ok := selectRoomRocketReward(launch.RocketID, role, userID, pool) + if !ok { + return nil + } + return addRewardItem(role, userID, item) + } + addAllRewards := func(role string, userID int64, pool []RoomRocketRewardItem) error { + for _, item := range pool { + if err := addRewardItem(role, userID, item); err != nil { + return err + } + } return nil } - if cfg.RewardStackPolicy == roomRocketRewardStackPriority { - // 去重策略下先发特殊身份奖励,再发普通在房奖励,避免 top1/点火人被普通池占位。 - if err := addReward(roomRocketRewardIgniter, launch.IgniterUserID, levelCfg.IgniterRewards); err != nil { - return nil, err - } - if err := addReward(roomRocketRewardTop1, launch.Top1UserID, levelCfg.Top1Rewards); err != nil { - return nil, err - } - for _, userID := range inRoomUserIDs { - if err := addReward(roomRocketRewardInRoom, userID, levelCfg.InRoomRewards); err != nil { - return nil, err - } - } - return rewards, nil - } - for _, userID := range inRoomUserIDs { - if err := addReward(roomRocketRewardInRoom, userID, levelCfg.InRoomRewards); err != nil { + // 在房奖励是大面积发放池,每个在线用户只按权重抽一个奖项,避免房间人数放大单次火箭成本。 + if err := addRandomReward(roomRocketRewardInRoom, userID, levelCfg.InRoomRewards); err != nil { return nil, err } } - if err := addReward(roomRocketRewardTop1, launch.Top1UserID, levelCfg.Top1Rewards); err != nil { + // top1 和点火人是确定身份权益:命中后发完整配置池;同一用户同时命中两个身份时继续叠加。 + if err := addAllRewards(roomRocketRewardTop1, launch.Top1UserID, levelCfg.Top1Rewards); err != nil { return nil, err } - if err := addReward(roomRocketRewardIgniter, launch.IgniterUserID, levelCfg.IgniterRewards); err != nil { + if err := addAllRewards(roomRocketRewardIgniter, launch.IgniterUserID, levelCfg.IgniterRewards); err != nil { return nil, err } return rewards, nil diff --git a/services/room-service/internal/room/service/room_rocket_test.go b/services/room-service/internal/room/service/room_rocket_test.go index a2212938..64aae985 100644 --- a/services/room-service/internal/room/service/room_rocket_test.go +++ b/services/room-service/internal/room/service/room_rocket_test.go @@ -433,6 +433,77 @@ func TestRoomRocketLaunchGrantsLockedTop1AndIgniterAfterLeave(t *testing.T) { } } +func TestRoomRocketLaunchStacksAllSpecialPoolsAndRandomInRoomReward(t *testing.T) { + ctx := context.Background() + repository := mysqltest.NewRepository(t) + now := &fixedRoomRocketClock{now: time.Date(2026, 5, 20, 13, 0, 0, 0, time.UTC)} + wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{ + {BillingReceiptId: "receipt-stack", GiftPointAdded: 100, HeatValue: 100, GiftTypeCode: "normal"}, + }} + svc := newRocketTestService(t, repository, wallet, now) + config := rocketConfigForTest(100, 1_000) + config.Levels[0].InRoomRewards = []roomservice.RoomRocketRewardItem{ + {RewardItemID: "level-1-in-room-a", ResourceGroupID: 1001, Weight: 1, DisplayName: "In Room A"}, + {RewardItemID: "level-1-in-room-b", ResourceGroupID: 1101, Weight: 1, DisplayName: "In Room B"}, + } + config.Levels[0].Top1Rewards = []roomservice.RoomRocketRewardItem{ + {RewardItemID: "level-1-top1-a", ResourceGroupID: 2001, Weight: 1, DisplayName: "Top 1 A"}, + {RewardItemID: "level-1-top1-b", ResourceGroupID: 2101, Weight: 1, DisplayName: "Top 1 B"}, + } + config.Levels[0].IgniterRewards = []roomservice.RoomRocketRewardItem{ + {RewardItemID: "level-1-igniter-a", ResourceGroupID: 3001, Weight: 1, DisplayName: "Igniter A"}, + {RewardItemID: "level-1-igniter-b", ResourceGroupID: 3101, Weight: 1, DisplayName: "Igniter B"}, + } + writeRocketConfig(t, ctx, repository, config) + + roomID := "room-rocket-stack" + ownerID := int64(401) + igniterID := int64(402) + createRocketRoom(t, ctx, svc, roomID, ownerID, 9003) + joinRocketRoom(t, ctx, svc, roomID, igniterID) + + if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{ + Meta: rocketMeta(roomID, igniterID, "stack"), + TargetType: "user", + TargetUserId: ownerID, + GiftId: "gift-stack", + GiftCount: 1, + }); err != nil { + t.Fatalf("ignite stack rocket failed: %v", err) + } + + now.now = now.now.Add(1100 * time.Millisecond) + if err := svc.SweepRoomRocketLaunchings(ctx); err != nil { + t.Fatalf("sweep stacked room rocket launches failed: %v", err) + } + + info, err := svc.GetRoomRocket(ctx, &roomv1.GetRoomRocketRequest{ + Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID}, + RoomId: roomID, + ViewerUserId: ownerID, + }) + if err != nil { + t.Fatalf("get stacked rocket after launch failed: %v", err) + } + rewards := info.GetRocket().GetState().GetLastRewards() + if len(rewards) != 6 { + t.Fatalf("launch must keep two in-room random grants plus all top1/igniter grants: %+v", rewards) + } + if countRocketRewards(rewards, ownerID, "in_room") != 1 { + t.Fatalf("owner must receive exactly one random in-room reward: %+v", rewards) + } + if countRocketRewards(rewards, igniterID, "in_room") != 1 { + t.Fatalf("top1 igniter must still receive exactly one random in-room reward: %+v", rewards) + } + if countRocketRewards(rewards, igniterID, "top1") != 2 || countRocketRewards(rewards, igniterID, "igniter") != 2 { + t.Fatalf("top1 igniter must receive every configured special reward: %+v", rewards) + } + if !hasRocketGrant(wallet.grants, igniterID, 2001) || !hasRocketGrant(wallet.grants, igniterID, 2101) || + !hasRocketGrant(wallet.grants, igniterID, 3001) || !hasRocketGrant(wallet.grants, igniterID, 3101) { + t.Fatalf("wallet must grant every top1 and igniter resource group: %+v", wallet.grants) + } +} + func TestRoomRocketUTCResetCancelsCrossDayIgnited(t *testing.T) { ctx := context.Background() repository := mysqltest.NewRepository(t) @@ -676,6 +747,16 @@ func hasRocketGrant(grants []*walletv1.GrantResourceGroupRequest, userID int64, return false } +func countRocketRewards(rewards []*roomv1.RoomRocketRewardGrant, userID int64, role string) int { + count := 0 + for _, reward := range rewards { + if reward.GetUserId() == userID && reward.GetRewardRole() == role { + count++ + } + } + return count +} + func rocketRewardUserIDs(rewards []*roomv1.RoomRocketRewardGrant) []int64 { userIDs := make([]int64, 0, len(rewards)) for _, reward := range rewards {