432 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package roomtreasure
import (
"context"
"errors"
"fmt"
"slices"
"strings"
"hyapp-admin-server/internal/integration/roomclient"
)
const (
roomTreasureLevelCount = 7
defaultEnergySource = "heat_value"
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 config.EnergySource == "gift_point_added" {
// gift_point_added 是历史配置值GIFT_POINT 已下线,后台读取旧配置时统一切到真实礼物贡献口径。
config.EnergySource = defaultEnergySource
}
if config.EnergySource != defaultEnergySource {
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
}