//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) } }