2026-05-15 23:44:56 +08:00

390 lines
11 KiB
Go

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 rewardPayloads(rows []model.VoiceRoomRocketRewardConfig) []RewardConfigPayload {
sorted := sortedRewardRows(rows)
result := make([]RewardConfigPayload, 0, len(sorted))
for _, row := range sorted {
result = append(result, rewardPayload(row))
}
return result
}
func rewardPreviewPayload(rows []model.VoiceRoomRocketRewardConfig) RewardPreviewResponse {
rows = sortedRewardRows(rows)
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 sortedRewardRows(rows []model.VoiceRoomRocketRewardConfig) []model.VoiceRoomRocketRewardConfig {
sorted := append([]model.VoiceRoomRocketRewardConfig(nil), rows...)
sort.Slice(sorted, func(i, j int) bool {
if sorted[i].RewardScene != sorted[j].RewardScene {
return rewardSceneOrder(sorted[i].RewardScene) < rewardSceneOrder(sorted[j].RewardScene)
}
if sorted[i].Sort != sorted[j].Sort {
return sorted[i].Sort < sorted[j].Sort
}
return sorted[i].ID < sorted[j].ID
})
return sorted
}
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 levelContentsResponse(snapshot configSnapshot, levels []model.VoiceRoomRocketLevelConfig, rewards []model.VoiceRoomRocketRewardConfig) *LevelContentsResponse {
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
})
rewardsByLevel := make(map[int][]model.VoiceRoomRocketRewardConfig)
for _, row := range rewards {
rewardsByLevel[row.Level] = append(rewardsByLevel[row.Level], row)
}
levelPayloads := make([]LevelContentPayload, 0, len(levels))
for _, row := range levels {
levelRewards := rewardsByLevel[row.Level]
levelPayloads = append(levelPayloads, LevelContentPayload{
LevelConfigPayload: levelPayload(row),
Rewards: rewardPayloads(levelRewards),
RewardPreview: rewardPreviewPayload(levelRewards),
})
}
return &LevelContentsResponse{
Configured: snapshot.Configured,
SysOrigin: snapshot.SysOrigin,
Enabled: snapshot.Enabled,
Timezone: snapshot.Timezone,
MaxLevel: snapshot.MaxLevel,
Levels: levelPayloads,
UpdateTime: formatDateTime(snapshot.UpdateTime),
}
}
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 toRewardRecordViews(rows []model.VoiceRoomRocketRewardRecord, profiles map[int64]integration.UserProfile) []RewardRecordView {
records := make([]RewardRecordView, 0, len(rows))
for _, row := range rows {
view := toRewardRecordView(row)
if profile, ok := profiles[row.UserID]; ok {
applyRewardRecordProfile(&view, profile)
}
records = append(records, view)
}
return records
}
func applyRewardRecordProfile(view *RewardRecordView, profile integration.UserProfile) {
if strings.TrimSpace(profile.UserAvatar) != "" {
view.UserAvatar = profile.UserAvatar
}
if strings.TrimSpace(profile.UserNickname) != "" {
view.UserNickname = profile.UserNickname
}
if strings.TrimSpace(profile.Account) != "" {
view.Account = profile.Account
}
}
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,
}
}