fix room rocket in-room reward limits
This commit is contained in:
parent
b55a01c0d6
commit
b025ac4dda
@ -774,8 +774,10 @@ type RoomRocketRewardItem struct {
|
||||
Weight int64 `protobuf:"varint,3,opt,name=weight,proto3" json:"weight,omitempty"`
|
||||
DisplayName string `protobuf:"bytes,4,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"`
|
||||
IconUrl string `protobuf:"bytes,5,opt,name=icon_url,json=iconUrl,proto3" json:"icon_url,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
// quantity 表示该奖项在一次火箭发射中最多发给多少个在房用户;为空或 0 时兼容旧配置按 1 份处理。
|
||||
Quantity int64 `protobuf:"varint,6,opt,name=quantity,proto3" json:"quantity,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RoomRocketRewardItem) Reset() {
|
||||
@ -843,6 +845,13 @@ func (x *RoomRocketRewardItem) GetIconUrl() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoomRocketRewardItem) GetQuantity() int64 {
|
||||
if x != nil {
|
||||
return x.Quantity
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// RoomRocketLevel 暴露单个等级火箭的物料、阈值和三类奖励池。
|
||||
type RoomRocketLevel struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@ -9520,13 +9529,14 @@ const file_proto_room_v1_room_proto_rawDesc = "" +
|
||||
"\rcreated_at_ms\x18\x11 \x01(\x03R\vcreatedAtMs\x122\n" +
|
||||
"\x15wallet_transaction_id\x18\x12 \x01(\tR\x13walletTransactionId\x12,\n" +
|
||||
"\x12coin_balance_after\x18\x13 \x01(\x03R\x10coinBalanceAfter\x12$\n" +
|
||||
"\x0etarget_user_id\x18\x14 \x01(\x03R\ftargetUserId\"\xbe\x01\n" +
|
||||
"\x0etarget_user_id\x18\x14 \x01(\x03R\ftargetUserId\"\xda\x01\n" +
|
||||
"\x14RoomRocketRewardItem\x12$\n" +
|
||||
"\x0ereward_item_id\x18\x01 \x01(\tR\frewardItemId\x12*\n" +
|
||||
"\x11resource_group_id\x18\x02 \x01(\x03R\x0fresourceGroupId\x12\x16\n" +
|
||||
"\x06weight\x18\x03 \x01(\x03R\x06weight\x12!\n" +
|
||||
"\fdisplay_name\x18\x04 \x01(\tR\vdisplayName\x12\x19\n" +
|
||||
"\bicon_url\x18\x05 \x01(\tR\aiconUrl\"\xd3\x03\n" +
|
||||
"\bicon_url\x18\x05 \x01(\tR\aiconUrl\x12\x1a\n" +
|
||||
"\bquantity\x18\x06 \x01(\x03R\bquantity\"\xd3\x03\n" +
|
||||
"\x0fRoomRocketLevel\x12\x14\n" +
|
||||
"\x05level\x18\x01 \x01(\x05R\x05level\x12%\n" +
|
||||
"\x0efuel_threshold\x18\x02 \x01(\x03R\rfuelThreshold\x12\x1b\n" +
|
||||
|
||||
@ -112,6 +112,8 @@ message RoomRocketRewardItem {
|
||||
int64 weight = 3;
|
||||
string display_name = 4;
|
||||
string icon_url = 5;
|
||||
// quantity 表示该奖项在一次火箭发射中最多发给多少个在房用户;为空或 0 时兼容旧配置按 1 份处理。
|
||||
int64 quantity = 6;
|
||||
}
|
||||
|
||||
// RoomRocketLevel 暴露单个等级火箭的物料、阈值和三类奖励池。
|
||||
|
||||
@ -127,6 +127,7 @@ type RoomRocketRewardItem struct {
|
||||
RewardItemID string
|
||||
ResourceGroupID int64
|
||||
Weight int64
|
||||
Quantity int64
|
||||
DisplayName string
|
||||
IconURL string
|
||||
}
|
||||
@ -623,6 +624,7 @@ func roomRocketRewardsFromProto(input []*roomv1.RoomRocketRewardItem) []RoomRock
|
||||
RewardItemID: reward.GetRewardItemId(),
|
||||
ResourceGroupID: reward.GetResourceGroupId(),
|
||||
Weight: reward.GetWeight(),
|
||||
Quantity: reward.GetQuantity(),
|
||||
DisplayName: reward.GetDisplayName(),
|
||||
IconURL: reward.GetIconUrl(),
|
||||
})
|
||||
@ -637,6 +639,7 @@ func roomRocketRewardsToProto(input []RoomRocketRewardItem) []*roomv1.RoomRocket
|
||||
RewardItemId: reward.RewardItemID,
|
||||
ResourceGroupId: reward.ResourceGroupID,
|
||||
Weight: reward.Weight,
|
||||
Quantity: reward.Quantity,
|
||||
DisplayName: reward.DisplayName,
|
||||
IconUrl: reward.IconURL,
|
||||
})
|
||||
|
||||
@ -63,6 +63,7 @@ type RoomRocketRewardItem struct {
|
||||
RewardItemID string `json:"rewardItemId"`
|
||||
ResourceGroupID int64 `json:"resourceGroupId"`
|
||||
Weight int64 `json:"weight"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
DisplayName string `json:"displayName"`
|
||||
IconURL string `json:"iconUrl"`
|
||||
}
|
||||
@ -246,7 +247,7 @@ func normalizeRewardPool(level int32, role string, rewards []RoomRocketRewardIte
|
||||
reward.RewardItemID = strings.TrimSpace(reward.RewardItemID)
|
||||
reward.DisplayName = strings.TrimSpace(reward.DisplayName)
|
||||
reward.IconURL = strings.TrimSpace(reward.IconURL)
|
||||
if reward.RewardItemID == "" && reward.ResourceGroupID == 0 && reward.Weight == 0 && reward.DisplayName == "" && reward.IconURL == "" {
|
||||
if reward.RewardItemID == "" && reward.ResourceGroupID == 0 && reward.Weight == 0 && reward.Quantity == 0 && reward.DisplayName == "" && reward.IconURL == "" {
|
||||
continue
|
||||
}
|
||||
if reward.ResourceGroupID <= 0 {
|
||||
@ -255,6 +256,10 @@ func normalizeRewardPool(level int32, role string, rewards []RoomRocketRewardIte
|
||||
if reward.Weight <= 0 {
|
||||
return nil, fmt.Errorf("%w: 第 %d 级奖励权重必须大于 0", ErrInvalidArgument, level)
|
||||
}
|
||||
if reward.Quantity <= 0 {
|
||||
// 历史配置没有 quantity 字段;后台读写都按 1 份兼容,避免旧配置打开页面即失败。
|
||||
reward.Quantity = 1
|
||||
}
|
||||
if reward.RewardItemID == "" {
|
||||
reward.RewardItemID = fmt.Sprintf("level_%d_%s_%d", level, role, index+1)
|
||||
}
|
||||
@ -382,6 +387,7 @@ func rewardsFromClient(input []roomclient.RoomRocketRewardItem) []RoomRocketRewa
|
||||
RewardItemID: reward.RewardItemID,
|
||||
ResourceGroupID: reward.ResourceGroupID,
|
||||
Weight: reward.Weight,
|
||||
Quantity: reward.Quantity,
|
||||
DisplayName: reward.DisplayName,
|
||||
IconURL: reward.IconURL,
|
||||
})
|
||||
@ -396,6 +402,7 @@ func rewardsToClient(input []RoomRocketRewardItem) []roomclient.RoomRocketReward
|
||||
RewardItemID: reward.RewardItemID,
|
||||
ResourceGroupID: reward.ResourceGroupID,
|
||||
Weight: reward.Weight,
|
||||
Quantity: reward.Quantity,
|
||||
DisplayName: reward.DisplayName,
|
||||
IconURL: reward.IconURL,
|
||||
})
|
||||
|
||||
@ -42,6 +42,22 @@ func TestNormalizeConfigDefaultsAndSortsLevels(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeConfigKeepsRewardQuantityAndDefaultsOldItems(t *testing.T) {
|
||||
config := defaultConfig("lalu")
|
||||
config.Levels[0].InRoomRewards = []RoomRocketRewardItem{
|
||||
{RewardItemID: "coin", ResourceGroupID: 1001, Weight: 100, Quantity: 2},
|
||||
{RewardItemID: "mic-wave", ResourceGroupID: 1002, Weight: 100},
|
||||
}
|
||||
normalized, err := normalizeConfig(config)
|
||||
if err != nil {
|
||||
t.Fatalf("normalizeConfig failed: %v", err)
|
||||
}
|
||||
rewards := normalized.Levels[0].InRoomRewards
|
||||
if rewards[0].Quantity != 2 || rewards[1].Quantity != 1 {
|
||||
t.Fatalf("reward quantity must be preserved and defaulted: %+v", rewards)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfigKeepsOperationalDefaults(t *testing.T) {
|
||||
config, err := normalizeConfig(defaultConfig("lalu"))
|
||||
if err != nil {
|
||||
|
||||
@ -113,6 +113,7 @@ func roomRocketRewardsFromProto(input []*roomv1.RoomRocketRewardItem) []RoomRock
|
||||
RewardItemID: reward.GetRewardItemId(),
|
||||
ResourceGroupID: reward.GetResourceGroupId(),
|
||||
Weight: reward.GetWeight(),
|
||||
Quantity: reward.GetQuantity(),
|
||||
DisplayName: reward.GetDisplayName(),
|
||||
IconURL: reward.GetIconUrl(),
|
||||
})
|
||||
|
||||
@ -294,6 +294,7 @@ type RoomRocketRewardItem struct {
|
||||
RewardItemID string `json:"rewardItemId"`
|
||||
ResourceGroupID int64 `json:"resourceGroupId"`
|
||||
Weight int64 `json:"weight"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
DisplayName string `json:"displayName"`
|
||||
IconURL string `json:"iconUrl"`
|
||||
}
|
||||
|
||||
@ -195,6 +195,10 @@ func normalizeRoomRocketRewardPool(input []RoomRocketRewardItem) []RoomRocketRew
|
||||
// 后台保存层会拒绝非法奖励;运行时跳过历史脏项,避免一个奖励池配置污染送礼主链路。
|
||||
continue
|
||||
}
|
||||
if reward.Quantity <= 0 {
|
||||
// 旧配置没有 quantity 字段;按 1 份兼容,避免上线后历史火箭奖励池被当成 0 份直接停发。
|
||||
reward.Quantity = 1
|
||||
}
|
||||
out = append(out, reward)
|
||||
}
|
||||
return out
|
||||
@ -340,6 +344,7 @@ func roomRocketRewardsToProto(input []RoomRocketRewardItem) []*roomv1.RoomRocket
|
||||
RewardItemId: reward.RewardItemID,
|
||||
ResourceGroupId: reward.ResourceGroupID,
|
||||
Weight: reward.Weight,
|
||||
Quantity: reward.Quantity,
|
||||
DisplayName: reward.DisplayName,
|
||||
IconUrl: reward.IconURL,
|
||||
})
|
||||
@ -1115,6 +1120,9 @@ func validRoomRocketRewardPool(pool []RoomRocketRewardItem) []RoomRocketRewardIt
|
||||
if item.Weight <= 0 || item.ResourceGroupID <= 0 {
|
||||
continue
|
||||
}
|
||||
if item.Quantity <= 0 {
|
||||
item.Quantity = 1
|
||||
}
|
||||
valid = append(valid, item)
|
||||
}
|
||||
return valid
|
||||
@ -1129,18 +1137,16 @@ func selectRoomRocketRewardsForUsers(rocketID string, role string, userIDs []int
|
||||
if len(basePool) == 0 {
|
||||
return assignments
|
||||
}
|
||||
available := append([]RoomRocketRewardItem(nil), basePool...)
|
||||
cycle := 0
|
||||
available := expandRoomRocketRewardSlots(basePool)
|
||||
for index, userID := range userIDs {
|
||||
if userID <= 0 {
|
||||
continue
|
||||
}
|
||||
if len(available) == 0 {
|
||||
// 一轮内每个配置项最多发一次;房间人数超过奖池大小时再开启下一轮,让大房间仍可继续按配置重复分配。
|
||||
available = append([]RoomRocketRewardItem(nil), basePool...)
|
||||
cycle++
|
||||
// 在房奖励数量是单次发射的硬上限;配置总数发完后,其余在线用户本次拿空,不生成 grant 和 IM 弹窗。
|
||||
break
|
||||
}
|
||||
selectedIndex, ok := selectRoomRocketRewardIndex(rocketID, role, userID, available, fmt.Sprintf("cycle:%d:index:%d", cycle, index))
|
||||
selectedIndex, ok := selectRoomRocketRewardIndex(rocketID, role, userID, available, fmt.Sprintf("limited:index:%d", index))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
@ -1150,12 +1156,20 @@ func selectRoomRocketRewardsForUsers(rocketID string, role string, userIDs []int
|
||||
return assignments
|
||||
}
|
||||
|
||||
func selectRoomRocketReward(rocketID string, role string, userID int64, pool []RoomRocketRewardItem) (RoomRocketRewardItem, bool) {
|
||||
index, ok := selectRoomRocketRewardIndex(rocketID, role, userID, pool, "")
|
||||
if !ok {
|
||||
return RoomRocketRewardItem{}, false
|
||||
func expandRoomRocketRewardSlots(pool []RoomRocketRewardItem) []RoomRocketRewardItem {
|
||||
total := 0
|
||||
for _, item := range pool {
|
||||
if item.Quantity > 0 {
|
||||
total += int(item.Quantity)
|
||||
}
|
||||
}
|
||||
return pool[index], true
|
||||
slots := make([]RoomRocketRewardItem, 0, total)
|
||||
for _, item := range pool {
|
||||
for count := int64(0); count < item.Quantity; count++ {
|
||||
slots = append(slots, item)
|
||||
}
|
||||
}
|
||||
return slots
|
||||
}
|
||||
|
||||
func selectRoomRocketRewardIndex(rocketID string, role string, userID int64, pool []RoomRocketRewardItem, salt string) (int, bool) {
|
||||
|
||||
@ -578,6 +578,72 @@ func TestRoomRocketLaunchStacksAllSpecialPoolsAndRandomInRoomReward(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoomRocketInRoomRewardQuantityCapsWinners(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
now := &fixedRoomRocketClock{now: time.Date(2026, 5, 20, 14, 0, 0, 0, time.UTC)}
|
||||
wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{
|
||||
{BillingReceiptId: "receipt-quantity", 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-coin", ResourceGroupID: 1001, Weight: 1, Quantity: 2, DisplayName: "Coin"},
|
||||
{RewardItemID: "level-1-in-room-mic", ResourceGroupID: 1101, Weight: 1, Quantity: 3, DisplayName: "Mic Wave"},
|
||||
}
|
||||
config.Levels[0].Top1Rewards = nil
|
||||
config.Levels[0].IgniterRewards = nil
|
||||
writeRocketConfig(t, ctx, repository, config)
|
||||
|
||||
roomID := "room-rocket-quantity"
|
||||
ownerID := int64(501)
|
||||
igniterID := int64(502)
|
||||
createRocketRoom(t, ctx, svc, roomID, ownerID, 9004)
|
||||
for userID := igniterID; userID <= 507; userID++ {
|
||||
joinRocketRoom(t, ctx, svc, roomID, userID)
|
||||
}
|
||||
|
||||
if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
|
||||
Meta: rocketMeta(roomID, igniterID, "quantity"),
|
||||
TargetType: "user",
|
||||
TargetUserId: ownerID,
|
||||
GiftId: "gift-quantity",
|
||||
GiftCount: 1,
|
||||
}); err != nil {
|
||||
t.Fatalf("ignite quantity rocket failed: %v", err)
|
||||
}
|
||||
|
||||
now.now = now.now.Add(1100 * time.Millisecond)
|
||||
if err := svc.SweepRoomRocketLaunchings(ctx); err != nil {
|
||||
t.Fatalf("sweep quantity 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 quantity rocket after launch failed: %v", err)
|
||||
}
|
||||
rewards := info.GetRocket().GetState().GetLastRewards()
|
||||
if countRocketRewardsByRole(rewards, "in_room") != 5 {
|
||||
t.Fatalf("in-room rewards must stop at configured quantity 5, got %+v", rewards)
|
||||
}
|
||||
if countRocketRewardsByRoleAndGroup(rewards, "in_room", 1001) != 2 {
|
||||
t.Fatalf("coin resource group must be granted exactly 2 times: %+v", rewards)
|
||||
}
|
||||
if countRocketRewardsByRoleAndGroup(rewards, "in_room", 1101) != 3 {
|
||||
t.Fatalf("mic wave resource group must be granted exactly 3 times: %+v", rewards)
|
||||
}
|
||||
if len(wallet.grants) != 5 {
|
||||
t.Fatalf("wallet must only receive 5 in-room grant calls, got %+v", wallet.grants)
|
||||
}
|
||||
if len(rocketRewardUserIDs(rewards)) != 5 {
|
||||
t.Fatalf("only 5 users should have reward rows; remaining online users receive empty: %+v", rewards)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoomRocketUTCResetCancelsCrossDayIgnited(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
@ -841,6 +907,16 @@ func countRocketRewardsByRoleAndGroup(rewards []*roomv1.RoomRocketRewardGrant, r
|
||||
return count
|
||||
}
|
||||
|
||||
func countRocketRewardsByRole(rewards []*roomv1.RoomRocketRewardGrant, role string) int {
|
||||
count := 0
|
||||
for _, reward := range rewards {
|
||||
if reward.GetRewardRole() == role {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func rocketRewardUserIDs(rewards []*roomv1.RoomRocketRewardGrant) []int64 {
|
||||
userIDs := make([]int64, 0, len(rewards))
|
||||
for _, reward := range rewards {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user