659 lines
25 KiB
Go
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)
|
|
}
|
|
}
|