yumi-golang/scripts/voice_room_rocket_live_verify.go
2026-05-23 18:38:09 +08:00

659 lines
25 KiB
Go

//go:build ignore
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"strconv"
"strings"
"time"
"chatapp3-golang/internal/common"
"chatapp3-golang/internal/config"
"chatapp3-golang/internal/integration"
"chatapp3-golang/internal/model"
"chatapp3-golang/internal/service/tencentim"
"chatapp3-golang/internal/service/voiceroomrocket"
rmq "github.com/apache/rocketmq-clients/golang/v5"
"github.com/apache/rocketmq-clients/golang/v5/credentials"
"github.com/redis/go-redis/v9"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type liveRoom struct {
RoomID int64
UserID int64
}
type liveProps struct {
ID int64
Type string
Name string
}
type liveRewardRow struct {
ID int64 `json:"id,string"`
UserID int64 `json:"userId,string"`
RewardScene string `json:"rewardScene"`
RewardType string `json:"rewardType"`
RewardItemID *int64 `json:"rewardItemId,omitempty,string"`
RewardName string `json:"rewardName"`
RewardAmount int64 `json:"rewardAmount"`
GrantStatus string `json:"grantStatus"`
GrantEventID string `json:"grantEventId"`
ErrorMessage string `json:"errorMessage,omitempty"`
}
type liveVerifySummary struct {
MySQLDSNHost string `json:"mysqlDsnHost"`
RedisAddr string `json:"redisAddr"`
MQEndpoint string `json:"mqEndpoint"`
MQTopic string `json:"mqTopic"`
MQTag string `json:"mqTag"`
MQConsumerGroup string `json:"mqConsumerGroup"`
MQMessageIDs []string `json:"mqMessageIds"`
RoomID int64 `json:"roomId,string"`
RoomAccount string `json:"roomAccount"`
RoomName string `json:"roomName"`
Top1UserID int64 `json:"top1UserId,string"`
IgniteUserID int64 `json:"igniteUserId,string"`
DayKey string `json:"dayKey"`
FirstTrackID string `json:"firstTrackId"`
SecondTrackID string `json:"secondTrackId"`
FirstAcceptedEnergy int64 `json:"firstAcceptedEnergy"`
SecondAcceptedEnergy int64 `json:"secondAcceptedEnergy"`
LaunchNo string `json:"launchNo"`
LaunchLevel int `json:"launchLevel"`
RewardRecords []liveRewardRow `json:"rewardRecords"`
RewardRecordCount int `json:"rewardRecordCount"`
SuccessRewardCount int `json:"successRewardCount"`
GoldWalletVerifiedCount int `json:"goldWalletVerifiedCount"`
ExpectedGoldByUser map[string]int64 `json:"expectedGoldByUser"`
WalletBalanceDeltaByUser map[string]int64 `json:"walletBalanceDeltaByUser"`
RidePropsID int64 `json:"ridePropsId,omitempty,string"`
RidePropsName string `json:"ridePropsName,omitempty"`
PropsRunningWaterCount int64 `json:"propsRunningWaterCount"`
AudienceSnapshotCount int64 `json:"audienceSnapshotCount"`
SelectedAudienceCount int64 `json:"selectedAudienceCount"`
BroadcastStreamCount int64 `json:"broadcastStreamCount"`
LaunchBroadcastCount int `json:"launchBroadcastCount"`
RewardBroadcastCount int `json:"rewardBroadcastCount"`
TencentIMDirectSendOK bool `json:"tencentImDirectSendOk"`
Top1PopupBeforeAck int `json:"top1PopupBeforeAck"`
Top1PopupAfterAck int `json:"top1PopupAfterAck"`
DuplicateReason string `json:"duplicateReason"`
ServiceMQInitiallyEnabled bool `json:"serviceMqInitiallyEnabled"`
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
os.Setenv("rocketmq.client.enableSsl", "false")
os.Setenv("mq.consoleAppender.enabled", "true")
rmq.EnableSsl = false
rmq.ResetLogger()
cfg := config.Load()
cfg.VoiceRoomRocket.MQ.Enabled = true
cfg.VoiceRoomRocket.MQ.ConsumerGroup = "voice-room-rocket-live-verify-" + strconv.FormatInt(time.Now().UnixNano(), 10)
cfg.VoiceRoomRocket.MQ.Tag = "voice_room_rocket_live_verify_" + strconv.FormatInt(time.Now().UnixNano(), 10)
cfg.VoiceRoomRocket.InRoomRewardScanIntervalSeconds = 1
cfg.VoiceRoomRocket.RewardGrantScanIntervalSeconds = 1
cfg.VoiceRoomRocket.InRoomRewardBatchSize = 100
cfg.VoiceRoomRocket.RewardGrantBatchSize = 100
cfg.VoiceRoomRocket.InRoomRewardUserLimit = 3
db, err := gorm.Open(mysql.Open(cfg.Store.MySQLDSN), &gorm.Config{})
must(err, "connect mysql")
sqlDB, err := db.DB()
must(err, "unwrap mysql")
defer sqlDB.Close()
must(sqlDB.PingContext(ctx), "ping mysql")
rdb := redis.NewClient(&redis.Options{
Addr: cfg.Store.RedisAddr,
Password: cfg.Store.RedisPassword,
DB: cfg.Store.RedisDB,
})
defer rdb.Close()
must(rdb.Ping(ctx).Err(), "ping redis")
gateways := integration.NewGateways(cfg)
service := voiceroomrocket.NewService(cfg, db, rdb, &gateways)
room := pickRoom(ctx, db)
if room.RoomID <= 0 || room.UserID <= 0 {
log.Fatalf("missing room_member sample: %+v", room)
}
roomProfile, err := loadRoomProfile(ctx, &gateways, room.RoomID)
must(err, "load room profile from java")
if strings.TrimSpace(roomProfile.RoomAccount) == "" {
log.Fatalf("java room profile missing roomAccount: roomId=%d profile=%+v", room.RoomID, roomProfile)
}
otherUsers := pickUsers(ctx, db, room.UserID, 5)
if len(otherUsers) < 3 {
log.Fatalf("missing user sample: room=%+v users=%v", room, otherUsers)
}
igniteUserID := otherUsers[0]
rideProps := pickProps(ctx, db, "RIDE")
if rideProps.ID <= 0 {
log.Fatalf("missing RIDE props_source_record sample")
}
imClient := tencentim.NewClient(cfg.TencentIM)
if _, err := imClient.CreateAVChatRoom(ctx, roomProfile.RoomAccount, roomProfile.RoomName); err != nil {
log.Fatalf("create/check tencent im room %s: %v", roomProfile.RoomAccount, err)
}
if err := imClient.SendCustomGroupMessage(ctx, roomProfile.RoomAccount, map[string]any{
"type": "VOICE_ROOM_ROCKET_LIVE_VERIFY_DIRECT_IM",
"data": map[string]any{
"roomId": strconv.FormatInt(room.RoomID, 10),
"at": time.Now().UnixMilli(),
},
}); err != nil {
log.Fatalf("send tencent im direct verify message: %v", err)
}
local, err := time.LoadLocation("Asia/Riyadh")
must(err, "load timezone")
startedAt := time.Now()
day := startedAt.In(local).Format("20060102")
cleanup(ctx, db, rdb, room.RoomID, day)
seedLiveConfig(ctx, service, db, rideProps)
seedPresence(ctx, rdb, room.RoomID, append([]int64{room.UserID, igniteUserID}, otherUsers[1:]...))
balanceUsers := uniqueInt64s(append([]int64{room.UserID, igniteUserID}, otherUsers[1:]...))
balancesBefore, err := gateways.MapGoldBalance(ctx, balanceUsers)
must(err, "load wallet balances before")
propsRunningWaterBefore := countPropsRunningWaterTotal(ctx, db, igniteUserID, rideProps.ID)
must(service.Start(ctx), "start rocket service")
time.Sleep(4 * time.Second)
producer := newProducer(cfg)
defer producer.GracefulStop()
firstTrack := "LIVE_VERIFY_" + strconv.FormatInt(startedAt.UnixNano(), 10) + "_1"
secondTrack := "LIVE_VERIFY_" + strconv.FormatInt(startedAt.UnixNano(), 10) + "_2"
firstMsgIDs := sendGift(ctx, producer, cfg, firstTrack, room.UserID, room.RoomID, 60, startedAt)
firstLog := waitGiftLog(ctx, db, firstTrack, 30*time.Second)
if firstLog.AcceptedEnergy != 60 || firstLog.Status != "SUCCESS" {
log.Fatalf("first mq gift mismatch: accepted=%d status=%s reason=%s", firstLog.AcceptedEnergy, firstLog.Status, firstLog.ErrorMessage)
}
secondMsgIDs := sendGift(ctx, producer, cfg, secondTrack, igniteUserID, room.RoomID, 80, startedAt.Add(time.Second))
secondLog := waitGiftLog(ctx, db, secondTrack, 30*time.Second)
if secondLog.AcceptedEnergy != 40 || secondLog.Status != "SUCCESS" {
log.Fatalf("second mq gift mismatch: accepted=%d status=%s reason=%s", secondLog.AcceptedEnergy, secondLog.Status, secondLog.ErrorMessage)
}
launch := waitLaunch(ctx, db, room.RoomID, day, 30*time.Second)
if strings.TrimSpace(launch.LaunchNo) == "" {
log.Fatalf("launch missing")
}
waitAudience(ctx, db, launch.ID, 30*time.Second)
rewardRows := waitRewardSuccess(ctx, db, launch.ID, 45*time.Second)
top1User := common.AuthUser{UserID: room.UserID, SysOrigin: "LIKEI"}
popups, err := service.ListRewardPopups(ctx, top1User, room.RoomID)
must(err, "list top1 reward popups")
ackIDs := make([]voiceroomrocket.FlexibleInt64, 0, len(popups.Records))
for _, record := range popups.Records {
ackIDs = append(ackIDs, voiceroomrocket.FlexibleInt64(record.ID))
}
if len(ackIDs) > 0 {
must(service.AckRewardPopups(ctx, top1User, voiceroomrocket.AckRewardPopupsRequest{RewardRecordIDs: ackIDs}), "ack reward popups")
}
popupsAfterAck, err := service.ListRewardPopups(ctx, top1User, room.RoomID)
must(err, "list top1 reward popups after ack")
duplicate, err := service.ProcessGiftPayload(ctx, giftPayload(secondTrack, igniteUserID, room.RoomID, 80, startedAt.Add(time.Second)))
must(err, "process duplicate gift")
rewardSummaries := summarizeRewardRows(rewardRows)
expectedGoldByUser, walletBalanceDeltaByUser, walletVerifiedCount := verifyWalletBalanceDeltas(ctx, &gateways, balancesBefore, rewardRows)
propsRunningWater := countPropsRunningWaterTotal(ctx, db, igniteUserID, rideProps.ID) - propsRunningWaterBefore
var snapshotCount int64
must(db.WithContext(ctx).Model(&model.VoiceRoomRocketAudienceSnapshot{}).Where("launch_id = ?", launch.ID).Count(&snapshotCount).Error, "count audience snapshots")
var selectedCount int64
must(db.WithContext(ctx).Model(&model.VoiceRoomRocketAudienceSnapshot{}).Where("launch_id = ? AND selected = ?", launch.ID, true).Count(&selectedCount).Error, "count selected audience")
broadcastCount, err := rdb.XLen(ctx, cfg.VoiceRoomRocket.BroadcastStreamKey).Result()
must(err, "count broadcast stream")
launchBroadcastCount, rewardBroadcastCount := countBroadcasts(ctx, rdb, cfg.VoiceRoomRocket.BroadcastStreamKey, launch.LaunchNo)
successCount := 0
for _, row := range rewardRows {
if row.GrantStatus == "SUCCESS" {
successCount++
}
}
summary := liveVerifySummary{
MySQLDSNHost: mysqlHost(cfg.Store.MySQLDSN),
RedisAddr: cfg.Store.RedisAddr,
MQEndpoint: cfg.VoiceRoomRocket.MQ.Endpoint,
MQTopic: cfg.VoiceRoomRocket.MQ.Topic,
MQTag: cfg.VoiceRoomRocket.MQ.Tag,
MQConsumerGroup: cfg.VoiceRoomRocket.MQ.ConsumerGroup,
MQMessageIDs: append(firstMsgIDs, secondMsgIDs...),
RoomID: room.RoomID,
RoomAccount: roomProfile.RoomAccount,
RoomName: roomProfile.RoomName,
Top1UserID: room.UserID,
IgniteUserID: igniteUserID,
DayKey: day,
FirstTrackID: firstTrack,
SecondTrackID: secondTrack,
FirstAcceptedEnergy: firstLog.AcceptedEnergy,
SecondAcceptedEnergy: secondLog.AcceptedEnergy,
LaunchNo: launch.LaunchNo,
LaunchLevel: launch.Level,
RewardRecords: rewardSummaries,
RewardRecordCount: len(rewardRows),
SuccessRewardCount: successCount,
GoldWalletVerifiedCount: walletVerifiedCount,
ExpectedGoldByUser: expectedGoldByUser,
WalletBalanceDeltaByUser: walletBalanceDeltaByUser,
RidePropsID: rideProps.ID,
RidePropsName: rideProps.Name,
PropsRunningWaterCount: propsRunningWater,
AudienceSnapshotCount: snapshotCount,
SelectedAudienceCount: selectedCount,
BroadcastStreamCount: broadcastCount,
LaunchBroadcastCount: launchBroadcastCount,
RewardBroadcastCount: rewardBroadcastCount,
TencentIMDirectSendOK: true,
Top1PopupBeforeAck: len(popups.Records),
Top1PopupAfterAck: len(popupsAfterAck.Records),
DuplicateReason: duplicate.Reason,
ServiceMQInitiallyEnabled: true,
}
encoded, err := json.MarshalIndent(summary, "", " ")
must(err, "marshal summary")
fmt.Println(string(encoded))
}
func pickRoom(ctx context.Context, db *gorm.DB) liveRoom {
var row liveRoom
_ = db.WithContext(ctx).
Table("room_member").
Select("room_id, user_id").
Where("sys_origin = ? AND room_id > 0 AND user_id > 0", "LIKEI").
Order("update_time DESC").
Limit(1).
Scan(&row).Error
return row
}
func pickUsers(ctx context.Context, db *gorm.DB, excludeUserID int64, limit int) []int64 {
var users []model.UserBaseInfo
_ = db.WithContext(ctx).
Where("origin_sys = ? AND id <> ?", "LIKEI", excludeUserID).
Order("id DESC").
Limit(limit).
Find(&users).Error
result := make([]int64, 0, len(users))
for _, user := range users {
if user.ID > 0 {
result = append(result, user.ID)
}
}
return result
}
func uniqueInt64s(values []int64) []int64 {
seen := map[int64]struct{}{}
result := make([]int64, 0, len(values))
for _, value := range values {
if value <= 0 {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}
func pickProps(ctx context.Context, db *gorm.DB, propsType string) liveProps {
var row liveProps
_ = db.WithContext(ctx).
Table("props_source_record").
Select("id, type, name").
Where("sys_origin = ? AND type = ? AND admin_free = ? AND is_del = ?", "LIKEI", propsType, true, false).
Order("id ASC").
Limit(1).
Scan(&row).Error
return row
}
func loadRoomProfile(ctx context.Context, gateways *integration.Gateways, roomID int64) (integration.RoomProfile, error) {
profiles, err := gateways.MapRoomProfiles(ctx, []int64{roomID})
if err != nil {
return integration.RoomProfile{}, err
}
if profile, ok := profiles[roomID]; ok {
return profile, nil
}
for _, profile := range profiles {
if int64(profile.ID) == roomID {
return profile, nil
}
}
return integration.RoomProfile{}, fmt.Errorf("room profile not found: %d", roomID)
}
func cleanup(ctx context.Context, db *gorm.DB, rdb *redis.Client, roomID int64, day string) {
tables := []string{
"voice_room_rocket_reward_record",
"voice_room_rocket_launch",
"voice_room_rocket_gift_event_log",
"voice_room_rocket_contribution",
"voice_room_rocket_level_round",
"voice_room_rocket_status",
}
for _, table := range tables {
must(db.WithContext(ctx).Exec("DELETE FROM "+table+" WHERE sys_origin = ? AND room_id = ? AND day_key = ?", "LIKEI", roomID, day).Error, "cleanup "+table)
}
must(db.WithContext(ctx).Exec("DELETE FROM voice_room_rocket_audience_snapshot WHERE sys_origin = ? AND room_id = ?", "LIKEI", roomID).Error, "cleanup audience")
must(db.WithContext(ctx).Exec("DELETE FROM voice_room_rocket_reward_config WHERE sys_origin = ? AND (reward_name LIKE ? OR reward_name LIKE ?)", "LIKEI", "Local Verify%", "Live Verify%").Error, "cleanup reward config")
for level := 1; level <= 6; level++ {
key := fmt.Sprintf("voice_room:rocket:rank:LIKEI:%d:%s:1:%d", roomID, day, level)
_ = rdb.Del(ctx, key).Err()
}
_ = rdb.Del(ctx, fmt.Sprintf("voice_room:rocket:lock:LIKEI:%d", roomID)).Err()
_ = rdb.Del(ctx, fmt.Sprintf("voice_room:online:LIKEI:%d", roomID)).Err()
_ = rdb.Del(ctx, "voice_room:rocket:in_room_due").Err()
}
func seedLiveConfig(ctx context.Context, service *voiceroomrocket.Service, db *gorm.DB, ride liveProps) {
_, err := service.SaveAdminConfig(ctx, voiceroomrocket.SaveConfigRequest{
SysOrigin: "LIKEI",
Enabled: true,
Timezone: "Asia/Riyadh",
MaxLevel: 6,
RankingTopLimit: 100,
RewardRecordKeepDays: 35,
BroadcastDurationSeconds: 7,
InRoomRewardDelaySeconds: 1,
RuleText: json.RawMessage(`{"zh":"live verify rule"}`),
})
must(err, "save rocket config")
levels := make([]voiceroomrocket.LevelConfigPayload, 0, 6)
for i, need := range []int64{100, 200, 300, 400, 500, 600} {
levels = append(levels, voiceroomrocket.LevelConfigPayload{
Level: i + 1,
NeedEnergy: need,
ShakeThresholdPercent: "95.0000",
Enabled: true,
Sort: i + 1,
})
}
_, err = service.SaveLevelConfigs(ctx, voiceroomrocket.SaveLevelConfigsRequest{SysOrigin: "LIKEI", Levels: levels})
must(err, "save level config")
rideItemID := ride.ID
expireDays := 1
_, err = service.SaveRewardConfigs(ctx, voiceroomrocket.SaveRewardConfigsRequest{
SysOrigin: "LIKEI",
Rewards: []voiceroomrocket.RewardConfigPayload{
{Level: 1, RewardScene: "TOP1", RewardType: "GOLD", RewardName: "Live Verify Top1 Gold", RewardAmount: 10, Enabled: true, Sort: 1},
{Level: 1, RewardScene: "IGNITE", RewardType: "GOLD", RewardName: "Live Verify Ignite Gold", RewardAmount: 5, Enabled: true, Sort: 1},
{Level: 1, RewardScene: "IGNITE", RewardType: "MOUNT", RewardItemID: &rideItemID, RewardName: "Live Verify Ignite Ride", RewardAmount: 1, ExpireDays: &expireDays, Enabled: true, Sort: 2},
{Level: 1, RewardScene: "IN_ROOM", RewardType: "GOLD", RewardName: "Live Verify In Room Gold", RewardAmount: 1, Enabled: true, Sort: 1},
},
})
must(err, "save reward config")
var count int64
must(db.WithContext(ctx).Model(&model.VoiceRoomRocketRewardConfig{}).Where("sys_origin = ? AND reward_name LIKE ? AND enabled = ?", "LIKEI", "Live Verify%", true).Count(&count).Error, "count live reward config")
if count != 4 {
log.Fatalf("live reward config count=%d, want 4", count)
}
}
func seedPresence(ctx context.Context, rdb *redis.Client, roomID int64, users []int64) {
key := fmt.Sprintf("voice_room:online:LIKEI:%d", roomID)
for _, userID := range users {
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.Expire(ctx, key, time.Minute).Err(), "expire online room")
}
func newProducer(cfg config.Config) rmq.Producer {
producer, err := rmq.NewProducer(&rmq.Config{
Endpoint: cfg.VoiceRoomRocket.MQ.Endpoint,
NameSpace: cfg.VoiceRoomRocket.MQ.Namespace,
Credentials: &credentials.SessionCredentials{
AccessKey: cfg.VoiceRoomRocket.MQ.AccessKey,
AccessSecret: cfg.VoiceRoomRocket.MQ.AccessSecret,
SecurityToken: cfg.VoiceRoomRocket.MQ.SecurityToken,
},
}, rmq.WithTopics(cfg.VoiceRoomRocket.MQ.Topic))
must(err, "new rocketmq producer")
must(producer.Start(), "start rocketmq producer")
return producer
}
func sendGift(ctx context.Context, producer rmq.Producer, cfg config.Config, trackID string, userID int64, roomID int64, gold int64, at time.Time) []string {
msg := &rmq.Message{
Topic: cfg.VoiceRoomRocket.MQ.Topic,
Body: []byte(giftPayload(trackID, userID, roomID, gold, at)),
}
msg.SetTag(cfg.VoiceRoomRocket.MQ.Tag)
msg.SetKeys(trackID)
receipts, err := producer.Send(ctx, msg)
must(err, "send rocketmq gift")
result := make([]string, 0, len(receipts))
for _, receipt := range receipts {
result = append(result, receipt.MessageID)
}
return result
}
func giftPayload(trackID string, sendUserID int64, roomID int64, gold int64, at time.Time) string {
return fmt.Sprintf(`{
"trackId": %q,
"sysOrigin": "LIKEI",
"sendUserId": %q,
"roomId": %q,
"quantity": 1,
"giftConfig": {"id": "90001", "giftCandy": %d, "type": "GOLD", "giftTab": "NORMAL"},
"createTime": %d
}`, trackID, strconv.FormatInt(sendUserID, 10), strconv.FormatInt(roomID, 10), gold, at.UnixMilli())
}
func waitGiftLog(ctx context.Context, db *gorm.DB, trackID string, timeout time.Duration) model.VoiceRoomRocketGiftEventLog {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
var row model.VoiceRoomRocketGiftEventLog
err := db.WithContext(ctx).Where("track_id = ?", trackID).First(&row).Error
if err == nil {
return row
}
if err != nil && err != gorm.ErrRecordNotFound {
log.Fatalf("load gift log %s: %v", trackID, err)
}
time.Sleep(300 * time.Millisecond)
}
log.Fatalf("timeout waiting gift log: %s", trackID)
return model.VoiceRoomRocketGiftEventLog{}
}
func waitLaunch(ctx context.Context, db *gorm.DB, roomID int64, day string, timeout time.Duration) model.VoiceRoomRocketLaunch {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
var row model.VoiceRoomRocketLaunch
err := db.WithContext(ctx).
Where("sys_origin = ? AND room_id = ? AND day_key = ? AND level = ?", "LIKEI", roomID, day, 1).
Order("launch_time DESC").
First(&row).Error
if err == nil {
return row
}
if err != nil && err != gorm.ErrRecordNotFound {
log.Fatalf("load launch: %v", err)
}
time.Sleep(300 * time.Millisecond)
}
log.Fatalf("timeout waiting launch")
return model.VoiceRoomRocketLaunch{}
}
func waitAudience(ctx context.Context, db *gorm.DB, launchID int64, timeout time.Duration) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
var launch model.VoiceRoomRocketLaunch
must(db.WithContext(ctx).Where("id = ?", launchID).First(&launch).Error, "load launch audience status")
if launch.InRoomSnapshotStatus == "SUCCESS" {
return
}
if launch.InRoomSnapshotStatus == "FAILED" {
log.Fatalf("in-room snapshot failed for launch=%d", launchID)
}
time.Sleep(300 * time.Millisecond)
}
log.Fatalf("timeout waiting in-room audience snapshot")
}
func waitRewardSuccess(ctx context.Context, db *gorm.DB, launchID int64, timeout time.Duration) []model.VoiceRoomRocketRewardRecord {
deadline := time.Now().Add(timeout)
var rows []model.VoiceRoomRocketRewardRecord
for time.Now().Before(deadline) {
rows = nil
must(db.WithContext(ctx).Where("launch_id = ?", launchID).Order("reward_scene ASC, reward_type ASC, user_id ASC").Find(&rows).Error, "load reward rows")
if len(rows) >= 6 {
allDone := true
for _, row := range rows {
if row.GrantStatus != "SUCCESS" {
allDone = false
if row.GrantStatus == "FAILED" {
log.Fatalf("reward failed: id=%d type=%s scene=%s err=%s", row.ID, row.RewardType, row.RewardScene, row.ErrorMessage)
}
}
}
if allDone {
return rows
}
}
time.Sleep(500 * time.Millisecond)
}
rows = nil
_ = db.WithContext(ctx).Where("launch_id = ?", launchID).Order("reward_scene ASC, reward_type ASC, user_id ASC").Find(&rows).Error
log.Fatalf("timeout waiting reward success: rows=%+v", rows)
return nil
}
func summarizeRewardRows(rows []model.VoiceRoomRocketRewardRecord) []liveRewardRow {
result := make([]liveRewardRow, 0, len(rows))
for _, row := range rows {
result = append(result, liveRewardRow{
ID: row.ID,
UserID: row.UserID,
RewardScene: row.RewardScene,
RewardType: row.RewardType,
RewardItemID: row.RewardItemID,
RewardName: row.RewardName,
RewardAmount: row.RewardAmount,
GrantStatus: row.GrantStatus,
GrantEventID: row.GrantEventID,
ErrorMessage: row.ErrorMessage,
})
}
return result
}
func verifyWalletBalanceDeltas(ctx context.Context, gateways *integration.Gateways, before map[int64]int64, rows []model.VoiceRoomRocketRewardRecord) (map[string]int64, map[string]int64, int) {
expected := map[int64]int64{}
for _, row := range rows {
if row.RewardType == "GOLD" {
expected[row.UserID] += row.RewardAmount
}
}
users := make([]int64, 0, len(expected))
for userID := range expected {
users = append(users, userID)
}
after, err := gateways.MapGoldBalance(ctx, users)
must(err, "load wallet balances after")
expectedJSON := make(map[string]int64, len(expected))
deltaJSON := make(map[string]int64, len(expected))
verified := 0
for userID, amount := range expected {
delta := after[userID] - before[userID]
expectedJSON[strconv.FormatInt(userID, 10)] = amount
deltaJSON[strconv.FormatInt(userID, 10)] = delta
if delta < amount {
log.Fatalf("wallet balance delta mismatch: user=%d delta=%d expected>=%d before=%d after=%d", userID, delta, amount, before[userID], after[userID])
}
for _, row := range rows {
if row.UserID == userID && row.RewardType == "GOLD" {
verified++
}
}
}
return expectedJSON, deltaJSON, verified
}
func countPropsRunningWaterTotal(ctx context.Context, db *gorm.DB, userID int64, propsID int64) int64 {
var count int64
must(db.WithContext(ctx).
Table("running_water_user_props").
Where("user_id = ? AND props_id = ? AND origin = ?", userID, propsID, "VOICE_ROOM_ROCKET").
Count(&count).Error, "count props running water")
return count
}
func countBroadcasts(ctx context.Context, rdb *redis.Client, streamKey string, launchNo string) (int, int) {
entries, err := rdb.XRevRangeN(ctx, streamKey, "+", "-", 100).Result()
must(err, "read broadcast stream")
launchCount := 0
rewardCount := 0
for _, entry := range entries {
if fmt.Sprint(entry.Values["launchNo"]) != launchNo {
continue
}
switch fmt.Sprint(entry.Values["type"]) {
case "VOICE_ROOM_ROCKET_LAUNCH_BROADCAST":
launchCount++
case "VOICE_ROOM_ROCKET_REWARD_POPUP", "VOICE_ROOM_ROCKET_REWARD_USER":
rewardCount++
}
}
return launchCount, rewardCount
}
func mysqlHost(dsn string) string {
start := strings.Index(dsn, "@tcp(")
if start < 0 {
return "configured"
}
start += len("@tcp(")
end := strings.Index(dsn[start:], ")")
if end < 0 {
return "configured"
}
return dsn[start : start+end]
}
func must(err error, label string) {
if err != nil {
log.Fatalf("%s: %v", label, err)
}
}