package voiceroomrocket import ( "encoding/json" "fmt" "math" "sort" "strconv" "strings" "time" "chatapp3-golang/internal/integration" "chatapp3-golang/internal/model" ) func (s *Service) normalizeSysOrigin(sysOrigin string) string { if strings.TrimSpace(sysOrigin) != "" { return strings.ToUpper(strings.TrimSpace(sysOrigin)) } if strings.TrimSpace(s.cfg.VoiceRoomRocket.DefaultSysOrigin) != "" { return strings.ToUpper(strings.TrimSpace(s.cfg.VoiceRoomRocket.DefaultSysOrigin)) } return defaultSysOrigin } func normalizeTimezone(value string) string { value = strings.TrimSpace(value) if value == "" { return defaultTimezone } return value } func resolveLocation(value string) *time.Location { location, err := time.LoadLocation(normalizeTimezone(value)) if err != nil { return time.UTC } return location } func dayKey(t time.Time, location *time.Location) string { if location == nil { location = time.UTC } return t.In(location).Format("20060102") } func normalizePage(cursor, limit int) (int, int) { if cursor <= 0 { cursor = 1 } if limit <= 0 { limit = 20 } if limit > 100 { limit = 100 } return cursor, limit } func formatDateTime(t time.Time) string { if t.IsZero() { return "" } return t.Format("2006-01-02 15:04:05") } func pointerTimeValue(value *time.Time) time.Time { if value == nil { return time.Time{} } return *value } func decimalPercent(current, need int64) (string, int) { if need <= 0 || current <= 0 { return "0.0000", 0 } percent := float64(current) * 100 / float64(need) display := int(math.Floor(percent)) if display > 100 { display = 100 } return fmt.Sprintf("%.4f", percent), display } func parsePercent(value string) float64 { parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64) if err != nil { return 0 } return parsed } func countDistinctAcceptUsers(users []giftEventUser) int { if len(users) == 0 { return 0 } set := make(map[int64]struct{}, len(users)) for _, user := range users { if user.AcceptUserID.Int64() <= 0 { continue } set[user.AcceptUserID.Int64()] = struct{}{} } return len(set) } func resolveMillisTime(raw int64, fallback time.Time) time.Time { if raw <= 0 { return fallback } if raw < 1_000_000_000_000 { return time.Unix(raw, 0) } return time.UnixMilli(raw) } func truncateString(value string, limit int) string { if limit <= 0 || len(value) <= limit { return value } return value[:limit] } func isDuplicateKeyErr(err error) bool { if err == nil { return false } message := strings.ToLower(err.Error()) return strings.Contains(message, "duplicate entry") || strings.Contains(message, "duplicate key") || strings.Contains(message, "unique constraint") } func levelPayload(row model.VoiceRoomRocketLevelConfig) LevelConfigPayload { return LevelConfigPayload{ ID: row.ID, Level: row.Level, NeedEnergy: row.NeedEnergy, ShakeThresholdPercent: row.ShakeThresholdPercent, PreviewRocketURL: row.PreviewRocketURL, RocketIconURL: row.RocketIconURL, RocketAnimationURL: row.RocketAnimationURL, ProgressBarURL: row.ProgressBarURL, Enabled: row.Enabled, Sort: row.Sort, } } func rewardPayload(row model.VoiceRoomRocketRewardConfig) RewardConfigPayload { return RewardConfigPayload{ ID: row.ID, Level: row.Level, RewardScene: row.RewardScene, RewardType: row.RewardType, RewardItemID: row.RewardItemID, RewardName: row.RewardName, RewardCover: row.RewardCover, RewardAmount: row.RewardAmount, ExpireDays: row.ExpireDays, RewardCopies: positiveOrDefault(row.RewardCopies, 1), Weight: row.Weight, Sort: row.Sort, Enabled: row.Enabled, } } func rewardPreviewPayload(rows []model.VoiceRoomRocketRewardConfig) RewardPreviewResponse { sort.Slice(rows, func(i, j int) bool { if rows[i].RewardScene != rows[j].RewardScene { return rewardSceneOrder(rows[i].RewardScene) < rewardSceneOrder(rows[j].RewardScene) } if rows[i].Sort != rows[j].Sort { return rows[i].Sort < rows[j].Sort } return rows[i].ID < rows[j].ID }) resp := RewardPreviewResponse{} for _, row := range rows { if strings.EqualFold(strings.TrimSpace(row.RewardType), rewardTypeNone) { continue } payload := rewardPayload(row) switch strings.ToUpper(strings.TrimSpace(row.RewardScene)) { case rewardSceneTop1: resp.Top1 = append(resp.Top1, payload) case rewardSceneIgnite: resp.Ignite = append(resp.Ignite, payload) case rewardSceneInRoom: resp.InRoom = append(resp.InRoom, payload) } } return resp } func rewardSceneOrder(scene string) int { switch strings.ToUpper(strings.TrimSpace(scene)) { case rewardSceneTop1: return 1 case rewardSceneIgnite: return 2 case rewardSceneInRoom: return 3 default: return 100 } } func isSupportedRewardType(rewardType string) bool { switch strings.ToUpper(strings.TrimSpace(rewardType)) { case rewardTypeGold, rewardTypeMount, rewardTypeAvatarFrame, rewardTypeChatBubble, rewardTypeVIPCard, rewardTypeGift, rewardTypeBadge, rewardTypeNone: return true default: return false } } func configResponse(snapshot configSnapshot, levels []model.VoiceRoomRocketLevelConfig, rewards []model.VoiceRoomRocketRewardConfig) *ConfigResponse { levelPayloads := make([]LevelConfigPayload, 0, len(levels)) sort.Slice(levels, func(i, j int) bool { if levels[i].Sort != levels[j].Sort { return levels[i].Sort < levels[j].Sort } return levels[i].Level < levels[j].Level }) for _, row := range levels { levelPayloads = append(levelPayloads, levelPayload(row)) } var ruleText json.RawMessage if strings.TrimSpace(snapshot.RuleText) != "" { ruleText = json.RawMessage(snapshot.RuleText) } var energyRuleJSON json.RawMessage if strings.TrimSpace(snapshot.EnergyRuleJSON) != "" { energyRuleJSON = json.RawMessage(snapshot.EnergyRuleJSON) } return &ConfigResponse{ Configured: snapshot.Configured, ID: snapshot.ID, SysOrigin: snapshot.SysOrigin, Enabled: snapshot.Enabled, Timezone: snapshot.Timezone, MaxLevel: snapshot.MaxLevel, RankingTopLimit: snapshot.RankingTopLimit, RewardRecordKeepDays: snapshot.RewardRecordKeepDays, BroadcastDurationSeconds: snapshot.BroadcastDurationSeconds, InRoomRewardDelaySeconds: snapshot.InRoomRewardDelaySeconds, EnergyRuleJSON: energyRuleJSON, RuleText: ruleText, Levels: levelPayloads, RewardPreview: rewardPreviewPayload(rewards), UpdateTime: formatDateTime(snapshot.UpdateTime), } } func toRewardRecordView(row model.VoiceRoomRocketRewardRecord) RewardRecordView { return RewardRecordView{ ID: row.ID, LaunchNo: row.LaunchNo, RoomID: row.RoomID, DayKey: row.DayKey, RoundNo: row.RoundNo, Level: row.Level, UserID: row.UserID, RewardScene: row.RewardScene, RewardType: row.RewardType, RewardItemID: row.RewardItemID, RewardName: row.RewardName, RewardCover: row.RewardCover, RewardAmount: row.RewardAmount, ExpireDays: row.ExpireDays, GrantStatus: row.GrantStatus, PopupStatus: row.PopupStatus, ErrorMessage: row.ErrorMessage, GrantTime: formatDateTime(pointerTimeValue(row.GrantTime)), CreateTime: formatDateTime(row.CreateTime), } } func toLaunchRecordView(row model.VoiceRoomRocketLaunch) LaunchRecordView { return LaunchRecordView{ ID: row.ID, LaunchNo: row.LaunchNo, SysOrigin: row.SysOrigin, RoomID: row.RoomID, RoomAccount: row.RoomAccount, DayKey: row.DayKey, RoundNo: row.RoundNo, Level: row.Level, FinalEnergy: row.FinalEnergy, Top1UserID: row.Top1UserID, IgniteUserID: row.IgniteUserID, InRoomSnapshotStatus: row.InRoomSnapshotStatus, LaunchTime: formatDateTime(row.LaunchTime), } } func userProfileToKingRecord(userID int64, rank int, score int64, profile integration.UserProfile) KingRecord { return KingRecord{ Rank: rank, UserID: userID, UserAvatar: profile.UserAvatar, UserNickname: profile.UserNickname, Account: profile.Account, CountryCode: profile.CountryCode, CountryName: profile.CountryName, ScoreEnergy: score, } }