428 lines
16 KiB
Go
428 lines
16 KiB
Go
package roomtreasure
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"hyapp-admin-server/internal/integration/roomclient"
|
|
)
|
|
|
|
const (
|
|
roomTreasureLevelCount = 7
|
|
|
|
defaultEnergySource = "gift_point_added"
|
|
defaultOpenDelayMS = int64(30_000)
|
|
defaultBroadcastScope = "region"
|
|
defaultRewardStackPolicy = "allow_stack"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidArgument = errors.New("invalid argument")
|
|
|
|
defaultLevelEnergyThresholds = []int64{1_000, 3_000, 6_000, 10_000, 15_000, 21_000, 28_000}
|
|
)
|
|
|
|
type Service struct {
|
|
roomClient roomclient.Client
|
|
}
|
|
|
|
type RoomTreasureConfig struct {
|
|
AppCode string `json:"appCode"`
|
|
Enabled bool `json:"enabled"`
|
|
ConfigVersion int64 `json:"configVersion"`
|
|
EnergySource string `json:"energySource"`
|
|
OpenDelayMS int64 `json:"openDelayMs"`
|
|
BroadcastEnabled bool `json:"broadcastEnabled"`
|
|
BroadcastScope string `json:"broadcastScope"`
|
|
BroadcastDelayMS int64 `json:"broadcastDelayMs"`
|
|
RewardStackPolicy string `json:"rewardStackPolicy"`
|
|
Levels []RoomTreasureLevelConfig `json:"levels"`
|
|
GiftEnergyRules []GiftEnergyRuleConfig `json:"giftEnergyRules"`
|
|
UpdatedByAdminID int64 `json:"updatedByAdminId"`
|
|
CreatedAtMS int64 `json:"createdAtMs"`
|
|
UpdatedAtMS int64 `json:"updatedAtMs"`
|
|
}
|
|
|
|
type RoomTreasureLevelConfig struct {
|
|
Level int32 `json:"level"`
|
|
EnergyThreshold int64 `json:"energyThreshold"`
|
|
CoverURL string `json:"coverUrl"`
|
|
AnimationURL string `json:"animationUrl"`
|
|
OpeningAnimationURL string `json:"openingAnimationUrl"`
|
|
OpenedImageURL string `json:"openedImageUrl"`
|
|
InRoomRewards []RoomTreasureRewardItem `json:"inRoomRewards"`
|
|
Top1Rewards []RoomTreasureRewardItem `json:"top1Rewards"`
|
|
IgniterRewards []RoomTreasureRewardItem `json:"igniterRewards"`
|
|
}
|
|
|
|
type RoomTreasureRewardItem struct {
|
|
RewardItemID string `json:"rewardItemId"`
|
|
ResourceGroupID int64 `json:"resourceGroupId"`
|
|
Weight int64 `json:"weight"`
|
|
DisplayName string `json:"displayName"`
|
|
IconURL string `json:"iconUrl"`
|
|
}
|
|
|
|
type GiftEnergyRuleConfig struct {
|
|
RuleID string `json:"ruleId"`
|
|
GiftID string `json:"giftId"`
|
|
GiftTypeCode string `json:"giftTypeCode"`
|
|
MultiplierPPM int64 `json:"multiplierPpm"`
|
|
FixedEnergy int64 `json:"fixedEnergy"`
|
|
Excluded bool `json:"excluded"`
|
|
}
|
|
|
|
type updateRoomTreasureConfigRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
EnergySource string `json:"energySource"`
|
|
OpenDelayMS int64 `json:"openDelayMs"`
|
|
BroadcastEnabled bool `json:"broadcastEnabled"`
|
|
BroadcastScope string `json:"broadcastScope"`
|
|
BroadcastDelayMS int64 `json:"broadcastDelayMs"`
|
|
RewardStackPolicy string `json:"rewardStackPolicy"`
|
|
Levels []RoomTreasureLevelConfig `json:"levels"`
|
|
GiftEnergyRules []GiftEnergyRuleConfig `json:"giftEnergyRules"`
|
|
}
|
|
|
|
func NewService(roomClient roomclient.Client) *Service {
|
|
return &Service{roomClient: roomClient}
|
|
}
|
|
|
|
// GetConfig 读取当前 App 的语音房宝箱配置;缺行时返回关闭态默认配置,便于首次进入后台页面。
|
|
func (s *Service) GetConfig(ctx context.Context) (RoomTreasureConfig, error) {
|
|
if s.roomClient == nil {
|
|
return RoomTreasureConfig{}, fmt.Errorf("room service client is not configured")
|
|
}
|
|
config, err := s.roomClient.GetRoomTreasureConfig(ctx)
|
|
if err != nil {
|
|
return RoomTreasureConfig{}, err
|
|
}
|
|
return configFromClient(config)
|
|
}
|
|
|
|
// UpdateConfig 保存房间宝箱低频配置;版本号由后台递增,运行时只使用已提交版本。
|
|
func (s *Service) UpdateConfig(ctx context.Context, req updateRoomTreasureConfigRequest, operatorAdminID int64) (RoomTreasureConfig, error) {
|
|
if s.roomClient == nil {
|
|
return RoomTreasureConfig{}, fmt.Errorf("room service client is not configured")
|
|
}
|
|
config, err := normalizeConfig(RoomTreasureConfig{
|
|
Enabled: req.Enabled,
|
|
EnergySource: req.EnergySource,
|
|
OpenDelayMS: req.OpenDelayMS,
|
|
BroadcastEnabled: req.BroadcastEnabled,
|
|
BroadcastScope: req.BroadcastScope,
|
|
BroadcastDelayMS: req.BroadcastDelayMS,
|
|
RewardStackPolicy: req.RewardStackPolicy,
|
|
Levels: req.Levels,
|
|
GiftEnergyRules: req.GiftEnergyRules,
|
|
})
|
|
if err != nil {
|
|
return RoomTreasureConfig{}, err
|
|
}
|
|
saved, err := s.roomClient.UpdateRoomTreasureConfig(ctx, roomclient.UpdateRoomTreasureConfigRequest{
|
|
Config: configToClient(config),
|
|
AdminID: operatorAdminID,
|
|
})
|
|
if err != nil {
|
|
return RoomTreasureConfig{}, err
|
|
}
|
|
return configFromClient(saved)
|
|
}
|
|
|
|
func defaultConfig(appCode string) RoomTreasureConfig {
|
|
levels := make([]RoomTreasureLevelConfig, 0, roomTreasureLevelCount)
|
|
for index, threshold := range defaultLevelEnergyThresholds {
|
|
levels = append(levels, RoomTreasureLevelConfig{
|
|
Level: int32(index + 1),
|
|
EnergyThreshold: threshold,
|
|
})
|
|
}
|
|
return RoomTreasureConfig{
|
|
AppCode: appCode,
|
|
EnergySource: defaultEnergySource,
|
|
OpenDelayMS: defaultOpenDelayMS,
|
|
BroadcastEnabled: true,
|
|
BroadcastScope: defaultBroadcastScope,
|
|
RewardStackPolicy: defaultRewardStackPolicy,
|
|
Levels: levels,
|
|
GiftEnergyRules: []GiftEnergyRuleConfig{},
|
|
}
|
|
}
|
|
|
|
func normalizeConfig(config RoomTreasureConfig) (RoomTreasureConfig, error) {
|
|
config.AppCode = strings.TrimSpace(config.AppCode)
|
|
if config.AppCode == "" {
|
|
return RoomTreasureConfig{}, fmt.Errorf("%w: app_code 不能为空", ErrInvalidArgument)
|
|
}
|
|
if config.ConfigVersion < 0 {
|
|
return RoomTreasureConfig{}, fmt.Errorf("%w: 配置版本不能小于 0", ErrInvalidArgument)
|
|
}
|
|
config.EnergySource = defaultString(strings.TrimSpace(config.EnergySource), defaultEnergySource)
|
|
if !stringIn(config.EnergySource, "gift_point_added", "heat_value") {
|
|
return RoomTreasureConfig{}, fmt.Errorf("%w: 能量来源不支持", ErrInvalidArgument)
|
|
}
|
|
if config.OpenDelayMS < 0 {
|
|
return RoomTreasureConfig{}, fmt.Errorf("%w: 开箱倒计时不能小于 0", ErrInvalidArgument)
|
|
}
|
|
config.BroadcastScope = defaultString(strings.TrimSpace(config.BroadcastScope), defaultBroadcastScope)
|
|
if !stringIn(config.BroadcastScope, "none", "region", "global") {
|
|
return RoomTreasureConfig{}, fmt.Errorf("%w: 广播范围不支持", ErrInvalidArgument)
|
|
}
|
|
if config.BroadcastDelayMS < 0 {
|
|
return RoomTreasureConfig{}, fmt.Errorf("%w: 广播延迟不能小于 0", ErrInvalidArgument)
|
|
}
|
|
config.RewardStackPolicy = defaultString(strings.TrimSpace(config.RewardStackPolicy), defaultRewardStackPolicy)
|
|
if !stringIn(config.RewardStackPolicy, "allow_stack", "priority_only") {
|
|
return RoomTreasureConfig{}, fmt.Errorf("%w: 奖励叠加策略不支持", ErrInvalidArgument)
|
|
}
|
|
levels, err := normalizeLevels(config.Levels)
|
|
if err != nil {
|
|
return RoomTreasureConfig{}, err
|
|
}
|
|
config.Levels = levels
|
|
giftEnergyRules, err := normalizeGiftEnergyRules(config.GiftEnergyRules)
|
|
if err != nil {
|
|
return RoomTreasureConfig{}, err
|
|
}
|
|
config.GiftEnergyRules = giftEnergyRules
|
|
return config, nil
|
|
}
|
|
|
|
func normalizeLevels(levels []RoomTreasureLevelConfig) ([]RoomTreasureLevelConfig, error) {
|
|
if len(levels) == 0 {
|
|
return defaultConfig("placeholder").Levels, nil
|
|
}
|
|
if len(levels) != roomTreasureLevelCount {
|
|
return nil, fmt.Errorf("%w: 宝箱等级必须配置 7 级", ErrInvalidArgument)
|
|
}
|
|
seen := make(map[int32]bool, roomTreasureLevelCount)
|
|
normalized := make([]RoomTreasureLevelConfig, 0, roomTreasureLevelCount)
|
|
for _, level := range levels {
|
|
if level.Level < 1 || level.Level > roomTreasureLevelCount {
|
|
return nil, fmt.Errorf("%w: 宝箱等级只能是 1 到 7", ErrInvalidArgument)
|
|
}
|
|
if seen[level.Level] {
|
|
return nil, fmt.Errorf("%w: 宝箱等级不能重复", ErrInvalidArgument)
|
|
}
|
|
seen[level.Level] = true
|
|
if level.EnergyThreshold <= 0 {
|
|
return nil, fmt.Errorf("%w: 第 %d 级能量阈值必须大于 0", ErrInvalidArgument, level.Level)
|
|
}
|
|
level.CoverURL = strings.TrimSpace(level.CoverURL)
|
|
level.AnimationURL = strings.TrimSpace(level.AnimationURL)
|
|
level.OpeningAnimationURL = strings.TrimSpace(level.OpeningAnimationURL)
|
|
level.OpenedImageURL = strings.TrimSpace(level.OpenedImageURL)
|
|
var err error
|
|
if level.InRoomRewards, err = normalizeRewardPool(level.Level, "in_room", level.InRoomRewards); err != nil {
|
|
return nil, err
|
|
}
|
|
if level.Top1Rewards, err = normalizeRewardPool(level.Level, "top1", level.Top1Rewards); err != nil {
|
|
return nil, err
|
|
}
|
|
if level.IgniterRewards, err = normalizeRewardPool(level.Level, "igniter", level.IgniterRewards); err != nil {
|
|
return nil, err
|
|
}
|
|
normalized = append(normalized, level)
|
|
}
|
|
slices.SortFunc(normalized, func(left, right RoomTreasureLevelConfig) int {
|
|
return int(left.Level - right.Level)
|
|
})
|
|
return normalized, nil
|
|
}
|
|
|
|
func normalizeRewardPool(level int32, role string, rewards []RoomTreasureRewardItem) ([]RoomTreasureRewardItem, error) {
|
|
normalized := make([]RoomTreasureRewardItem, 0, len(rewards))
|
|
for index, reward := range rewards {
|
|
reward.RewardItemID = strings.TrimSpace(reward.RewardItemID)
|
|
reward.DisplayName = strings.TrimSpace(reward.DisplayName)
|
|
reward.IconURL = strings.TrimSpace(reward.IconURL)
|
|
if reward.RewardItemID == "" && reward.ResourceGroupID == 0 && reward.Weight == 0 && reward.DisplayName == "" && reward.IconURL == "" {
|
|
continue
|
|
}
|
|
if reward.ResourceGroupID <= 0 {
|
|
return nil, fmt.Errorf("%w: 第 %d 级奖励资源组不能为空", ErrInvalidArgument, level)
|
|
}
|
|
if reward.Weight <= 0 {
|
|
return nil, fmt.Errorf("%w: 第 %d 级奖励权重必须大于 0", ErrInvalidArgument, level)
|
|
}
|
|
if reward.RewardItemID == "" {
|
|
reward.RewardItemID = fmt.Sprintf("level_%d_%s_%d", level, role, index+1)
|
|
}
|
|
normalized = append(normalized, reward)
|
|
}
|
|
return normalized, nil
|
|
}
|
|
|
|
func normalizeGiftEnergyRules(rules []GiftEnergyRuleConfig) ([]GiftEnergyRuleConfig, error) {
|
|
normalized := make([]GiftEnergyRuleConfig, 0, len(rules))
|
|
for index, rule := range rules {
|
|
rule.RuleID = strings.TrimSpace(rule.RuleID)
|
|
rule.GiftID = strings.TrimSpace(rule.GiftID)
|
|
rule.GiftTypeCode = strings.TrimSpace(rule.GiftTypeCode)
|
|
if rule.RuleID == "" && rule.GiftID == "" && rule.GiftTypeCode == "" && rule.MultiplierPPM == 0 && rule.FixedEnergy == 0 && !rule.Excluded {
|
|
continue
|
|
}
|
|
if rule.GiftID == "" && rule.GiftTypeCode == "" {
|
|
return nil, fmt.Errorf("%w: 礼物能量规则必须选择礼物或礼物类型", ErrInvalidArgument)
|
|
}
|
|
if rule.MultiplierPPM < 0 || rule.FixedEnergy < 0 {
|
|
return nil, fmt.Errorf("%w: 礼物能量规则不能配置负数", ErrInvalidArgument)
|
|
}
|
|
if !rule.Excluded && rule.MultiplierPPM == 0 && rule.FixedEnergy == 0 {
|
|
return nil, fmt.Errorf("%w: 礼物能量规则必须配置倍率或固定能量", ErrInvalidArgument)
|
|
}
|
|
if rule.RuleID == "" {
|
|
rule.RuleID = fmt.Sprintf("gift_energy_rule_%d", index+1)
|
|
}
|
|
normalized = append(normalized, rule)
|
|
}
|
|
return normalized, nil
|
|
}
|
|
|
|
func defaultString(value string, fallback string) string {
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
func stringIn(value string, candidates ...string) bool {
|
|
for _, candidate := range candidates {
|
|
if value == candidate {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func configFromClient(input roomclient.RoomTreasureConfig) (RoomTreasureConfig, error) {
|
|
config := RoomTreasureConfig{
|
|
AppCode: input.AppCode,
|
|
Enabled: input.Enabled,
|
|
ConfigVersion: input.ConfigVersion,
|
|
EnergySource: input.EnergySource,
|
|
OpenDelayMS: input.OpenDelayMS,
|
|
BroadcastEnabled: input.BroadcastEnabled,
|
|
BroadcastScope: input.BroadcastScope,
|
|
BroadcastDelayMS: input.BroadcastDelayMS,
|
|
RewardStackPolicy: input.RewardStackPolicy,
|
|
Levels: levelsFromClient(input.Levels),
|
|
GiftEnergyRules: energyRulesFromClient(input.GiftEnergyRules),
|
|
UpdatedByAdminID: input.UpdatedByAdminID,
|
|
CreatedAtMS: input.CreatedAtMS,
|
|
UpdatedAtMS: input.UpdatedAtMS,
|
|
}
|
|
return normalizeConfig(config)
|
|
}
|
|
|
|
func configToClient(input RoomTreasureConfig) roomclient.RoomTreasureConfig {
|
|
return roomclient.RoomTreasureConfig{
|
|
Enabled: input.Enabled,
|
|
EnergySource: input.EnergySource,
|
|
OpenDelayMS: input.OpenDelayMS,
|
|
BroadcastEnabled: input.BroadcastEnabled,
|
|
BroadcastScope: input.BroadcastScope,
|
|
BroadcastDelayMS: input.BroadcastDelayMS,
|
|
RewardStackPolicy: input.RewardStackPolicy,
|
|
Levels: levelsToClient(input.Levels),
|
|
GiftEnergyRules: energyRulesToClient(input.GiftEnergyRules),
|
|
}
|
|
}
|
|
|
|
func levelsFromClient(input []roomclient.RoomTreasureLevelConfig) []RoomTreasureLevelConfig {
|
|
out := make([]RoomTreasureLevelConfig, 0, len(input))
|
|
for _, level := range input {
|
|
out = append(out, RoomTreasureLevelConfig{
|
|
Level: level.Level,
|
|
EnergyThreshold: level.EnergyThreshold,
|
|
CoverURL: level.CoverURL,
|
|
AnimationURL: level.AnimationURL,
|
|
OpeningAnimationURL: level.OpeningAnimationURL,
|
|
OpenedImageURL: level.OpenedImageURL,
|
|
InRoomRewards: rewardsFromClient(level.InRoomRewards),
|
|
Top1Rewards: rewardsFromClient(level.Top1Rewards),
|
|
IgniterRewards: rewardsFromClient(level.IgniterRewards),
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func levelsToClient(input []RoomTreasureLevelConfig) []roomclient.RoomTreasureLevelConfig {
|
|
out := make([]roomclient.RoomTreasureLevelConfig, 0, len(input))
|
|
for _, level := range input {
|
|
out = append(out, roomclient.RoomTreasureLevelConfig{
|
|
Level: level.Level,
|
|
EnergyThreshold: level.EnergyThreshold,
|
|
CoverURL: level.CoverURL,
|
|
AnimationURL: level.AnimationURL,
|
|
OpeningAnimationURL: level.OpeningAnimationURL,
|
|
OpenedImageURL: level.OpenedImageURL,
|
|
InRoomRewards: rewardsToClient(level.InRoomRewards),
|
|
Top1Rewards: rewardsToClient(level.Top1Rewards),
|
|
IgniterRewards: rewardsToClient(level.IgniterRewards),
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func rewardsFromClient(input []roomclient.RoomTreasureRewardItem) []RoomTreasureRewardItem {
|
|
out := make([]RoomTreasureRewardItem, 0, len(input))
|
|
for _, reward := range input {
|
|
out = append(out, RoomTreasureRewardItem{
|
|
RewardItemID: reward.RewardItemID,
|
|
ResourceGroupID: reward.ResourceGroupID,
|
|
Weight: reward.Weight,
|
|
DisplayName: reward.DisplayName,
|
|
IconURL: reward.IconURL,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func rewardsToClient(input []RoomTreasureRewardItem) []roomclient.RoomTreasureRewardItem {
|
|
out := make([]roomclient.RoomTreasureRewardItem, 0, len(input))
|
|
for _, reward := range input {
|
|
out = append(out, roomclient.RoomTreasureRewardItem{
|
|
RewardItemID: reward.RewardItemID,
|
|
ResourceGroupID: reward.ResourceGroupID,
|
|
Weight: reward.Weight,
|
|
DisplayName: reward.DisplayName,
|
|
IconURL: reward.IconURL,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func energyRulesFromClient(input []roomclient.GiftEnergyRuleConfig) []GiftEnergyRuleConfig {
|
|
out := make([]GiftEnergyRuleConfig, 0, len(input))
|
|
for _, rule := range input {
|
|
out = append(out, GiftEnergyRuleConfig{
|
|
RuleID: rule.RuleID,
|
|
GiftID: rule.GiftID,
|
|
GiftTypeCode: rule.GiftTypeCode,
|
|
MultiplierPPM: rule.MultiplierPPM,
|
|
FixedEnergy: rule.FixedEnergy,
|
|
Excluded: rule.Excluded,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func energyRulesToClient(input []GiftEnergyRuleConfig) []roomclient.GiftEnergyRuleConfig {
|
|
out := make([]roomclient.GiftEnergyRuleConfig, 0, len(input))
|
|
for _, rule := range input {
|
|
out = append(out, roomclient.GiftEnergyRuleConfig{
|
|
RuleID: rule.RuleID,
|
|
GiftID: rule.GiftID,
|
|
GiftTypeCode: rule.GiftTypeCode,
|
|
MultiplierPPM: rule.MultiplierPPM,
|
|
FixedEnergy: rule.FixedEnergy,
|
|
Excluded: rule.Excluded,
|
|
})
|
|
}
|
|
return out
|
|
}
|