2026-05-15 12:07:26 +08:00

463 lines
15 KiB
Go

package voiceroomrocket
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"chatapp3-golang/internal/model"
"chatapp3-golang/internal/utils"
"gorm.io/gorm"
)
// GetConfig 返回 App 火箭配置、等级资源和当前等级奖励预览。
func (s *Service) GetConfig(ctx context.Context, user AuthUser) (*ConfigResponse, error) {
return s.getConfigResponse(ctx, user.SysOrigin, 0)
}
// GetRule 返回火箭规则说明。
func (s *Service) GetRule(ctx context.Context, user AuthUser) (json.RawMessage, error) {
snapshot, err := s.loadConfig(ctx, user.SysOrigin)
if err != nil {
return nil, err
}
if strings.TrimSpace(snapshot.RuleText) == "" {
return json.RawMessage(`{}`), nil
}
return json.RawMessage(snapshot.RuleText), nil
}
// GetAdminConfig 返回后台配置总览。
func (s *Service) GetAdminConfig(ctx context.Context, sysOrigin string) (*ConfigResponse, error) {
return s.getConfigResponse(ctx, sysOrigin, 0)
}
// SaveAdminConfig 保存后台系统配置。
func (s *Service) SaveAdminConfig(ctx context.Context, req SaveConfigRequest) (*ConfigResponse, error) {
sysOrigin := s.normalizeSysOrigin(req.SysOrigin)
timezone := defaultTimezone
maxLevel := req.MaxLevel
if maxLevel <= 0 {
maxLevel = defaultMaxLevel
} else if maxLevel > defaultMaxLevel {
maxLevel = defaultMaxLevel
}
rankingLimit := req.RankingTopLimit
if rankingLimit <= 0 || rankingLimit > 100 {
rankingLimit = defaultRankingTopLimit
}
keepDays := req.RewardRecordKeepDays
if keepDays <= 0 {
keepDays = defaultRewardRecordKeepDays
}
broadcastSeconds := req.BroadcastDurationSeconds
if broadcastSeconds <= 0 {
broadcastSeconds = defaultBroadcastDurationSeconds
}
inRoomDelay := req.InRoomRewardDelaySeconds
if inRoomDelay <= 0 {
inRoomDelay = defaultInRoomRewardDelaySeconds
}
energyRule := strings.TrimSpace(string(req.EnergyRuleJSON))
if energyRule != "" && energyRule != "null" && !json.Valid([]byte(energyRule)) {
return nil, NewAppError(http.StatusBadRequest, "invalid_energy_rule_json", "energyRuleJson must be valid JSON")
}
ruleText := strings.TrimSpace(string(req.RuleText))
if ruleText != "" && ruleText != "null" && !json.Valid([]byte(ruleText)) {
return nil, NewAppError(http.StatusBadRequest, "invalid_rule_text", "ruleText must be valid JSON")
}
if err := s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
now := time.Now()
var row model.VoiceRoomRocketConfig
err := tx.Where("sys_origin = ?", sysOrigin).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
id, idErr := utils.NextID()
if idErr != nil {
return idErr
}
row = model.VoiceRoomRocketConfig{
ID: id,
SysOrigin: sysOrigin,
CreateTime: now,
}
} else if err != nil {
return err
}
row.Enabled = req.Enabled
row.Timezone = timezone
row.MaxLevel = maxLevel
row.RankingTopLimit = rankingLimit
row.RewardRecordKeepDays = keepDays
row.BroadcastDurationSeconds = broadcastSeconds
row.InRoomRewardDelaySeconds = inRoomDelay
row.EnergyRuleJSON = nullJSONToEmpty(energyRule)
row.RuleText = nullJSONToEmpty(ruleText)
row.UpdateTime = now
if row.CreateTime.IsZero() {
row.CreateTime = now
}
return tx.Save(&row).Error
}); err != nil {
return nil, err
}
return s.GetAdminConfig(ctx, sysOrigin)
}
// ListLevelConfigs 返回后台等级配置。
func (s *Service) ListLevelConfigs(ctx context.Context, sysOrigin string) ([]LevelConfigPayload, error) {
rows, err := s.loadLevelConfigs(ctx, s.normalizeSysOrigin(sysOrigin), false)
if err != nil {
return nil, err
}
result := make([]LevelConfigPayload, 0, len(rows))
for _, row := range rows {
result = append(result, levelPayload(row))
}
return result, nil
}
// SaveLevelConfigs 批量保存后台等级配置。
func (s *Service) SaveLevelConfigs(ctx context.Context, req SaveLevelConfigsRequest) ([]LevelConfigPayload, error) {
sysOrigin := s.normalizeSysOrigin(req.SysOrigin)
if len(req.Levels) == 0 {
return nil, NewAppError(http.StatusBadRequest, "missing_level_configs", "levels is required")
}
seen := map[int]struct{}{}
if err := s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
now := time.Now()
for _, item := range req.Levels {
if item.Level <= 0 || item.Level > defaultMaxLevel {
return NewAppError(http.StatusBadRequest, "invalid_level", "level must be between 1 and 6")
}
if _, ok := seen[item.Level]; ok {
return NewAppError(http.StatusBadRequest, "duplicate_level", "level must be unique")
}
seen[item.Level] = struct{}{}
if item.NeedEnergy <= 0 {
return NewAppError(http.StatusBadRequest, "invalid_need_energy", "needEnergy must be positive")
}
threshold := strings.TrimSpace(item.ShakeThresholdPercent)
if threshold == "" {
threshold = "0.0000"
}
var row model.VoiceRoomRocketLevelConfig
err := tx.Where("sys_origin = ? AND level = ?", sysOrigin, item.Level).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
id, idErr := utils.NextID()
if idErr != nil {
return idErr
}
row = model.VoiceRoomRocketLevelConfig{
ID: id,
SysOrigin: sysOrigin,
Level: item.Level,
CreateTime: now,
}
} else if err != nil {
return err
}
row.NeedEnergy = item.NeedEnergy
row.ShakeThresholdPercent = threshold
row.PreviewRocketURL = strings.TrimSpace(item.PreviewRocketURL)
row.RocketIconURL = strings.TrimSpace(item.RocketIconURL)
row.RocketAnimationURL = strings.TrimSpace(item.RocketAnimationURL)
row.ProgressBarURL = strings.TrimSpace(item.ProgressBarURL)
row.Enabled = item.Enabled
row.Sort = item.Sort
row.UpdateTime = now
if row.CreateTime.IsZero() {
row.CreateTime = now
}
if err := tx.Save(&row).Error; err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
return s.ListLevelConfigs(ctx, sysOrigin)
}
// ListRewardConfigs 返回后台奖励配置。
func (s *Service) ListRewardConfigs(ctx context.Context, sysOrigin string, level int) ([]RewardConfigPayload, error) {
query := s.repo.DB.WithContext(ctx).
Where("sys_origin = ?", s.normalizeSysOrigin(sysOrigin)).
Order("level ASC, reward_scene ASC, sort ASC, id ASC")
if level > 0 {
query = query.Where("level = ?", level)
}
var rows []model.VoiceRoomRocketRewardConfig
if err := query.Find(&rows).Error; err != nil {
return nil, err
}
result := make([]RewardConfigPayload, 0, len(rows))
for _, row := range rows {
result = append(result, rewardPayload(row))
}
return result, nil
}
// SaveRewardConfigs 批量保存后台奖励配置。
func (s *Service) SaveRewardConfigs(ctx context.Context, req SaveRewardConfigsRequest) ([]RewardConfigPayload, error) {
sysOrigin := s.normalizeSysOrigin(req.SysOrigin)
targetLevels := map[int]struct{}{}
if req.Level > 0 {
if req.Level > defaultMaxLevel {
return nil, NewAppError(http.StatusBadRequest, "invalid_level", "level must be between 1 and 6")
}
targetLevels[req.Level] = struct{}{}
}
if err := s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
now := time.Now()
savedIDsByLevel := map[int][]int64{}
if len(req.Rewards) == 0 {
if len(targetLevels) == 0 {
return NewAppError(http.StatusBadRequest, "missing_reward_configs", "rewards is required")
}
for level := range targetLevels {
if err := tx.Where("sys_origin = ? AND level = ?", sysOrigin, level).
Delete(&model.VoiceRoomRocketRewardConfig{}).Error; err != nil {
return err
}
}
return nil
}
for _, item := range req.Rewards {
scene := strings.ToUpper(strings.TrimSpace(item.RewardScene))
if scene != rewardSceneTop1 && scene != rewardSceneIgnite && scene != rewardSceneInRoom {
return NewAppError(http.StatusBadRequest, "invalid_reward_scene", "rewardScene is invalid")
}
if item.Level <= 0 || item.Level > defaultMaxLevel {
return NewAppError(http.StatusBadRequest, "invalid_level", "level must be between 1 and 6")
}
if req.Level > 0 && item.Level != req.Level {
return NewAppError(http.StatusBadRequest, "invalid_level", "reward level must match request level")
}
targetLevels[item.Level] = struct{}{}
rewardType := strings.ToUpper(strings.TrimSpace(item.RewardType))
if rewardType == "" {
return NewAppError(http.StatusBadRequest, "missing_reward_type", "rewardType is required")
}
if !isSupportedRewardType(rewardType) {
return NewAppError(http.StatusBadRequest, "invalid_reward_type", "rewardType is invalid")
}
if rewardType == rewardTypeNone && scene != rewardSceneInRoom {
return NewAppError(http.StatusBadRequest, "invalid_reward_type", "NONE reward is only allowed for IN_ROOM")
}
rewardName := strings.TrimSpace(item.RewardName)
if rewardType == rewardTypeNone && rewardName == "" {
rewardName = "轮空"
}
if rewardName == "" {
return NewAppError(http.StatusBadRequest, "missing_reward_name", "rewardName is required")
}
amount := item.RewardAmount
if rewardType == rewardTypeNone {
amount = 0
item.RewardItemID = nil
item.ExpireDays = nil
} else if amount <= 0 {
amount = 1
}
var row model.VoiceRoomRocketRewardConfig
if item.ID > 0 {
if err := tx.Where("id = ? AND sys_origin = ?", item.ID, sysOrigin).First(&row).Error; err != nil {
return err
}
} else {
id, idErr := utils.NextID()
if idErr != nil {
return idErr
}
row = model.VoiceRoomRocketRewardConfig{
ID: id,
SysOrigin: sysOrigin,
CreateTime: now,
}
}
row.Level = item.Level
row.RewardScene = scene
row.RewardType = rewardType
row.RewardItemID = item.RewardItemID
row.RewardName = rewardName
row.RewardCover = strings.TrimSpace(item.RewardCover)
row.RewardAmount = amount
row.ExpireDays = item.ExpireDays
row.Weight = item.Weight
row.Sort = item.Sort
row.Enabled = item.Enabled
row.UpdateTime = now
if row.CreateTime.IsZero() {
row.CreateTime = now
}
if err := tx.Save(&row).Error; err != nil {
return err
}
savedIDsByLevel[item.Level] = append(savedIDsByLevel[item.Level], row.ID)
}
for level := range targetLevels {
query := tx.Where("sys_origin = ? AND level = ?", sysOrigin, level)
if ids := savedIDsByLevel[level]; len(ids) > 0 {
query = query.Where("id NOT IN ?", ids)
}
if err := query.Delete(&model.VoiceRoomRocketRewardConfig{}).Error; err != nil {
return err
}
}
return nil
}); err != nil {
return nil, err
}
return s.ListRewardConfigs(ctx, sysOrigin, req.Level)
}
func (s *Service) getConfigResponse(ctx context.Context, sysOrigin string, currentLevel int) (*ConfigResponse, error) {
snapshot, err := s.loadConfig(ctx, sysOrigin)
if err != nil {
return nil, err
}
levels, err := s.loadLevelConfigs(ctx, snapshot.SysOrigin, true)
if err != nil {
return nil, err
}
rewardLevel := currentLevel
if rewardLevel <= 0 {
rewardLevel = 1
}
rewards, err := s.loadRewardConfigs(ctx, snapshot.SysOrigin, rewardLevel)
if err != nil {
return nil, err
}
return configResponse(snapshot, levels, rewards), nil
}
func (s *Service) loadConfig(ctx context.Context, sysOrigin string) (configSnapshot, error) {
sysOrigin = s.normalizeSysOrigin(sysOrigin)
var row model.VoiceRoomRocketConfig
err := s.repo.DB.WithContext(ctx).Where("sys_origin = ?", sysOrigin).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return configSnapshot{
SysOrigin: sysOrigin,
Enabled: false,
Timezone: defaultTimezone,
MaxLevel: defaultMaxLevel,
RankingTopLimit: defaultRankingTopLimit,
RewardRecordKeepDays: defaultRewardRecordKeepDays,
BroadcastDurationSeconds: defaultBroadcastDurationSeconds,
InRoomRewardDelaySeconds: defaultInRoomRewardDelaySeconds,
}, nil
}
if err != nil {
return configSnapshot{}, err
}
return configSnapshot{
Configured: true,
ID: row.ID,
SysOrigin: row.SysOrigin,
Enabled: row.Enabled,
Timezone: defaultTimezone,
MaxLevel: positiveOrDefault(row.MaxLevel, defaultMaxLevel),
RankingTopLimit: clampPositive(row.RankingTopLimit, defaultRankingTopLimit, 100),
RewardRecordKeepDays: positiveOrDefault(row.RewardRecordKeepDays, defaultRewardRecordKeepDays),
BroadcastDurationSeconds: positiveOrDefault(row.BroadcastDurationSeconds, defaultBroadcastDurationSeconds),
InRoomRewardDelaySeconds: positiveOrDefault(row.InRoomRewardDelaySeconds, defaultInRoomRewardDelaySeconds),
EnergyRuleJSON: row.EnergyRuleJSON,
RuleText: row.RuleText,
UpdateTime: row.UpdateTime,
}, nil
}
func (s *Service) loadLevelConfigs(ctx context.Context, sysOrigin string, enabledOnly bool) ([]model.VoiceRoomRocketLevelConfig, error) {
query := s.repo.DB.WithContext(ctx).
Where("sys_origin = ?", s.normalizeSysOrigin(sysOrigin)).
Order("sort ASC, level ASC")
if enabledOnly {
query = query.Where("enabled = ?", true)
}
var rows []model.VoiceRoomRocketLevelConfig
if err := query.Find(&rows).Error; err != nil {
return nil, err
}
if len(rows) == 0 && enabledOnly {
return defaultLevelRows(s.normalizeSysOrigin(sysOrigin)), nil
}
return rows, nil
}
func (s *Service) loadLevelConfig(ctx context.Context, sysOrigin string, level int) (model.VoiceRoomRocketLevelConfig, error) {
var row model.VoiceRoomRocketLevelConfig
err := s.repo.DB.WithContext(ctx).
Where("sys_origin = ? AND level = ? AND enabled = ?", s.normalizeSysOrigin(sysOrigin), level, true).
First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
defaults := defaultLevelRows(s.normalizeSysOrigin(sysOrigin))
for _, item := range defaults {
if item.Level == level {
return item, nil
}
}
return model.VoiceRoomRocketLevelConfig{}, NewAppError(http.StatusBadRequest, "rocket_level_not_configured", "rocket level is not configured")
}
return row, err
}
func (s *Service) loadRewardConfigs(ctx context.Context, sysOrigin string, level int) ([]model.VoiceRoomRocketRewardConfig, error) {
query := s.repo.DB.WithContext(ctx).
Where("sys_origin = ? AND enabled = ?", s.normalizeSysOrigin(sysOrigin), true).
Order("reward_scene ASC, sort ASC, id ASC")
if level > 0 {
query = query.Where("level = ?", level)
}
var rows []model.VoiceRoomRocketRewardConfig
if err := query.Find(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func defaultLevelRows(sysOrigin string) []model.VoiceRoomRocketLevelConfig {
rows := make([]model.VoiceRoomRocketLevelConfig, 0, defaultMaxLevel)
for i := 1; i <= defaultMaxLevel; i++ {
rows = append(rows, model.VoiceRoomRocketLevelConfig{
SysOrigin: sysOrigin,
Level: i,
NeedEnergy: int64(i * 100),
ShakeThresholdPercent: "95.0000",
Enabled: true,
Sort: i,
})
}
return rows
}
func positiveOrDefault(value int, fallback int) int {
if value <= 0 {
return fallback
}
return value
}
func clampPositive(value int, fallback int, maxValue int) int {
if value <= 0 {
value = fallback
}
if maxValue > 0 && value > maxValue {
return maxValue
}
return value
}
func nullJSONToEmpty(value string) string {
value = strings.TrimSpace(value)
if value == "" || value == "null" {
return "{}"
}
return value
}