463 lines
15 KiB
Go
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
|
|
}
|