fix room rocket top1 contribution tracking
This commit is contained in:
parent
70c928c38b
commit
6fc9499a4b
File diff suppressed because it is too large
Load Diff
@ -139,6 +139,13 @@ message RoomRocketRewardGrant {
|
||||
string status = 8;
|
||||
}
|
||||
|
||||
// RoomRocketContribution 是当前火箭等级内的实际加燃料贡献累计。
|
||||
message RoomRocketContribution {
|
||||
int64 user_id = 1;
|
||||
int64 fuel = 2;
|
||||
int64 updated_at_ms = 3;
|
||||
}
|
||||
|
||||
// RoomRocketPendingLaunch 是已经点火、等待延迟发射的单枚火箭。
|
||||
message RoomRocketPendingLaunch {
|
||||
string rocket_id = 1;
|
||||
@ -169,6 +176,7 @@ message RoomRocketState {
|
||||
int64 config_version = 12;
|
||||
repeated RoomRocketRewardGrant last_rewards = 13;
|
||||
repeated RoomRocketPendingLaunch pending_launches = 14;
|
||||
repeated RoomRocketContribution current_contributions = 15;
|
||||
}
|
||||
|
||||
// RoomRocketInfo 是 App 初始化火箭 UI 的完整读模型。
|
||||
|
||||
@ -457,6 +457,8 @@ type SendGift struct {
|
||||
RocketConfigVersion int64 `json:"rocket_config_version,omitempty"`
|
||||
// RocketPendingLaunches 是送礼提交后的待发射队列,恢复时不能只保存当前进度。
|
||||
RocketPendingLaunches []RocketPendingLaunch `json:"rocket_pending_launches,omitempty"`
|
||||
// RocketCurrentContributions 保存当前等级真实加燃料贡献,恢复时用于继续按本级贡献锁 top1。
|
||||
RocketCurrentContributions []RocketContribution `json:"rocket_current_contributions,omitempty"`
|
||||
}
|
||||
|
||||
// Type 返回命令类型。
|
||||
@ -474,6 +476,13 @@ type RocketRewardGrant struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// RocketContribution 记录当前火箭等级内的实际燃料贡献快照。
|
||||
type RocketContribution struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Fuel int64 `json:"fuel"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||
}
|
||||
|
||||
// RocketPendingLaunch 记录一枚已点火火箭的延迟发射快照。
|
||||
type RocketPendingLaunch struct {
|
||||
RocketID string `json:"rocket_id"`
|
||||
@ -507,6 +516,7 @@ type LaunchRoomRocket struct {
|
||||
IgniterUserID int64 `json:"igniter_user_id,omitempty"`
|
||||
LaunchedAtMS int64 `json:"launched_at_ms"`
|
||||
InRoomUserIDs []int64 `json:"in_room_user_ids,omitempty"`
|
||||
CurrentContributions []RocketContribution `json:"current_contributions,omitempty"`
|
||||
Rewards []RocketRewardGrant `json:"rewards,omitempty"`
|
||||
PendingLaunches []RocketPendingLaunch `json:"pending_launches,omitempty"`
|
||||
}
|
||||
@ -564,6 +574,7 @@ func IdempotencyPayload(payload []byte) ([]byte, error) {
|
||||
delete(values, "rocket_id")
|
||||
delete(values, "rocket_config_version")
|
||||
delete(values, "rocket_pending_launches")
|
||||
delete(values, "rocket_current_contributions")
|
||||
// LaunchRoomRocket 的以下字段是 worker 在发射时结算出的结果,不属于触发发射的幂等语义。
|
||||
delete(values, "current_level")
|
||||
delete(values, "current_fuel")
|
||||
@ -576,6 +587,7 @@ func IdempotencyPayload(payload []byte) ([]byte, error) {
|
||||
delete(values, "igniter_user_id")
|
||||
delete(values, "launched_at_ms")
|
||||
delete(values, "in_room_user_ids")
|
||||
delete(values, "current_contributions")
|
||||
delete(values, "rewards")
|
||||
delete(values, "pending_launches")
|
||||
// SetRoomPassword 的哈希带随机盐;通用 payload 比较先去掉哈希,service 层再用 transient 明文和已存 bcrypt 哈希确认是否同一密码。
|
||||
|
||||
@ -443,36 +443,53 @@ func sendGiftTargetGiftValues(cmd *command.SendGift) map[int64]int64 {
|
||||
|
||||
func rocketStateFromSettledGift(cmd *command.SendGift) *state.RocketState {
|
||||
return &state.RocketState{
|
||||
CurrentLevel: cmd.RocketLevel,
|
||||
CurrentFuel: cmd.RocketFuel,
|
||||
FuelThreshold: cmd.RocketFuelThreshold,
|
||||
Status: cmd.RocketStatus,
|
||||
IgnitedAtMS: cmd.RocketIgnitedAtMS,
|
||||
LaunchAtMS: cmd.RocketLaunchAtMS,
|
||||
ResetAtMS: cmd.RocketResetAtMS,
|
||||
Top1UserID: cmd.RocketTop1UserID,
|
||||
IgniterUserID: cmd.RocketIgniterUserID,
|
||||
RocketID: cmd.RocketID,
|
||||
ConfigVersion: cmd.RocketConfigVersion,
|
||||
PendingLaunches: rocketPendingLaunchesFromCommand(cmd.RocketPendingLaunches),
|
||||
CurrentLevel: cmd.RocketLevel,
|
||||
CurrentFuel: cmd.RocketFuel,
|
||||
FuelThreshold: cmd.RocketFuelThreshold,
|
||||
Status: cmd.RocketStatus,
|
||||
IgnitedAtMS: cmd.RocketIgnitedAtMS,
|
||||
LaunchAtMS: cmd.RocketLaunchAtMS,
|
||||
ResetAtMS: cmd.RocketResetAtMS,
|
||||
Top1UserID: cmd.RocketTop1UserID,
|
||||
IgniterUserID: cmd.RocketIgniterUserID,
|
||||
RocketID: cmd.RocketID,
|
||||
ConfigVersion: cmd.RocketConfigVersion,
|
||||
PendingLaunches: rocketPendingLaunchesFromCommand(cmd.RocketPendingLaunches),
|
||||
CurrentContributions: rocketContributionsFromCommand(cmd.RocketCurrentContributions),
|
||||
}
|
||||
}
|
||||
|
||||
func rocketStateFromLaunchCommand(cmd *command.LaunchRoomRocket) *state.RocketState {
|
||||
return &state.RocketState{
|
||||
CurrentLevel: cmd.CurrentLevel,
|
||||
CurrentFuel: cmd.CurrentFuel,
|
||||
FuelThreshold: cmd.CurrentFuelThreshold,
|
||||
Status: cmd.CurrentStatus,
|
||||
LaunchedAtMS: cmd.LaunchedAtMS,
|
||||
ResetAtMS: cmd.ResetAtMS,
|
||||
RocketID: cmd.CurrentRocketID,
|
||||
ConfigVersion: cmd.ConfigVersion,
|
||||
PendingLaunches: rocketPendingLaunchesFromCommand(cmd.PendingLaunches),
|
||||
LastRewards: rocketRewardsFromCommand(cmd.Rewards),
|
||||
CurrentLevel: cmd.CurrentLevel,
|
||||
CurrentFuel: cmd.CurrentFuel,
|
||||
FuelThreshold: cmd.CurrentFuelThreshold,
|
||||
Status: cmd.CurrentStatus,
|
||||
LaunchedAtMS: cmd.LaunchedAtMS,
|
||||
ResetAtMS: cmd.ResetAtMS,
|
||||
RocketID: cmd.CurrentRocketID,
|
||||
ConfigVersion: cmd.ConfigVersion,
|
||||
PendingLaunches: rocketPendingLaunchesFromCommand(cmd.PendingLaunches),
|
||||
CurrentContributions: rocketContributionsFromCommand(cmd.CurrentContributions),
|
||||
LastRewards: rocketRewardsFromCommand(cmd.Rewards),
|
||||
}
|
||||
}
|
||||
|
||||
func rocketContributionsFromCommand(input []command.RocketContribution) []state.RocketContribution {
|
||||
contributions := make([]state.RocketContribution, 0, len(input))
|
||||
for _, contribution := range input {
|
||||
if contribution.UserID <= 0 || contribution.Fuel <= 0 {
|
||||
continue
|
||||
}
|
||||
contributions = append(contributions, state.RocketContribution{
|
||||
UserID: contribution.UserID,
|
||||
Fuel: contribution.Fuel,
|
||||
UpdatedAtMS: contribution.UpdatedAtMS,
|
||||
})
|
||||
}
|
||||
return contributions
|
||||
}
|
||||
|
||||
func rocketPendingLaunchExists(input []state.RocketPendingLaunch, rocketID string, level int32) bool {
|
||||
for _, launch := range input {
|
||||
if launch.RocketID == rocketID && launch.Level == level {
|
||||
|
||||
@ -243,20 +243,21 @@ func roomRocketProtoStateForView(current *roomv1.RoomRocketState, config RoomRoc
|
||||
internal := state.RocketState{}
|
||||
if current != nil {
|
||||
internal = state.RocketState{
|
||||
CurrentLevel: current.GetCurrentLevel(),
|
||||
CurrentFuel: current.GetCurrentFuel(),
|
||||
FuelThreshold: current.GetFuelThreshold(),
|
||||
Status: current.GetStatus(),
|
||||
IgnitedAtMS: current.GetIgnitedAtMs(),
|
||||
LaunchAtMS: current.GetLaunchAtMs(),
|
||||
LaunchedAtMS: current.GetLaunchedAtMs(),
|
||||
ResetAtMS: current.GetResetAtMs(),
|
||||
Top1UserID: current.GetTop1UserId(),
|
||||
IgniterUserID: current.GetIgniterUserId(),
|
||||
RocketID: current.GetRocketId(),
|
||||
ConfigVersion: current.GetConfigVersion(),
|
||||
PendingLaunches: statePendingLaunchesFromProto(current.GetPendingLaunches()),
|
||||
LastRewards: stateRewardsFromProto(current.GetLastRewards()),
|
||||
CurrentLevel: current.GetCurrentLevel(),
|
||||
CurrentFuel: current.GetCurrentFuel(),
|
||||
FuelThreshold: current.GetFuelThreshold(),
|
||||
Status: current.GetStatus(),
|
||||
IgnitedAtMS: current.GetIgnitedAtMs(),
|
||||
LaunchAtMS: current.GetLaunchAtMs(),
|
||||
LaunchedAtMS: current.GetLaunchedAtMs(),
|
||||
ResetAtMS: current.GetResetAtMs(),
|
||||
Top1UserID: current.GetTop1UserId(),
|
||||
IgniterUserID: current.GetIgniterUserId(),
|
||||
RocketID: current.GetRocketId(),
|
||||
ConfigVersion: current.GetConfigVersion(),
|
||||
PendingLaunches: statePendingLaunchesFromProto(current.GetPendingLaunches()),
|
||||
CurrentContributions: stateContributionsFromProto(current.GetCurrentContributions()),
|
||||
LastRewards: stateRewardsFromProto(current.GetLastRewards()),
|
||||
}
|
||||
}
|
||||
normalized := rocketStateForNow(&internal, config, now, false)
|
||||
@ -299,6 +300,21 @@ func statePendingLaunchesFromProto(input []*roomv1.RoomRocketPendingLaunch) []st
|
||||
return out
|
||||
}
|
||||
|
||||
func stateContributionsFromProto(input []*roomv1.RoomRocketContribution) []state.RocketContribution {
|
||||
out := make([]state.RocketContribution, 0, len(input))
|
||||
for _, contribution := range input {
|
||||
if contribution.GetUserId() <= 0 || contribution.GetFuel() <= 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, state.RocketContribution{
|
||||
UserID: contribution.GetUserId(),
|
||||
Fuel: contribution.GetFuel(),
|
||||
UpdatedAtMS: contribution.GetUpdatedAtMs(),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func roomRocketLevelsToProto(levels []RoomRocketLevelConfig) []*roomv1.RoomRocketLevel {
|
||||
out := make([]*roomv1.RoomRocketLevel, 0, len(levels))
|
||||
for _, level := range levels {
|
||||
@ -367,11 +383,17 @@ func (s *Service) applyRoomRocketGift(now time.Time, current *state.RoomState, c
|
||||
overflowFuel = addedFuel - effectiveFuel
|
||||
if effectiveFuel > 0 {
|
||||
rocket.CurrentFuel += effectiveFuel
|
||||
rocket.CurrentContributions = addRocketContribution(rocket.CurrentContributions, cmd.ActorUserID(), effectiveFuel, nowMS)
|
||||
progressFuel = rocket.CurrentFuel
|
||||
result.touched = true
|
||||
}
|
||||
if rocket.CurrentFuel >= rocket.FuelThreshold {
|
||||
levelCfg := roomRocketLevelByNumber(cfg, rocket.CurrentLevel)
|
||||
top1UserID := firstRocketContributionUserID(rocket.CurrentContributions)
|
||||
if top1UserID <= 0 {
|
||||
// 理论上 effectiveFuel > 0 后一定有当前等级贡献;兜底用点火人,避免历史脏状态把 top1 奖励发给 0。
|
||||
top1UserID = cmd.ActorUserID()
|
||||
}
|
||||
pending := state.RocketPendingLaunch{
|
||||
RocketID: rocket.RocketID,
|
||||
Level: rocket.CurrentLevel,
|
||||
@ -379,7 +401,7 @@ func (s *Service) applyRoomRocketGift(now time.Time, current *state.RoomState, c
|
||||
IgnitedAtMS: nowMS,
|
||||
LaunchAtMS: nowMS + cfg.LaunchDelayMS,
|
||||
ResetAtMS: rocket.ResetAtMS,
|
||||
Top1UserID: firstRankUserID(current.GiftRank),
|
||||
Top1UserID: top1UserID,
|
||||
IgniterUserID: cmd.ActorUserID(),
|
||||
ConfigVersion: cfg.ConfigVersion,
|
||||
CoverURL: levelCfg.CoverURL,
|
||||
@ -420,10 +442,12 @@ func (s *Service) applyRoomRocketGift(now time.Time, current *state.RoomState, c
|
||||
rocket.FuelThreshold = nextLevelCfg.FuelThreshold
|
||||
rocket.Status = state.RocketStatusCharging
|
||||
rocket.RocketID = idgen.New("room_rocket")
|
||||
rocket.CurrentContributions = nil
|
||||
} else {
|
||||
// 5 级是当天最高等级;点火后不再生成第 6 级,UTC 日界统一重置回 1 级。
|
||||
rocket.Status = state.RocketStatusCompleted
|
||||
rocket.RocketID = ""
|
||||
rocket.CurrentContributions = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -468,6 +492,7 @@ func (s *Service) applyRoomRocketGift(now time.Time, current *state.RoomState, c
|
||||
settled.RocketID = rocket.RocketID
|
||||
settled.RocketConfigVersion = rocket.ConfigVersion
|
||||
settled.RocketPendingLaunches = rocketPendingLaunchesToCommand(rocket.PendingLaunches)
|
||||
settled.RocketCurrentContributions = rocketContributionsToCommand(rocket.CurrentContributions)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@ -634,11 +659,61 @@ func roomRocketFuelByRule(base int64, rule GiftFuelRuleConfig) int64 {
|
||||
return fuel
|
||||
}
|
||||
|
||||
func firstRankUserID(items []state.RankItem) int64 {
|
||||
if len(items) == 0 {
|
||||
func addRocketContribution(input []state.RocketContribution, userID int64, fuel int64, updatedAtMS int64) []state.RocketContribution {
|
||||
if userID <= 0 || fuel <= 0 {
|
||||
return normalizeRocketContributions(input)
|
||||
}
|
||||
out := append([]state.RocketContribution(nil), input...)
|
||||
for index := range out {
|
||||
if out[index].UserID == userID {
|
||||
// top1 只看当前火箭等级真实写入燃料的累计;溢出部分不会带到下一等级,也不会影响当前等级排名。
|
||||
out[index].Fuel += fuel
|
||||
out[index].UpdatedAtMS = updatedAtMS
|
||||
return normalizeRocketContributions(out)
|
||||
}
|
||||
}
|
||||
out = append(out, state.RocketContribution{UserID: userID, Fuel: fuel, UpdatedAtMS: updatedAtMS})
|
||||
return normalizeRocketContributions(out)
|
||||
}
|
||||
|
||||
func normalizeRocketContributions(input []state.RocketContribution) []state.RocketContribution {
|
||||
out := make([]state.RocketContribution, 0, len(input))
|
||||
for _, item := range input {
|
||||
if item.UserID <= 0 || item.Fuel <= 0 {
|
||||
continue
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
slices.SortFunc(out, func(left, right state.RocketContribution) int {
|
||||
if left.Fuel != right.Fuel {
|
||||
if left.Fuel > right.Fuel {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
if left.UpdatedAtMS != right.UpdatedAtMS {
|
||||
if left.UpdatedAtMS > right.UpdatedAtMS {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
if left.UserID < right.UserID {
|
||||
return -1
|
||||
}
|
||||
if left.UserID > right.UserID {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func firstRocketContributionUserID(items []state.RocketContribution) int64 {
|
||||
normalized := normalizeRocketContributions(items)
|
||||
if len(normalized) == 0 {
|
||||
return 0
|
||||
}
|
||||
return items[0].UserID
|
||||
return normalized[0].UserID
|
||||
}
|
||||
|
||||
func minInt64(left int64, right int64) int64 {
|
||||
@ -680,21 +755,30 @@ func rocketStateToRoomProto(input *state.RocketState) *roomv1.RoomRocketState {
|
||||
CoverUrl: launch.CoverURL,
|
||||
})
|
||||
}
|
||||
contributions := make([]*roomv1.RoomRocketContribution, 0, len(input.CurrentContributions))
|
||||
for _, contribution := range normalizeRocketContributions(input.CurrentContributions) {
|
||||
contributions = append(contributions, &roomv1.RoomRocketContribution{
|
||||
UserId: contribution.UserID,
|
||||
Fuel: contribution.Fuel,
|
||||
UpdatedAtMs: contribution.UpdatedAtMS,
|
||||
})
|
||||
}
|
||||
return &roomv1.RoomRocketState{
|
||||
CurrentLevel: input.CurrentLevel,
|
||||
CurrentFuel: input.CurrentFuel,
|
||||
FuelThreshold: input.FuelThreshold,
|
||||
Status: input.Status,
|
||||
IgnitedAtMs: input.IgnitedAtMS,
|
||||
LaunchAtMs: input.LaunchAtMS,
|
||||
LaunchedAtMs: input.LaunchedAtMS,
|
||||
ResetAtMs: input.ResetAtMS,
|
||||
Top1UserId: input.Top1UserID,
|
||||
IgniterUserId: input.IgniterUserID,
|
||||
RocketId: input.RocketID,
|
||||
ConfigVersion: input.ConfigVersion,
|
||||
LastRewards: rewards,
|
||||
PendingLaunches: pending,
|
||||
CurrentLevel: input.CurrentLevel,
|
||||
CurrentFuel: input.CurrentFuel,
|
||||
FuelThreshold: input.FuelThreshold,
|
||||
Status: input.Status,
|
||||
IgnitedAtMs: input.IgnitedAtMS,
|
||||
LaunchAtMs: input.LaunchAtMS,
|
||||
LaunchedAtMs: input.LaunchedAtMS,
|
||||
ResetAtMs: input.ResetAtMS,
|
||||
Top1UserId: input.Top1UserID,
|
||||
IgniterUserId: input.IgniterUserID,
|
||||
RocketId: input.RocketID,
|
||||
ConfigVersion: input.ConfigVersion,
|
||||
LastRewards: rewards,
|
||||
PendingLaunches: pending,
|
||||
CurrentContributions: contributions,
|
||||
}
|
||||
}
|
||||
|
||||
@ -863,6 +947,7 @@ func (s *Service) launchRoomRocket(ctx context.Context, cmd command.LaunchRoomRo
|
||||
settledCommand.IgniterUserID = pending.IgniterUserID
|
||||
settledCommand.LaunchedAtMS = nowMS
|
||||
settledCommand.InRoomUserIDs = inRoomUserIDs
|
||||
settledCommand.CurrentContributions = rocketContributionsToCommand(current.Rocket.CurrentContributions)
|
||||
settledCommand.Rewards = rocketRewardsToCommand(rewards)
|
||||
settledCommand.PendingLaunches = rocketPendingLaunchesToCommand(pendingAfter)
|
||||
commandPayload, err := command.Serialize(settledCommand)
|
||||
@ -1073,6 +1158,19 @@ func rocketRewardsToCommand(input []state.RocketRewardGrant) []command.RocketRew
|
||||
return out
|
||||
}
|
||||
|
||||
func rocketContributionsToCommand(input []state.RocketContribution) []command.RocketContribution {
|
||||
normalized := normalizeRocketContributions(input)
|
||||
out := make([]command.RocketContribution, 0, len(normalized))
|
||||
for _, contribution := range normalized {
|
||||
out = append(out, command.RocketContribution{
|
||||
UserID: contribution.UserID,
|
||||
Fuel: contribution.Fuel,
|
||||
UpdatedAtMS: contribution.UpdatedAtMS,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func rocketPendingLaunchesToCommand(input []state.RocketPendingLaunch) []command.RocketPendingLaunch {
|
||||
out := make([]command.RocketPendingLaunch, 0, len(input))
|
||||
for _, launch := range input {
|
||||
|
||||
@ -436,6 +436,74 @@ func TestRoomRocketLaunchGrantsLockedTop1AndIgniterAfterLeave(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoomRocketTop1UsesCurrentLevelFuelContributionNotRoomGiftRank(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
now := &fixedRoomRocketClock{now: time.Date(2026, 5, 20, 12, 0, 0, 0, time.UTC)}
|
||||
wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{
|
||||
{BillingReceiptId: "receipt-old-rank", GiftPointAdded: 10, HeatValue: 10, GiftTypeCode: "normal"},
|
||||
{BillingReceiptId: "receipt-current-level", GiftPointAdded: 90, HeatValue: 90, GiftTypeCode: "normal"},
|
||||
}}
|
||||
svc := newRocketTestService(t, repository, wallet, now)
|
||||
writeRocketConfig(t, ctx, repository, rocketConfigForTest(100, 1_000))
|
||||
|
||||
roomID := "room-rocket-current-level-top1"
|
||||
ownerID := int64(501)
|
||||
igniterID := int64(502)
|
||||
createRocketRoom(t, ctx, svc, roomID, ownerID, 9004)
|
||||
joinRocketRoom(t, ctx, svc, roomID, igniterID)
|
||||
|
||||
if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
|
||||
Meta: rocketMeta(roomID, ownerID, "old-room-rank"),
|
||||
TargetType: "user",
|
||||
TargetUserId: igniterID,
|
||||
GiftId: "gift-old-room-rank",
|
||||
GiftCount: 1,
|
||||
}); err != nil {
|
||||
t.Fatalf("seed old room gift rank failed: %v", err)
|
||||
}
|
||||
if _, err := svc.LeaveRoom(ctx, &roomv1.LeaveRoomRequest{Meta: rocketMeta(roomID, ownerID, "owner-leave")}); err != nil {
|
||||
t.Fatalf("owner leave room failed: %v", err)
|
||||
}
|
||||
if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
|
||||
Meta: rocketMeta(roomID, igniterID, "current-level-fill"),
|
||||
TargetType: "user",
|
||||
TargetUserId: igniterID,
|
||||
GiftId: "gift-current-level-fill",
|
||||
GiftCount: 1,
|
||||
}); err != nil {
|
||||
t.Fatalf("ignite current level rocket failed: %v", err)
|
||||
}
|
||||
|
||||
now.now = now.now.Add(1100 * time.Millisecond)
|
||||
if err := svc.SweepRoomRocketLaunchings(ctx); err != nil {
|
||||
t.Fatalf("sweep current-level top1 rocket failed: %v", err)
|
||||
}
|
||||
|
||||
if hasRocketGrant(wallet.grants, ownerID, 2001) {
|
||||
t.Fatalf("left historical room GiftRank top1 must not receive current-level top1 reward: %+v", wallet.grants)
|
||||
}
|
||||
if !hasRocketGrant(wallet.grants, igniterID, 2001) || !hasRocketGrant(wallet.grants, igniterID, 3001) || !hasRocketGrant(wallet.grants, igniterID, 1001) {
|
||||
t.Fatalf("only current-level contributor/igniter/in-room user must receive all level rewards: %+v", wallet.grants)
|
||||
}
|
||||
|
||||
info, err := svc.GetRoomRocket(ctx, &roomv1.GetRoomRocketRequest{
|
||||
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID},
|
||||
RoomId: roomID,
|
||||
ViewerUserId: igniterID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("get current-level top1 rocket failed: %v", err)
|
||||
}
|
||||
rewards := info.GetRocket().GetState().GetLastRewards()
|
||||
if len(rewards) != 3 || countRocketRewards(rewards, ownerID, "top1") != 0 ||
|
||||
countRocketRewards(rewards, igniterID, "top1") != 1 ||
|
||||
countRocketRewards(rewards, igniterID, "igniter") != 1 ||
|
||||
countRocketRewards(rewards, igniterID, "in_room") != 1 {
|
||||
t.Fatalf("reward view must not include historical room GiftRank top1: %+v", rewards)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoomRocketLaunchStacksAllSpecialPoolsAndRandomInRoomReward(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
|
||||
@ -171,6 +171,16 @@ type RocketPendingLaunch struct {
|
||||
CoverURL string
|
||||
}
|
||||
|
||||
// RocketContribution 保存当前火箭等级里真正进入燃料条的用户贡献。
|
||||
type RocketContribution struct {
|
||||
// UserID 是加燃料的送礼人;top1 只从这一批当前等级贡献里选。
|
||||
UserID int64
|
||||
// Fuel 是当前等级内实际写入进度条的燃料,溢出部分不算入下一等级。
|
||||
Fuel int64
|
||||
// UpdatedAtMS 用于贡献相同时按最近贡献排序,保持榜单和 UI 直觉一致。
|
||||
UpdatedAtMS int64
|
||||
}
|
||||
|
||||
// RocketState 保存 Room Cell 内当前火箭进度和最近一次发射结果。
|
||||
type RocketState struct {
|
||||
// CurrentLevel 是当前正在积攒的火箭等级,UTC 日重置后回到 1。
|
||||
@ -196,6 +206,8 @@ type RocketState struct {
|
||||
ConfigVersion int64
|
||||
// PendingLaunches 是已经满阈值但还没到延迟发射时间的火箭队列。
|
||||
PendingLaunches []RocketPendingLaunch
|
||||
// CurrentContributions 只记录当前正在积攒等级的实际燃料贡献,点火进入下一等级后清空。
|
||||
CurrentContributions []RocketContribution
|
||||
// LastRewards 保存最近一次发射发奖结果,用于断线重连后补 UI。
|
||||
LastRewards []RocketRewardGrant
|
||||
}
|
||||
@ -207,6 +219,7 @@ func (t *RocketState) Clone() *RocketState {
|
||||
}
|
||||
cloned := *t
|
||||
cloned.PendingLaunches = append([]RocketPendingLaunch(nil), t.PendingLaunches...)
|
||||
cloned.CurrentContributions = append([]RocketContribution(nil), t.CurrentContributions...)
|
||||
cloned.LastRewards = append([]RocketRewardGrant(nil), t.LastRewards...)
|
||||
return &cloned
|
||||
}
|
||||
@ -628,6 +641,7 @@ func cloneRocketState(input *RocketState) *RocketState {
|
||||
}
|
||||
cloned := *input
|
||||
cloned.PendingLaunches = append([]RocketPendingLaunch(nil), input.PendingLaunches...)
|
||||
cloned.CurrentContributions = append([]RocketContribution(nil), input.CurrentContributions...)
|
||||
cloned.LastRewards = append([]RocketRewardGrant(nil), input.LastRewards...)
|
||||
return &cloned
|
||||
}
|
||||
@ -664,21 +678,30 @@ func rocketStateToProto(input *RocketState) *roomv1.RoomRocketState {
|
||||
CoverUrl: launch.CoverURL,
|
||||
})
|
||||
}
|
||||
contributions := make([]*roomv1.RoomRocketContribution, 0, len(input.CurrentContributions))
|
||||
for _, contribution := range input.CurrentContributions {
|
||||
contributions = append(contributions, &roomv1.RoomRocketContribution{
|
||||
UserId: contribution.UserID,
|
||||
Fuel: contribution.Fuel,
|
||||
UpdatedAtMs: contribution.UpdatedAtMS,
|
||||
})
|
||||
}
|
||||
return &roomv1.RoomRocketState{
|
||||
CurrentLevel: input.CurrentLevel,
|
||||
CurrentFuel: input.CurrentFuel,
|
||||
FuelThreshold: input.FuelThreshold,
|
||||
Status: input.Status,
|
||||
IgnitedAtMs: input.IgnitedAtMS,
|
||||
LaunchAtMs: input.LaunchAtMS,
|
||||
LaunchedAtMs: input.LaunchedAtMS,
|
||||
ResetAtMs: input.ResetAtMS,
|
||||
Top1UserId: input.Top1UserID,
|
||||
IgniterUserId: input.IgniterUserID,
|
||||
RocketId: input.RocketID,
|
||||
ConfigVersion: input.ConfigVersion,
|
||||
LastRewards: rewards,
|
||||
PendingLaunches: pending,
|
||||
CurrentLevel: input.CurrentLevel,
|
||||
CurrentFuel: input.CurrentFuel,
|
||||
FuelThreshold: input.FuelThreshold,
|
||||
Status: input.Status,
|
||||
IgnitedAtMs: input.IgnitedAtMS,
|
||||
LaunchAtMs: input.LaunchAtMS,
|
||||
LaunchedAtMs: input.LaunchedAtMS,
|
||||
ResetAtMs: input.ResetAtMS,
|
||||
Top1UserId: input.Top1UserID,
|
||||
IgniterUserId: input.IgniterUserID,
|
||||
RocketId: input.RocketID,
|
||||
ConfigVersion: input.ConfigVersion,
|
||||
LastRewards: rewards,
|
||||
PendingLaunches: pending,
|
||||
CurrentContributions: contributions,
|
||||
}
|
||||
}
|
||||
|
||||
@ -714,20 +737,29 @@ func rocketStateFromProto(input *roomv1.RoomRocketState) *RocketState {
|
||||
CoverURL: launch.GetCoverUrl(),
|
||||
})
|
||||
}
|
||||
contributions := make([]RocketContribution, 0, len(input.GetCurrentContributions()))
|
||||
for _, contribution := range input.GetCurrentContributions() {
|
||||
contributions = append(contributions, RocketContribution{
|
||||
UserID: contribution.GetUserId(),
|
||||
Fuel: contribution.GetFuel(),
|
||||
UpdatedAtMS: contribution.GetUpdatedAtMs(),
|
||||
})
|
||||
}
|
||||
return &RocketState{
|
||||
CurrentLevel: input.GetCurrentLevel(),
|
||||
CurrentFuel: input.GetCurrentFuel(),
|
||||
FuelThreshold: input.GetFuelThreshold(),
|
||||
Status: input.GetStatus(),
|
||||
IgnitedAtMS: input.GetIgnitedAtMs(),
|
||||
LaunchAtMS: input.GetLaunchAtMs(),
|
||||
LaunchedAtMS: input.GetLaunchedAtMs(),
|
||||
ResetAtMS: input.GetResetAtMs(),
|
||||
Top1UserID: input.GetTop1UserId(),
|
||||
IgniterUserID: input.GetIgniterUserId(),
|
||||
RocketID: input.GetRocketId(),
|
||||
ConfigVersion: input.GetConfigVersion(),
|
||||
PendingLaunches: pending,
|
||||
LastRewards: rewards,
|
||||
CurrentLevel: input.GetCurrentLevel(),
|
||||
CurrentFuel: input.GetCurrentFuel(),
|
||||
FuelThreshold: input.GetFuelThreshold(),
|
||||
Status: input.GetStatus(),
|
||||
IgnitedAtMS: input.GetIgnitedAtMs(),
|
||||
LaunchAtMS: input.GetLaunchAtMs(),
|
||||
LaunchedAtMS: input.GetLaunchedAtMs(),
|
||||
ResetAtMS: input.GetResetAtMs(),
|
||||
Top1UserID: input.GetTop1UserId(),
|
||||
IgniterUserID: input.GetIgniterUserId(),
|
||||
RocketID: input.GetRocketId(),
|
||||
ConfigVersion: input.GetConfigVersion(),
|
||||
PendingLaunches: pending,
|
||||
CurrentContributions: contributions,
|
||||
LastRewards: rewards,
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user