火箭结算不正确问题

This commit is contained in:
hy001 2026-05-23 18:38:09 +08:00
parent 3ec7cc6a62
commit adcc722284
5 changed files with 184 additions and 5 deletions

View File

@ -399,6 +399,9 @@ func TestInRoomRewardWorkerSnapshotsSelectsAndGrants(t *testing.T) {
if err := rdb.SAdd(ctx, onlineRoomKey("LIKEI", roomID), userID).Err(); err != nil { if err := rdb.SAdd(ctx, onlineRoomKey("LIKEI", roomID), userID).Err(); err != nil {
t.Fatalf("sadd online: %v", err) t.Fatalf("sadd online: %v", err)
} }
if err := rdb.Set(ctx, userRoomKey("LIKEI", userID), strconv.FormatInt(roomID, 10), time.Minute).Err(); err != nil {
t.Fatalf("set user room: %v", err)
}
if err := rdb.Set(ctx, userSeenKey("LIKEI", userID), strconv.FormatInt(time.Now().UnixMilli(), 10), time.Minute).Err(); err != nil { if err := rdb.Set(ctx, userSeenKey("LIKEI", userID), strconv.FormatInt(time.Now().UnixMilli(), 10), time.Minute).Err(); err != nil {
t.Fatalf("set seen: %v", err) t.Fatalf("set seen: %v", err)
} }
@ -471,6 +474,92 @@ func TestInRoomRewardWorkerSnapshotsSelectsAndGrants(t *testing.T) {
} }
} }
func TestInRoomRewardWorkerSkipsUsersNoLongerInRoom(t *testing.T) {
gateway := newFakeRocketGateway()
service, db, rdb, mr := newTestServiceWithRedis(t, gateway)
defer mr.Close()
ctx := context.Background()
now := time.Now()
seedRocketConfig(t, db)
seedLevelConfigs(t, db, []int64{100, 200, 300, 400, 500, 600})
seedRewardConfig(t, db, 1, rewardSceneTop1, rewardTypeGold, 10)
seedRewardConfig(t, db, 1, rewardSceneIgnite, rewardTypeGold, 5)
seedRewardConfigWithCopies(t, db, 1, rewardSceneInRoom, rewardTypeGold, 1, 4)
roomID := int64(9001)
for _, userID := range []int64{1001, 1002} {
if err := rdb.SAdd(ctx, onlineRoomKey("LIKEI", roomID), userID).Err(); err != nil {
t.Fatalf("sadd active online: %v", err)
}
if err := rdb.Set(ctx, userRoomKey("LIKEI", userID), strconv.FormatInt(roomID, 10), time.Minute).Err(); err != nil {
t.Fatalf("set active room: %v", err)
}
if err := rdb.Set(ctx, userSeenKey("LIKEI", userID), strconv.FormatInt(now.UnixMilli(), 10), time.Minute).Err(); err != nil {
t.Fatalf("set active seen: %v", err)
}
}
staleUsers := []int64{1003, 1004}
for _, userID := range staleUsers {
if err := rdb.SAdd(ctx, onlineRoomKey("LIKEI", roomID), userID).Err(); err != nil {
t.Fatalf("sadd stale online: %v", err)
}
if err := rdb.Set(ctx, userSeenKey("LIKEI", userID), strconv.FormatInt(now.UnixMilli(), 10), time.Minute).Err(); err != nil {
t.Fatalf("set stale seen: %v", err)
}
}
if err := rdb.Set(ctx, userRoomKey("LIKEI", staleUsers[1]), strconv.FormatInt(roomID+1, 10), time.Minute).Err(); err != nil {
t.Fatalf("set stale room: %v", err)
}
payload := `{
"trackId":"track-launch-in-room-stale",
"sysOrigin":"LIKEI",
"sendUserId":"1001",
"roomId":"9001",
"quantity":1,
"giftConfig":{"id":"11","giftCandy":100,"type":"GOLD","giftTab":"NORMAL"},
"createTime":` + strconvFormatMillis(now) + `
}`
resp, err := service.ProcessGiftPayload(ctx, payload)
if err != nil {
t.Fatalf("ProcessGiftPayload() error = %v", err)
}
if !resp.Processed {
t.Fatalf("response = %+v, want processed", resp)
}
if _, err := service.ProcessDueLaunches(ctx, now.Add(11*time.Second), 10); err != nil {
t.Fatalf("ProcessDueLaunches() error = %v", err)
}
if _, err := service.ProcessDueInRoomRewards(ctx, now.Add(30*time.Second), 10); err != nil {
t.Fatalf("ProcessDueInRoomRewards() error = %v", err)
}
var snapshotCount int64
if err := db.Model(&model.VoiceRoomRocketAudienceSnapshot{}).Count(&snapshotCount).Error; err != nil {
t.Fatalf("count snapshot: %v", err)
}
if snapshotCount != 2 {
t.Fatalf("snapshot count = %d, want 2 active users only", snapshotCount)
}
for _, userID := range staleUsers {
var userSnapshotCount int64
if err := db.Model(&model.VoiceRoomRocketAudienceSnapshot{}).Where("user_id = ?", userID).Count(&userSnapshotCount).Error; err != nil {
t.Fatalf("count stale snapshot: %v", err)
}
if userSnapshotCount != 0 {
t.Fatalf("stale user %d snapshot count = %d, want 0", userID, userSnapshotCount)
}
var rewardCount int64
if err := db.Model(&model.VoiceRoomRocketRewardRecord{}).Where("user_id = ? AND reward_scene = ?", userID, rewardSceneInRoom).Count(&rewardCount).Error; err != nil {
t.Fatalf("count stale reward: %v", err)
}
if rewardCount != 0 {
t.Fatalf("stale user %d in-room reward count = %d, want 0", userID, rewardCount)
}
}
}
func TestProcessPendingRewardsGrantsBadgeThroughBadgeGateway(t *testing.T) { func TestProcessPendingRewardsGrantsBadgeThroughBadgeGateway(t *testing.T) {
gateway := newFakeRocketGateway() gateway := newFakeRocketGateway()
service, db := newTestService(t) service, db := newTestService(t)

View File

@ -33,9 +33,14 @@ func (s *Service) listOnlineRoomUsers(ctx context.Context, sysOrigin string, roo
continue continue
} }
seen[userID] = struct{}{} seen[userID] = struct{}{}
seenAt, active := s.loadActiveRoomSeenTime(ctx, sysOrigin, roomID, userID, now)
if !active {
_ = s.repo.Redis.SRem(ctx, onlineRoomKey(sysOrigin, roomID), userID).Err()
continue
}
users = append(users, roomAudienceUser{ users = append(users, roomAudienceUser{
UserID: userID, UserID: userID,
LastSeenTime: s.loadUserSeenTime(ctx, sysOrigin, userID, now), LastSeenTime: seenAt,
}) })
} }
sort.Slice(users, func(i, j int) bool { sort.Slice(users, func(i, j int) bool {
@ -44,6 +49,43 @@ func (s *Service) listOnlineRoomUsers(ctx context.Context, sysOrigin string, roo
return users, nil return users, nil
} }
func (s *Service) loadActiveRoomSeenTime(ctx context.Context, sysOrigin string, roomID, userID int64, now time.Time) (time.Time, bool) {
if s.repo.Redis == nil {
return time.Time{}, false
}
rawRoomID, err := s.repo.Redis.Get(ctx, userRoomKey(sysOrigin, userID)).Result()
if err != nil {
return time.Time{}, false
}
currentRoomID, err := strconv.ParseInt(strings.TrimSpace(rawRoomID), 10, 64)
if err != nil || currentRoomID != roomID {
return time.Time{}, false
}
seenAt, ok := s.loadUserSeenTimeStrict(ctx, sysOrigin, userID, now)
if !ok {
return time.Time{}, false
}
if ttl := s.roomPresenceTTL(); ttl > 0 && now.Sub(seenAt) > ttl {
return time.Time{}, false
}
return seenAt, true
}
func (s *Service) loadUserSeenTimeStrict(ctx context.Context, sysOrigin string, userID int64, fallback time.Time) (time.Time, bool) {
if s.repo.Redis == nil {
return time.Time{}, false
}
raw, err := s.repo.Redis.Get(ctx, userSeenKey(sysOrigin, userID)).Result()
if err != nil {
return time.Time{}, false
}
millis, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
if err != nil || millis <= 0 {
return time.Time{}, false
}
return resolveMillisTime(millis, fallback), true
}
func (s *Service) loadUserSeenTime(ctx context.Context, sysOrigin string, userID int64, fallback time.Time) time.Time { func (s *Service) loadUserSeenTime(ctx context.Context, sysOrigin string, userID int64, fallback time.Time) time.Time {
if s.repo.Redis == nil { if s.repo.Redis == nil {
return fallback return fallback
@ -59,10 +101,22 @@ func (s *Service) loadUserSeenTime(ctx context.Context, sysOrigin string, userID
return resolveMillisTime(millis, fallback) return resolveMillisTime(millis, fallback)
} }
func (s *Service) roomPresenceTTL() time.Duration {
seconds := s.cfg.VoiceRoomRedPacket.PresenceTTLSeconds
if seconds <= 0 {
seconds = defaultPresenceTTLSeconds
}
return time.Duration(seconds) * time.Second
}
func onlineRoomKey(sysOrigin string, roomID int64) string { func onlineRoomKey(sysOrigin string, roomID int64) string {
return fmt.Sprintf("voice_room:online:%s:%d", strings.ToUpper(strings.TrimSpace(sysOrigin)), roomID) return fmt.Sprintf("voice_room:online:%s:%d", strings.ToUpper(strings.TrimSpace(sysOrigin)), roomID)
} }
func userRoomKey(sysOrigin string, userID int64) string {
return fmt.Sprintf("voice_room:user_room:%s:%d", strings.ToUpper(strings.TrimSpace(sysOrigin)), userID)
}
func userSeenKey(sysOrigin string, userID int64) string { func userSeenKey(sysOrigin string, userID int64) string {
return fmt.Sprintf("voice_room:user_seen:%s:%d", strings.ToUpper(strings.TrimSpace(sysOrigin)), userID) return fmt.Sprintf("voice_room:user_seen:%s:%d", strings.ToUpper(strings.TrimSpace(sysOrigin)), userID)
} }

View File

@ -32,6 +32,7 @@ const (
defaultWorkerBatchSize = 100 defaultWorkerBatchSize = 100
defaultWorkerInterval = time.Second defaultWorkerInterval = time.Second
defaultRoomLockTTL = 10 * time.Second defaultRoomLockTTL = 10 * time.Second
defaultPresenceTTLSeconds = 60
statusCharging = "CHARGING" statusCharging = "CHARGING"
statusLaunching = "LAUNCHING" statusLaunching = "LAUNCHING"

View File

@ -429,6 +429,7 @@ func seedPresence(ctx context.Context, rdb *redis.Client, roomID int64, users []
key := fmt.Sprintf("voice_room:online:LIKEI:%d", roomID) key := fmt.Sprintf("voice_room:online:LIKEI:%d", roomID)
for _, userID := range users { for _, userID := range users {
must(rdb.SAdd(ctx, key, userID).Err(), "seed online room") must(rdb.SAdd(ctx, key, userID).Err(), "seed online room")
must(rdb.Set(ctx, fmt.Sprintf("voice_room:user_room:LIKEI:%d", userID), strconv.FormatInt(roomID, 10), time.Minute).Err(), "seed user room")
must(rdb.Set(ctx, fmt.Sprintf("voice_room:user_seen:LIKEI:%d", userID), strconv.FormatInt(time.Now().UnixMilli(), 10), time.Minute).Err(), "seed user seen") must(rdb.Set(ctx, fmt.Sprintf("voice_room:user_seen:LIKEI:%d", userID), strconv.FormatInt(time.Now().UnixMilli(), 10), time.Minute).Err(), "seed user seen")
} }
must(rdb.Expire(ctx, key, time.Minute).Err(), "expire online room") must(rdb.Expire(ctx, key, time.Minute).Err(), "expire online room")

View File

@ -109,6 +109,9 @@ type verifySummary struct {
Top1PopupBeforeAck int `json:"top1PopupBeforeAck"` Top1PopupBeforeAck int `json:"top1PopupBeforeAck"`
Top1PopupAfterAck int `json:"top1PopupAfterAck"` Top1PopupAfterAck int `json:"top1PopupAfterAck"`
IgnitePopupCount int `json:"ignitePopupCount"` IgnitePopupCount int `json:"ignitePopupCount"`
LeftUserID int64 `json:"leftUserId,string"`
LeftUserSnapshotCount int64 `json:"leftUserSnapshotCount"`
LeftUserRewardCount int64 `json:"leftUserRewardCount"`
RewardRecordCount int64 `json:"rewardRecordCount"` RewardRecordCount int64 `json:"rewardRecordCount"`
SnapshotCount int64 `json:"snapshotCount"` SnapshotCount int64 `json:"snapshotCount"`
SelectedAudienceCount int64 `json:"selectedAudienceCount"` SelectedAudienceCount int64 `json:"selectedAudienceCount"`
@ -162,6 +165,11 @@ func main() {
cleanup(ctx, db, rdb, room.RoomID, day) cleanup(ctx, db, rdb, room.RoomID, day)
seedConfig(ctx, service, db) seedConfig(ctx, service, db)
seedPresence(ctx, rdb, room.RoomID, append([]int64{room.UserID, igniteUserID}, otherUsers[1:]...)) seedPresence(ctx, rdb, room.RoomID, append([]int64{room.UserID, igniteUserID}, otherUsers[1:]...))
leftUserID := otherUsers[2]
must(rdb.Del(ctx,
fmt.Sprintf("voice_room:user_room:LIKEI:%d", leftUserID),
fmt.Sprintf("voice_room:user_seen:LIKEI:%d", leftUserID),
).Err(), "simulate left user presence")
initial, err := service.GetStatus(ctx, user, room.RoomID) initial, err := service.GetStatus(ctx, user, room.RoomID)
must(err, "initial status") must(err, "initial status")
@ -179,7 +187,17 @@ func main() {
secondPayload := mqEnvelope(giftPayload("LOCAL_VERIFY_TRACK_2", igniteUserID, room.RoomID, 80, now.Add(time.Second))) secondPayload := mqEnvelope(giftPayload("LOCAL_VERIFY_TRACK_2", igniteUserID, room.RoomID, 80, now.Add(time.Second)))
second, err := service.ProcessGiftPayload(ctx, secondPayload) second, err := service.ProcessGiftPayload(ctx, secondPayload)
must(err, "process second gift") must(err, "process second gift")
assert(second.Processed && second.AcceptedEnergy == 40 && second.Launched && second.LaunchNo != "", "second gift result mismatch") assert(second.Processed && second.AcceptedEnergy == 40 && !second.Launched, "second gift result mismatch")
launched, err := service.ProcessDueLaunches(ctx, now.Add(11*time.Second), 10)
must(err, "process due launches")
assert(launched == 1, "due launch count mismatch")
var launch model.VoiceRoomRocketLaunch
must(db.WithContext(ctx).
Where("room_id = ? AND day_key = ?", room.RoomID, day).
Order("create_time DESC").
First(&launch).Error, "load launch")
assert(strings.TrimSpace(launch.LaunchNo) != "", "launch no missing")
finalStatus, err := service.GetStatus(ctx, user, room.RoomID) finalStatus, err := service.GetStatus(ctx, user, room.RoomID)
must(err, "final status") must(err, "final status")
@ -213,7 +231,7 @@ func main() {
must(err, "top1 reward records") must(err, "top1 reward records")
assert(len(rewardRecords.Records) >= 1, "top1 reward record count mismatch") assert(len(rewardRecords.Records) >= 1, "top1 reward record count mismatch")
dueProcessed, err := service.ProcessDueInRoomRewards(ctx, time.Now().Add(2*time.Second), 10) dueProcessed, err := service.ProcessDueInRoomRewards(ctx, now.Add(30*time.Second), 10)
must(err, "process due in-room rewards") must(err, "process due in-room rewards")
assert(dueProcessed == 1, "due in-room reward count mismatch") assert(dueProcessed == 1, "due in-room reward count mismatch")
@ -233,10 +251,22 @@ func main() {
must(db.WithContext(ctx).Model(&model.VoiceRoomRocketAudienceSnapshot{}). must(db.WithContext(ctx).Model(&model.VoiceRoomRocketAudienceSnapshot{}).
Where("room_id = ?", room.RoomID). Where("room_id = ?", room.RoomID).
Count(&snapshotCount).Error, "count snapshots") Count(&snapshotCount).Error, "count snapshots")
assert(snapshotCount == 3, "snapshot count mismatch")
var selectedCount int64 var selectedCount int64
must(db.WithContext(ctx).Model(&model.VoiceRoomRocketAudienceSnapshot{}). must(db.WithContext(ctx).Model(&model.VoiceRoomRocketAudienceSnapshot{}).
Where("room_id = ? AND selected = ?", room.RoomID, true). Where("room_id = ? AND selected = ?", room.RoomID, true).
Count(&selectedCount).Error, "count selected snapshots") Count(&selectedCount).Error, "count selected snapshots")
assert(selectedCount == 3, "selected snapshot count mismatch")
var leftSnapshotCount int64
must(db.WithContext(ctx).Model(&model.VoiceRoomRocketAudienceSnapshot{}).
Where("room_id = ? AND user_id = ?", room.RoomID, leftUserID).
Count(&leftSnapshotCount).Error, "count left user snapshots")
assert(leftSnapshotCount == 0, "left user snapshot mismatch")
var leftRewardCount int64
must(db.WithContext(ctx).Model(&model.VoiceRoomRocketRewardRecord{}).
Where("room_id = ? AND day_key = ? AND user_id = ?", room.RoomID, day, leftUserID).
Count(&leftRewardCount).Error, "count left user rewards")
assert(leftRewardCount == 0, "left user reward mismatch")
var successCount int64 var successCount int64
must(db.WithContext(ctx).Model(&model.VoiceRoomRocketRewardRecord{}). must(db.WithContext(ctx).Model(&model.VoiceRoomRocketRewardRecord{}).
Where("room_id = ? AND day_key = ? AND grant_status = ?", room.RoomID, day, "SUCCESS"). Where("room_id = ? AND day_key = ? AND grant_status = ?", room.RoomID, day, "SUCCESS").
@ -258,13 +288,16 @@ func main() {
FirstAcceptedEnergy: first.AcceptedEnergy, FirstAcceptedEnergy: first.AcceptedEnergy,
SecondRawEnergy: 80, SecondRawEnergy: 80,
SecondAcceptedEnergy: second.AcceptedEnergy, SecondAcceptedEnergy: second.AcceptedEnergy,
LaunchNo: second.LaunchNo, LaunchNo: launch.LaunchNo,
StatusLevel: finalStatus.CurrentLevel, StatusLevel: finalStatus.CurrentLevel,
StatusEnergy: finalStatus.CurrentEnergy, StatusEnergy: finalStatus.CurrentEnergy,
KingRecords: len(king.Records), KingRecords: len(king.Records),
Top1PopupBeforeAck: len(top1Popups.Records), Top1PopupBeforeAck: len(top1Popups.Records),
Top1PopupAfterAck: len(top1AfterAck.Records), Top1PopupAfterAck: len(top1AfterAck.Records),
IgnitePopupCount: len(ignitePopups.Records), IgnitePopupCount: len(ignitePopups.Records),
LeftUserID: leftUserID,
LeftUserSnapshotCount: leftSnapshotCount,
LeftUserRewardCount: leftRewardCount,
RewardRecordCount: rewardCount, RewardRecordCount: rewardCount,
SnapshotCount: snapshotCount, SnapshotCount: snapshotCount,
SelectedAudienceCount: selectedCount, SelectedAudienceCount: selectedCount,
@ -312,6 +345,7 @@ func seedPresence(ctx context.Context, rdb *redis.Client, roomID int64, users []
key := fmt.Sprintf("voice_room:online:LIKEI:%d", roomID) key := fmt.Sprintf("voice_room:online:LIKEI:%d", roomID)
for _, userID := range users { for _, userID := range users {
must(rdb.SAdd(ctx, key, userID).Err(), "seed online room") must(rdb.SAdd(ctx, key, userID).Err(), "seed online room")
must(rdb.Set(ctx, fmt.Sprintf("voice_room:user_room:LIKEI:%d", userID), strconv.FormatInt(roomID, 10), time.Minute).Err(), "seed user room")
must(rdb.Set(ctx, fmt.Sprintf("voice_room:user_seen:LIKEI:%d", userID), strconv.FormatInt(time.Now().UnixMilli(), 10), time.Minute).Err(), "seed user seen") must(rdb.Set(ctx, fmt.Sprintf("voice_room:user_seen:LIKEI:%d", userID), strconv.FormatInt(time.Now().UnixMilli(), 10), time.Minute).Err(), "seed user seen")
} }
must(rdb.Expire(ctx, key, time.Minute).Err(), "expire online room") must(rdb.Expire(ctx, key, time.Minute).Err(), "expire online room")
@ -373,7 +407,7 @@ func seedConfig(ctx context.Context, service *voiceroomrocket.Service, db *gorm.
Rewards: []voiceroomrocket.RewardConfigPayload{ Rewards: []voiceroomrocket.RewardConfigPayload{
{Level: 1, RewardScene: "TOP1", RewardType: "GOLD", RewardName: "Local Verify Top1 Gold", RewardAmount: 10, Enabled: true, Sort: 1}, {Level: 1, RewardScene: "TOP1", RewardType: "GOLD", RewardName: "Local Verify Top1 Gold", RewardAmount: 10, Enabled: true, Sort: 1},
{Level: 1, RewardScene: "IGNITE", RewardType: "GOLD", RewardName: "Local Verify Ignite Gold", RewardAmount: 5, Enabled: true, Sort: 1}, {Level: 1, RewardScene: "IGNITE", RewardType: "GOLD", RewardName: "Local Verify Ignite Gold", RewardAmount: 5, Enabled: true, Sort: 1},
{Level: 1, RewardScene: "IN_ROOM", RewardType: "GOLD", RewardName: "Local Verify In Room Gold", RewardAmount: 1, Enabled: true, Sort: 1}, {Level: 1, RewardScene: "IN_ROOM", RewardType: "GOLD", RewardName: "Local Verify In Room Gold", RewardAmount: 1, RewardCopies: 3, Enabled: true, Sort: 1},
}, },
}) })
must(err, "save rewards") must(err, "save rewards")