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

457 lines
18 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/voiceroomrocket"
"github.com/redis/go-redis/v9"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type localGateway struct {
db *gorm.DB
goldEvents map[string]integration.GoldReceiptCommand
}
func (g localGateway) MapRoomProfiles(ctx context.Context, roomIDs []int64) (map[int64]integration.RoomProfile, error) {
result := make(map[int64]integration.RoomProfile, len(roomIDs))
for _, roomID := range roomIDs {
result[roomID] = integration.RoomProfile{
ID: integration.Int64Value(roomID),
RoomAccount: strconv.FormatInt(roomID, 10),
RoomName: "local-room-" + strconv.FormatInt(roomID, 10),
SysOrigin: "LIKEI",
}
}
return result, nil
}
func (g localGateway) GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error) {
var user model.UserBaseInfo
if err := g.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
return integration.UserProfile{}, err
}
return integration.UserProfile{
ID: integration.Int64Value(user.ID),
Account: user.Account,
UserAvatar: user.UserAvatar,
UserNickname: user.UserNickname,
CountryCode: user.CountryCode,
CountryName: user.CountryName,
OriginSys: user.OriginSys,
}, nil
}
func (g localGateway) ExistsGoldEvent(ctx context.Context, eventID string) (bool, error) {
_, ok := g.goldEvents[eventID]
return ok, nil
}
func (g localGateway) ChangeGoldBalance(ctx context.Context, cmd integration.GoldReceiptCommand) error {
g.goldEvents[cmd.EventID] = cmd
return nil
}
func (g localGateway) GivePropsBackpack(ctx context.Context, req integration.GivePropsBackpackRequest) error {
return nil
}
func (g localGateway) GetNobleVIPAbility(ctx context.Context, sourceID int64) (integration.PropsNobleVIPAbility, error) {
return integration.PropsNobleVIPAbility{}, nil
}
func (g localGateway) SwitchUseProps(ctx context.Context, userID int64, propsID int64) error {
return nil
}
func (g localGateway) ActivateTemporaryBadge(ctx context.Context, userID int64, badgeID int64, days int) error {
return nil
}
func (g localGateway) RemoveUserProfileCacheAll(ctx context.Context, userID int64) error {
return nil
}
type sampleRoom struct {
RoomID int64
UserID int64
}
type verifySummary struct {
DSNHost string `json:"dsnHost"`
RedisAddr string `json:"redisAddr"`
RoomID int64 `json:"roomId,string"`
Top1UserID int64 `json:"top1UserId,string"`
IgniteUserID int64 `json:"igniteUserId,string"`
DayKey string `json:"dayKey"`
FirstAcceptedEnergy int64 `json:"firstAcceptedEnergy"`
SecondRawEnergy int64 `json:"secondRawEnergy"`
SecondAcceptedEnergy int64 `json:"secondAcceptedEnergy"`
LaunchNo string `json:"launchNo"`
StatusLevel int `json:"statusLevel"`
StatusEnergy int64 `json:"statusEnergy"`
KingRecords int `json:"kingRecords"`
Top1PopupBeforeAck int `json:"top1PopupBeforeAck"`
Top1PopupAfterAck int `json:"top1PopupAfterAck"`
IgnitePopupCount int `json:"ignitePopupCount"`
LeftUserID int64 `json:"leftUserId,string"`
LeftUserSnapshotCount int64 `json:"leftUserSnapshotCount"`
LeftUserRewardCount int64 `json:"leftUserRewardCount"`
RewardRecordCount int64 `json:"rewardRecordCount"`
SnapshotCount int64 `json:"snapshotCount"`
SelectedAudienceCount int64 `json:"selectedAudienceCount"`
GrantedRewardCount int64 `json:"grantedRewardCount"`
PendingRewardCount int64 `json:"pendingRewardCount"`
GoldGrantCount int `json:"goldGrantCount"`
BroadcastEventCount int64 `json:"broadcastEventCount"`
MQEnvelopeVerified bool `json:"mqEnvelopeVerified"`
DuplicateReason string `json:"duplicateReason"`
}
func main() {
ctx := context.Background()
dsn := env("CHATAPP_STORE_MYSQL_DSN", "root:123456@tcp(127.0.0.1:13306)/likei?charset=utf8mb4&parseTime=True&loc=Asia%2FRiyadh")
redisAddr := env("CHATAPP_STORE_REDIS_ADDR", "127.0.0.1:16379")
db, err := gorm.Open(mysql.Open(dsn), &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: redisAddr})
defer rdb.Close()
must(rdb.Ping(ctx).Err(), "ping redis")
room := pickRoom(ctx, db)
otherUsers := pickUsers(ctx, db, room.UserID, 3)
if room.RoomID <= 0 || room.UserID <= 0 || len(otherUsers) < 3 {
log.Fatalf("missing real room/user sample: room=%+v otherUsers=%v", room, otherUsers)
}
igniteUserID := otherUsers[0]
cfg := config.Config{
VoiceRoomRocket: config.VoiceRoomRocketConfig{
DefaultSysOrigin: "LIKEI",
Timezone: "Asia/Riyadh",
InRoomRewardDueZSetKey: "voice_room:rocket:in_room_due",
BroadcastStreamKey: "voice_room:rocket:broadcast",
RoomLockTTLSeconds: 10,
},
}
gateway := localGateway{db: db, goldEvents: map[string]integration.GoldReceiptCommand{}}
service := voiceroomrocket.NewService(cfg, db, rdb, gateway)
user := common.AuthUser{UserID: room.UserID, SysOrigin: "LIKEI"}
igniteUser := common.AuthUser{UserID: igniteUserID, SysOrigin: "LIKEI"}
now := time.Now()
day := now.In(time.FixedZone("UTC+3", 3*60*60)).Format("20060102")
cleanup(ctx, db, rdb, room.RoomID, day)
seedConfig(ctx, service, db)
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)
must(err, "initial status")
assert(initial.CurrentLevel == 1 && initial.CurrentEnergy == 0 && initial.NeedEnergy == 100, "initial status mismatch")
firstPayload := giftPayload("LOCAL_VERIFY_TRACK_1", room.UserID, room.RoomID, 60, now)
first, err := service.ProcessGiftPayload(ctx, firstPayload)
must(err, "process first gift")
assert(first.Processed && first.AcceptedEnergy == 60 && !first.Launched, "first gift result mismatch")
mid, err := service.GetStatus(ctx, user, room.RoomID)
must(err, "mid status")
assert(mid.CurrentLevel == 1 && mid.CurrentEnergy == 60 && mid.DisplayPercent == 60, "mid status mismatch")
secondPayload := mqEnvelope(giftPayload("LOCAL_VERIFY_TRACK_2", igniteUserID, room.RoomID, 80, now.Add(time.Second)))
second, err := service.ProcessGiftPayload(ctx, secondPayload)
must(err, "process second gift")
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)
must(err, "final status")
assert(finalStatus.CurrentLevel == 2 && finalStatus.CurrentEnergy == 0 && finalStatus.NeedEnergy == 200, "final status mismatch")
king, err := service.GetKingRanking(ctx, user, room.RoomID, 1, 1, 1, 100)
must(err, "king ranking")
assert(len(king.Records) >= 2, "king records missing")
assert(king.Records[0].UserID == room.UserID && king.Records[0].ScoreEnergy == 60, "top1 ranking mismatch")
assert(king.Records[1].UserID == igniteUserID && king.Records[1].ScoreEnergy == 40, "ignite ranking mismatch")
top1Popups, err := service.ListRewardPopups(ctx, user, room.RoomID)
must(err, "top1 popups")
assert(len(top1Popups.Records) >= 1, "top1 popup count mismatch")
ignitePopups, err := service.ListRewardPopups(ctx, igniteUser, room.RoomID)
must(err, "ignite popups")
assert(len(ignitePopups.Records) >= 1, "ignite popup count mismatch")
ackIDs := make([]voiceroomrocket.FlexibleInt64, 0, len(top1Popups.Records))
for _, record := range top1Popups.Records {
ackIDs = append(ackIDs, voiceroomrocket.FlexibleInt64(record.ID))
}
must(service.AckRewardPopups(ctx, user, voiceroomrocket.AckRewardPopupsRequest{
RewardRecordIDs: ackIDs,
}), "ack top1 popup")
top1AfterAck, err := service.ListRewardPopups(ctx, user, room.RoomID)
must(err, "top1 popups after ack")
assert(len(top1AfterAck.Records) == 0, "top1 popup ack mismatch")
rewardRecords, err := service.PageRewardRecords(ctx, user, room.RoomID, 1, 20)
must(err, "top1 reward records")
assert(len(rewardRecords.Records) >= 1, "top1 reward record count mismatch")
dueProcessed, err := service.ProcessDueInRoomRewards(ctx, now.Add(30*time.Second), 10)
must(err, "process due in-room rewards")
assert(dueProcessed == 1, "due in-room reward count mismatch")
granted, err := service.ProcessPendingRewards(ctx, 100)
must(err, "process pending rewards")
assert(granted >= 5, "granted reward count mismatch")
duplicate, err := service.ProcessGiftPayload(ctx, secondPayload)
must(err, "process duplicate gift")
assert(duplicate.Reason == "duplicate_event", "duplicate reason mismatch")
var rewardCount int64
must(db.WithContext(ctx).Model(&model.VoiceRoomRocketRewardRecord{}).
Where("room_id = ? AND day_key = ?", room.RoomID, day).
Count(&rewardCount).Error, "count rewards")
var snapshotCount int64
must(db.WithContext(ctx).Model(&model.VoiceRoomRocketAudienceSnapshot{}).
Where("room_id = ?", room.RoomID).
Count(&snapshotCount).Error, "count snapshots")
assert(snapshotCount == 3, "snapshot count mismatch")
var selectedCount int64
must(db.WithContext(ctx).Model(&model.VoiceRoomRocketAudienceSnapshot{}).
Where("room_id = ? AND selected = ?", room.RoomID, true).
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
must(db.WithContext(ctx).Model(&model.VoiceRoomRocketRewardRecord{}).
Where("room_id = ? AND day_key = ? AND grant_status = ?", room.RoomID, day, "SUCCESS").
Count(&successCount).Error, "count granted rewards")
var pendingCount int64
must(db.WithContext(ctx).Model(&model.VoiceRoomRocketRewardRecord{}).
Where("room_id = ? AND day_key = ? AND grant_status <> ?", room.RoomID, day, "SUCCESS").
Count(&pendingCount).Error, "count pending rewards")
broadcastCount, err := rdb.XLen(ctx, "voice_room:rocket:broadcast").Result()
must(err, "count broadcast stream")
summary := verifySummary{
DSNHost: "127.0.0.1:13306/likei",
RedisAddr: redisAddr,
RoomID: room.RoomID,
Top1UserID: room.UserID,
IgniteUserID: igniteUserID,
DayKey: day,
FirstAcceptedEnergy: first.AcceptedEnergy,
SecondRawEnergy: 80,
SecondAcceptedEnergy: second.AcceptedEnergy,
LaunchNo: launch.LaunchNo,
StatusLevel: finalStatus.CurrentLevel,
StatusEnergy: finalStatus.CurrentEnergy,
KingRecords: len(king.Records),
Top1PopupBeforeAck: len(top1Popups.Records),
Top1PopupAfterAck: len(top1AfterAck.Records),
IgnitePopupCount: len(ignitePopups.Records),
LeftUserID: leftUserID,
LeftUserSnapshotCount: leftSnapshotCount,
LeftUserRewardCount: leftRewardCount,
RewardRecordCount: rewardCount,
SnapshotCount: snapshotCount,
SelectedAudienceCount: selectedCount,
GrantedRewardCount: successCount,
PendingRewardCount: pendingCount,
GoldGrantCount: len(gateway.goldEvents),
BroadcastEventCount: broadcastCount,
MQEnvelopeVerified: true,
DuplicateReason: duplicate.Reason,
}
encoded, err := json.MarshalIndent(summary, "", " ")
must(err, "marshal summary")
fmt.Println(string(encoded))
}
func pickRoom(ctx context.Context, db *gorm.DB) sampleRoom {
var row sampleRoom
_ = 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 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 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 voice_room_rocket_audience_snapshot")
must(db.WithContext(ctx).Exec("DELETE FROM voice_room_rocket_reward_config WHERE sys_origin = ? AND reward_name LIKE ?", "LIKEI", "Local 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()
_ = rdb.Del(ctx, "voice_room:rocket:broadcast").Err()
}
func seedConfig(ctx context.Context, service *voiceroomrocket.Service, db *gorm.DB) {
_, 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":"本地验证规则"}`),
})
must(err, "save 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 levels")
_, err = service.SaveRewardConfigs(ctx, voiceroomrocket.SaveRewardConfigsRequest{
SysOrigin: "LIKEI",
Rewards: []voiceroomrocket.RewardConfigPayload{
{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: "IN_ROOM", RewardType: "GOLD", RewardName: "Local Verify In Room Gold", RewardAmount: 1, RewardCopies: 3, Enabled: true, Sort: 1},
},
})
must(err, "save rewards")
var count int64
must(db.WithContext(ctx).Model(&model.VoiceRoomRocketLevelConfig{}).Where("sys_origin = ? AND enabled = ?", "LIKEI", true).Count(&count).Error, "count levels")
assert(count >= 6, "level config missing")
}
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 mqEnvelope(body string) string {
encodedBody, err := json.Marshal(body)
must(err, "marshal mq body")
return fmt.Sprintf(`{"tag":"give_gift_v3","body":%s}`, string(encodedBody))
}
func env(key, fallback string) string {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
return value
}
func must(err error, label string) {
if err != nil {
log.Fatalf("%s: %v", label, err)
}
}
func assert(ok bool, message string) {
if !ok {
log.Fatal(message)
}
}