442 lines
16 KiB
Go
442 lines
16 KiB
Go
package roomrocket
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"slices"
|
||
"strings"
|
||
|
||
"hyapp-admin-server/internal/appctx"
|
||
"hyapp-admin-server/internal/integration/roomclient"
|
||
)
|
||
|
||
const (
|
||
roomRocketLevelCount = 5
|
||
|
||
defaultFuelSource = "heat_value"
|
||
defaultLaunchDelayMS = int64(30_000)
|
||
defaultBroadcastScope = "region"
|
||
defaultRewardStackPolicy = "allow_stack"
|
||
)
|
||
|
||
var (
|
||
ErrInvalidArgument = errors.New("invalid argument")
|
||
|
||
defaultLevelFuelThresholds = []int64{1_000, 3_000, 6_000, 10_000, 15_000}
|
||
)
|
||
|
||
type Service struct {
|
||
roomClient roomclient.Client
|
||
}
|
||
|
||
type RoomRocketConfig struct {
|
||
AppCode string `json:"appCode"`
|
||
Enabled bool `json:"enabled"`
|
||
ConfigVersion int64 `json:"configVersion"`
|
||
FuelSource string `json:"fuelSource"`
|
||
LaunchDelayMS int64 `json:"launchDelayMs"`
|
||
BroadcastEnabled bool `json:"broadcastEnabled"`
|
||
BroadcastScope string `json:"broadcastScope"`
|
||
BroadcastDelayMS int64 `json:"broadcastDelayMs"`
|
||
RewardStackPolicy string `json:"rewardStackPolicy"`
|
||
Levels []RoomRocketLevelConfig `json:"levels"`
|
||
GiftFuelRules []GiftFuelRuleConfig `json:"giftFuelRules"`
|
||
UpdatedByAdminID int64 `json:"updatedByAdminId"`
|
||
CreatedAtMS int64 `json:"createdAtMs"`
|
||
UpdatedAtMS int64 `json:"updatedAtMs"`
|
||
}
|
||
|
||
type RoomRocketLevelConfig struct {
|
||
Level int32 `json:"level"`
|
||
FuelThreshold int64 `json:"fuelThreshold"`
|
||
CoverURL string `json:"coverUrl"`
|
||
AnimationURL string `json:"animationUrl"`
|
||
LaunchAnimationURL string `json:"launchAnimationUrl"`
|
||
LaunchedImageURL string `json:"launchedImageUrl"`
|
||
InRoomRewards []RoomRocketRewardItem `json:"inRoomRewards"`
|
||
Top1Rewards []RoomRocketRewardItem `json:"top1Rewards"`
|
||
IgniterRewards []RoomRocketRewardItem `json:"igniterRewards"`
|
||
}
|
||
|
||
type RoomRocketRewardItem struct {
|
||
RewardItemID string `json:"rewardItemId"`
|
||
ResourceGroupID int64 `json:"resourceGroupId"`
|
||
Weight int64 `json:"weight"`
|
||
Quantity int64 `json:"quantity"`
|
||
DisplayName string `json:"displayName"`
|
||
IconURL string `json:"iconUrl"`
|
||
}
|
||
|
||
type GiftFuelRuleConfig struct {
|
||
RuleID string `json:"ruleId"`
|
||
GiftID string `json:"giftId"`
|
||
GiftTypeCode string `json:"giftTypeCode"`
|
||
MultiplierPPM int64 `json:"multiplierPpm"`
|
||
FixedFuel int64 `json:"fixedFuel"`
|
||
Excluded bool `json:"excluded"`
|
||
}
|
||
|
||
type updateRoomRocketConfigRequest struct {
|
||
Enabled bool `json:"enabled"`
|
||
FuelSource string `json:"fuelSource"`
|
||
LaunchDelayMS int64 `json:"launchDelayMs"`
|
||
BroadcastEnabled bool `json:"broadcastEnabled"`
|
||
BroadcastScope string `json:"broadcastScope"`
|
||
BroadcastDelayMS int64 `json:"broadcastDelayMs"`
|
||
RewardStackPolicy string `json:"rewardStackPolicy"`
|
||
Levels []RoomRocketLevelConfig `json:"levels"`
|
||
GiftFuelRules []GiftFuelRuleConfig `json:"giftFuelRules"`
|
||
}
|
||
|
||
func NewService(roomClient roomclient.Client) *Service {
|
||
return &Service{roomClient: roomClient}
|
||
}
|
||
|
||
// GetConfig 读取当前 App 的语音房火箭配置;缺行时返回关闭态默认配置,便于首次进入后台页面。
|
||
func (s *Service) GetConfig(ctx context.Context) (RoomRocketConfig, error) {
|
||
if s.roomClient == nil {
|
||
return RoomRocketConfig{}, fmt.Errorf("room service client is not configured")
|
||
}
|
||
config, err := s.roomClient.GetRoomRocketConfig(ctx)
|
||
if err != nil {
|
||
return RoomRocketConfig{}, err
|
||
}
|
||
return configFromClient(config)
|
||
}
|
||
|
||
// UpdateConfig 保存房间火箭低频配置;版本号由后台递增,运行时只使用已提交版本。
|
||
func (s *Service) UpdateConfig(ctx context.Context, req updateRoomRocketConfigRequest, operatorAdminID int64) (RoomRocketConfig, error) {
|
||
if s.roomClient == nil {
|
||
return RoomRocketConfig{}, fmt.Errorf("room service client is not configured")
|
||
}
|
||
config, err := normalizeConfig(configFromUpdateRequest(ctx, req))
|
||
if err != nil {
|
||
return RoomRocketConfig{}, err
|
||
}
|
||
saved, err := s.roomClient.UpdateRoomRocketConfig(ctx, roomclient.UpdateRoomRocketConfigRequest{
|
||
Config: configToClient(config),
|
||
AdminID: operatorAdminID,
|
||
})
|
||
if err != nil {
|
||
return RoomRocketConfig{}, err
|
||
}
|
||
return configFromClient(saved)
|
||
}
|
||
|
||
func configFromUpdateRequest(ctx context.Context, req updateRoomRocketConfigRequest) RoomRocketConfig {
|
||
// 房间火箭配置按 App 隔离;更新请求体不让前端传 app_code,必须使用中间件写入的上下文值。
|
||
return RoomRocketConfig{
|
||
AppCode: appctx.FromContext(ctx),
|
||
Enabled: req.Enabled,
|
||
FuelSource: req.FuelSource,
|
||
LaunchDelayMS: req.LaunchDelayMS,
|
||
BroadcastEnabled: req.BroadcastEnabled,
|
||
BroadcastScope: req.BroadcastScope,
|
||
BroadcastDelayMS: req.BroadcastDelayMS,
|
||
RewardStackPolicy: req.RewardStackPolicy,
|
||
Levels: req.Levels,
|
||
GiftFuelRules: req.GiftFuelRules,
|
||
}
|
||
}
|
||
|
||
func defaultConfig(appCode string) RoomRocketConfig {
|
||
levels := make([]RoomRocketLevelConfig, 0, roomRocketLevelCount)
|
||
for index, threshold := range defaultLevelFuelThresholds {
|
||
levels = append(levels, RoomRocketLevelConfig{
|
||
Level: int32(index + 1),
|
||
FuelThreshold: threshold,
|
||
})
|
||
}
|
||
return RoomRocketConfig{
|
||
AppCode: appCode,
|
||
FuelSource: defaultFuelSource,
|
||
LaunchDelayMS: defaultLaunchDelayMS,
|
||
BroadcastEnabled: true,
|
||
BroadcastScope: defaultBroadcastScope,
|
||
RewardStackPolicy: defaultRewardStackPolicy,
|
||
Levels: levels,
|
||
GiftFuelRules: []GiftFuelRuleConfig{},
|
||
}
|
||
}
|
||
|
||
func normalizeConfig(config RoomRocketConfig) (RoomRocketConfig, error) {
|
||
config.AppCode = strings.TrimSpace(config.AppCode)
|
||
if config.AppCode == "" {
|
||
return RoomRocketConfig{}, fmt.Errorf("%w: app_code 不能为空", ErrInvalidArgument)
|
||
}
|
||
if config.ConfigVersion < 0 {
|
||
return RoomRocketConfig{}, fmt.Errorf("%w: 配置版本不能小于 0", ErrInvalidArgument)
|
||
}
|
||
config.FuelSource = defaultString(strings.TrimSpace(config.FuelSource), defaultFuelSource)
|
||
if config.FuelSource != defaultFuelSource {
|
||
return RoomRocketConfig{}, fmt.Errorf("%w: 燃料来源不支持", ErrInvalidArgument)
|
||
}
|
||
if config.LaunchDelayMS < 0 {
|
||
return RoomRocketConfig{}, fmt.Errorf("%w: 发射倒计时不能小于 0", ErrInvalidArgument)
|
||
}
|
||
config.BroadcastScope = defaultString(strings.TrimSpace(config.BroadcastScope), defaultBroadcastScope)
|
||
if !stringIn(config.BroadcastScope, "none", "region", "global") {
|
||
return RoomRocketConfig{}, fmt.Errorf("%w: 广播范围不支持", ErrInvalidArgument)
|
||
}
|
||
if config.BroadcastDelayMS < 0 {
|
||
return RoomRocketConfig{}, fmt.Errorf("%w: 广播延迟不能小于 0", ErrInvalidArgument)
|
||
}
|
||
config.RewardStackPolicy = defaultString(strings.TrimSpace(config.RewardStackPolicy), defaultRewardStackPolicy)
|
||
if !stringIn(config.RewardStackPolicy, "allow_stack", "priority_only") {
|
||
return RoomRocketConfig{}, fmt.Errorf("%w: 奖励叠加策略不支持", ErrInvalidArgument)
|
||
}
|
||
levels, err := normalizeLevels(config.Levels)
|
||
if err != nil {
|
||
return RoomRocketConfig{}, err
|
||
}
|
||
config.Levels = levels
|
||
giftFuelRules, err := normalizeGiftFuelRules(config.GiftFuelRules)
|
||
if err != nil {
|
||
return RoomRocketConfig{}, err
|
||
}
|
||
config.GiftFuelRules = giftFuelRules
|
||
return config, nil
|
||
}
|
||
|
||
func normalizeLevels(levels []RoomRocketLevelConfig) ([]RoomRocketLevelConfig, error) {
|
||
if len(levels) == 0 {
|
||
return defaultConfig("placeholder").Levels, nil
|
||
}
|
||
if len(levels) != roomRocketLevelCount {
|
||
return nil, fmt.Errorf("%w: 火箭等级必须配置 5 级", ErrInvalidArgument)
|
||
}
|
||
seen := make(map[int32]bool, roomRocketLevelCount)
|
||
normalized := make([]RoomRocketLevelConfig, 0, roomRocketLevelCount)
|
||
for _, level := range levels {
|
||
if level.Level < 1 || level.Level > roomRocketLevelCount {
|
||
return nil, fmt.Errorf("%w: 火箭等级只能是 1 到 5", ErrInvalidArgument)
|
||
}
|
||
if seen[level.Level] {
|
||
return nil, fmt.Errorf("%w: 火箭等级不能重复", ErrInvalidArgument)
|
||
}
|
||
seen[level.Level] = true
|
||
if level.FuelThreshold <= 0 {
|
||
return nil, fmt.Errorf("%w: 第 %d 级燃料阈值必须大于 0", ErrInvalidArgument, level.Level)
|
||
}
|
||
level.CoverURL = strings.TrimSpace(level.CoverURL)
|
||
level.AnimationURL = strings.TrimSpace(level.AnimationURL)
|
||
level.LaunchAnimationURL = strings.TrimSpace(level.LaunchAnimationURL)
|
||
level.LaunchedImageURL = strings.TrimSpace(level.LaunchedImageURL)
|
||
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 RoomRocketLevelConfig) int {
|
||
return int(left.Level - right.Level)
|
||
})
|
||
return normalized, nil
|
||
}
|
||
|
||
func normalizeRewardPool(level int32, role string, rewards []RoomRocketRewardItem) ([]RoomRocketRewardItem, error) {
|
||
normalized := make([]RoomRocketRewardItem, 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.Quantity == 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.Quantity <= 0 {
|
||
// 历史配置没有 quantity 字段;后台读写都按 1 份兼容,避免旧配置打开页面即失败。
|
||
reward.Quantity = 1
|
||
}
|
||
if reward.RewardItemID == "" {
|
||
reward.RewardItemID = fmt.Sprintf("level_%d_%s_%d", level, role, index+1)
|
||
}
|
||
normalized = append(normalized, reward)
|
||
}
|
||
return normalized, nil
|
||
}
|
||
|
||
func normalizeGiftFuelRules(rules []GiftFuelRuleConfig) ([]GiftFuelRuleConfig, error) {
|
||
normalized := make([]GiftFuelRuleConfig, 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.FixedFuel == 0 && !rule.Excluded {
|
||
continue
|
||
}
|
||
if rule.GiftID == "" && rule.GiftTypeCode == "" {
|
||
return nil, fmt.Errorf("%w: 礼物燃料规则必须选择礼物或礼物类型", ErrInvalidArgument)
|
||
}
|
||
if rule.MultiplierPPM < 0 || rule.FixedFuel < 0 {
|
||
return nil, fmt.Errorf("%w: 礼物燃料规则不能配置负数", ErrInvalidArgument)
|
||
}
|
||
if !rule.Excluded && rule.MultiplierPPM == 0 && rule.FixedFuel == 0 {
|
||
return nil, fmt.Errorf("%w: 礼物燃料规则必须配置倍率或固定燃料", ErrInvalidArgument)
|
||
}
|
||
if rule.RuleID == "" {
|
||
rule.RuleID = fmt.Sprintf("gift_fuel_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.RoomRocketConfig) (RoomRocketConfig, error) {
|
||
config := RoomRocketConfig{
|
||
AppCode: input.AppCode,
|
||
Enabled: input.Enabled,
|
||
ConfigVersion: input.ConfigVersion,
|
||
FuelSource: input.FuelSource,
|
||
LaunchDelayMS: input.LaunchDelayMS,
|
||
BroadcastEnabled: input.BroadcastEnabled,
|
||
BroadcastScope: input.BroadcastScope,
|
||
BroadcastDelayMS: input.BroadcastDelayMS,
|
||
RewardStackPolicy: input.RewardStackPolicy,
|
||
Levels: levelsFromClient(input.Levels),
|
||
GiftFuelRules: fuelRulesFromClient(input.GiftFuelRules),
|
||
UpdatedByAdminID: input.UpdatedByAdminID,
|
||
CreatedAtMS: input.CreatedAtMS,
|
||
UpdatedAtMS: input.UpdatedAtMS,
|
||
}
|
||
return normalizeConfig(config)
|
||
}
|
||
|
||
func configToClient(input RoomRocketConfig) roomclient.RoomRocketConfig {
|
||
return roomclient.RoomRocketConfig{
|
||
Enabled: input.Enabled,
|
||
FuelSource: input.FuelSource,
|
||
LaunchDelayMS: input.LaunchDelayMS,
|
||
BroadcastEnabled: input.BroadcastEnabled,
|
||
BroadcastScope: input.BroadcastScope,
|
||
BroadcastDelayMS: input.BroadcastDelayMS,
|
||
RewardStackPolicy: input.RewardStackPolicy,
|
||
Levels: levelsToClient(input.Levels),
|
||
GiftFuelRules: fuelRulesToClient(input.GiftFuelRules),
|
||
}
|
||
}
|
||
|
||
func levelsFromClient(input []roomclient.RoomRocketLevelConfig) []RoomRocketLevelConfig {
|
||
out := make([]RoomRocketLevelConfig, 0, len(input))
|
||
for _, level := range input {
|
||
out = append(out, RoomRocketLevelConfig{
|
||
Level: level.Level,
|
||
FuelThreshold: level.FuelThreshold,
|
||
CoverURL: level.CoverURL,
|
||
AnimationURL: level.AnimationURL,
|
||
LaunchAnimationURL: level.LaunchAnimationURL,
|
||
LaunchedImageURL: level.LaunchedImageURL,
|
||
InRoomRewards: rewardsFromClient(level.InRoomRewards),
|
||
Top1Rewards: rewardsFromClient(level.Top1Rewards),
|
||
IgniterRewards: rewardsFromClient(level.IgniterRewards),
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func levelsToClient(input []RoomRocketLevelConfig) []roomclient.RoomRocketLevelConfig {
|
||
out := make([]roomclient.RoomRocketLevelConfig, 0, len(input))
|
||
for _, level := range input {
|
||
out = append(out, roomclient.RoomRocketLevelConfig{
|
||
Level: level.Level,
|
||
FuelThreshold: level.FuelThreshold,
|
||
CoverURL: level.CoverURL,
|
||
AnimationURL: level.AnimationURL,
|
||
LaunchAnimationURL: level.LaunchAnimationURL,
|
||
LaunchedImageURL: level.LaunchedImageURL,
|
||
InRoomRewards: rewardsToClient(level.InRoomRewards),
|
||
Top1Rewards: rewardsToClient(level.Top1Rewards),
|
||
IgniterRewards: rewardsToClient(level.IgniterRewards),
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func rewardsFromClient(input []roomclient.RoomRocketRewardItem) []RoomRocketRewardItem {
|
||
out := make([]RoomRocketRewardItem, 0, len(input))
|
||
for _, reward := range input {
|
||
out = append(out, RoomRocketRewardItem{
|
||
RewardItemID: reward.RewardItemID,
|
||
ResourceGroupID: reward.ResourceGroupID,
|
||
Weight: reward.Weight,
|
||
Quantity: reward.Quantity,
|
||
DisplayName: reward.DisplayName,
|
||
IconURL: reward.IconURL,
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func rewardsToClient(input []RoomRocketRewardItem) []roomclient.RoomRocketRewardItem {
|
||
out := make([]roomclient.RoomRocketRewardItem, 0, len(input))
|
||
for _, reward := range input {
|
||
out = append(out, roomclient.RoomRocketRewardItem{
|
||
RewardItemID: reward.RewardItemID,
|
||
ResourceGroupID: reward.ResourceGroupID,
|
||
Weight: reward.Weight,
|
||
Quantity: reward.Quantity,
|
||
DisplayName: reward.DisplayName,
|
||
IconURL: reward.IconURL,
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func fuelRulesFromClient(input []roomclient.GiftFuelRuleConfig) []GiftFuelRuleConfig {
|
||
out := make([]GiftFuelRuleConfig, 0, len(input))
|
||
for _, rule := range input {
|
||
out = append(out, GiftFuelRuleConfig{
|
||
RuleID: rule.RuleID,
|
||
GiftID: rule.GiftID,
|
||
GiftTypeCode: rule.GiftTypeCode,
|
||
MultiplierPPM: rule.MultiplierPPM,
|
||
FixedFuel: rule.FixedFuel,
|
||
Excluded: rule.Excluded,
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func fuelRulesToClient(input []GiftFuelRuleConfig) []roomclient.GiftFuelRuleConfig {
|
||
out := make([]roomclient.GiftFuelRuleConfig, 0, len(input))
|
||
for _, rule := range input {
|
||
out = append(out, roomclient.GiftFuelRuleConfig{
|
||
RuleID: rule.RuleID,
|
||
GiftID: rule.GiftID,
|
||
GiftTypeCode: rule.GiftTypeCode,
|
||
MultiplierPPM: rule.MultiplierPPM,
|
||
FixedFuel: rule.FixedFuel,
|
||
Excluded: rule.Excluded,
|
||
})
|
||
}
|
||
return out
|
||
}
|