feat: complete voice room rocket launch rewards
This commit is contained in:
parent
dd9020a542
commit
51b8b52c57
@ -61,6 +61,7 @@ func main() {
|
|||||||
voiceRoomRedPacketService := voiceroomredpacket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet, regionIMGroupService, app.Gateways.Room)
|
voiceRoomRedPacketService := voiceroomredpacket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet, regionIMGroupService, app.Gateways.Room)
|
||||||
voiceRoomRedPacketService.SetRegionResolver(&app.Gateways)
|
voiceRoomRedPacketService.SetRegionResolver(&app.Gateways)
|
||||||
voiceRoomRocketService := voiceroomrocket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
voiceRoomRocketService := voiceroomrocket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
|
voiceRoomRocketService.SetRegionBroadcaster(regionIMGroupService)
|
||||||
weekStarService := weekstar.NewWeekStarService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
weekStarService := weekstar.NewWeekStarService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
roomTurnoverRewardService := roomturnoverreward.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
roomTurnoverRewardService := roomturnoverreward.NewService(app.Config, app.Repository.DB, &app.Gateways)
|
||||||
hostCenterService := hostcenter.NewService(app.Config, &app.Gateways)
|
hostCenterService := hostcenter.NewService(app.Config, &app.Gateways)
|
||||||
|
|||||||
@ -31,6 +31,7 @@ func main() {
|
|||||||
voiceRoomRedPacketService := voiceroomredpacket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet, regionIMGroupService, app.Gateways.Room)
|
voiceRoomRedPacketService := voiceroomredpacket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, app.Gateways.Wallet, regionIMGroupService, app.Gateways.Room)
|
||||||
voiceRoomRedPacketService.SetRegionResolver(&app.Gateways)
|
voiceRoomRedPacketService.SetRegionResolver(&app.Gateways)
|
||||||
voiceRoomRocketService := voiceroomrocket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
voiceRoomRocketService := voiceroomrocket.NewService(app.Config, app.Repository.DB, app.Repository.Redis, &app.Gateways)
|
||||||
|
voiceRoomRocketService.SetRegionBroadcaster(regionIMGroupService)
|
||||||
|
|
||||||
workerCtx, workerCancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
workerCtx, workerCancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
defer workerCancel()
|
defer workerCancel()
|
||||||
|
|||||||
@ -39,13 +39,12 @@ func (s *Service) GetAdminConfig(ctx context.Context, sysOrigin string) (*Config
|
|||||||
// SaveAdminConfig 保存后台系统配置。
|
// SaveAdminConfig 保存后台系统配置。
|
||||||
func (s *Service) SaveAdminConfig(ctx context.Context, req SaveConfigRequest) (*ConfigResponse, error) {
|
func (s *Service) SaveAdminConfig(ctx context.Context, req SaveConfigRequest) (*ConfigResponse, error) {
|
||||||
sysOrigin := s.normalizeSysOrigin(req.SysOrigin)
|
sysOrigin := s.normalizeSysOrigin(req.SysOrigin)
|
||||||
timezone := normalizeTimezone(req.Timezone)
|
timezone := defaultTimezone
|
||||||
if _, err := time.LoadLocation(timezone); err != nil {
|
|
||||||
return nil, NewAppError(http.StatusBadRequest, "invalid_timezone", err.Error())
|
|
||||||
}
|
|
||||||
maxLevel := req.MaxLevel
|
maxLevel := req.MaxLevel
|
||||||
if maxLevel <= 0 {
|
if maxLevel <= 0 {
|
||||||
maxLevel = defaultMaxLevel
|
maxLevel = defaultMaxLevel
|
||||||
|
} else if maxLevel > defaultMaxLevel {
|
||||||
|
maxLevel = defaultMaxLevel
|
||||||
}
|
}
|
||||||
rankingLimit := req.RankingTopLimit
|
rankingLimit := req.RankingTopLimit
|
||||||
if rankingLimit <= 0 || rankingLimit > 100 {
|
if rankingLimit <= 0 || rankingLimit > 100 {
|
||||||
@ -206,11 +205,28 @@ func (s *Service) ListRewardConfigs(ctx context.Context, sysOrigin string, level
|
|||||||
// SaveRewardConfigs 批量保存后台奖励配置。
|
// SaveRewardConfigs 批量保存后台奖励配置。
|
||||||
func (s *Service) SaveRewardConfigs(ctx context.Context, req SaveRewardConfigsRequest) ([]RewardConfigPayload, error) {
|
func (s *Service) SaveRewardConfigs(ctx context.Context, req SaveRewardConfigsRequest) ([]RewardConfigPayload, error) {
|
||||||
sysOrigin := s.normalizeSysOrigin(req.SysOrigin)
|
sysOrigin := s.normalizeSysOrigin(req.SysOrigin)
|
||||||
if len(req.Rewards) == 0 {
|
targetLevels := map[int]struct{}{}
|
||||||
return nil, NewAppError(http.StatusBadRequest, "missing_reward_configs", "rewards is required")
|
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 {
|
if err := s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
now := time.Now()
|
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 {
|
for _, item := range req.Rewards {
|
||||||
scene := strings.ToUpper(strings.TrimSpace(item.RewardScene))
|
scene := strings.ToUpper(strings.TrimSpace(item.RewardScene))
|
||||||
if scene != rewardSceneTop1 && scene != rewardSceneIgnite && scene != rewardSceneInRoom {
|
if scene != rewardSceneTop1 && scene != rewardSceneIgnite && scene != rewardSceneInRoom {
|
||||||
@ -219,6 +235,10 @@ func (s *Service) SaveRewardConfigs(ctx context.Context, req SaveRewardConfigsRe
|
|||||||
if item.Level <= 0 || item.Level > defaultMaxLevel {
|
if item.Level <= 0 || item.Level > defaultMaxLevel {
|
||||||
return NewAppError(http.StatusBadRequest, "invalid_level", "level must be between 1 and 6")
|
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))
|
rewardType := strings.ToUpper(strings.TrimSpace(item.RewardType))
|
||||||
if rewardType == "" {
|
if rewardType == "" {
|
||||||
return NewAppError(http.StatusBadRequest, "missing_reward_type", "rewardType is required")
|
return NewAppError(http.StatusBadRequest, "missing_reward_type", "rewardType is required")
|
||||||
@ -226,12 +246,22 @@ func (s *Service) SaveRewardConfigs(ctx context.Context, req SaveRewardConfigsRe
|
|||||||
if !isSupportedRewardType(rewardType) {
|
if !isSupportedRewardType(rewardType) {
|
||||||
return NewAppError(http.StatusBadRequest, "invalid_reward_type", "rewardType is invalid")
|
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)
|
rewardName := strings.TrimSpace(item.RewardName)
|
||||||
|
if rewardType == rewardTypeNone && rewardName == "" {
|
||||||
|
rewardName = "轮空"
|
||||||
|
}
|
||||||
if rewardName == "" {
|
if rewardName == "" {
|
||||||
return NewAppError(http.StatusBadRequest, "missing_reward_name", "rewardName is required")
|
return NewAppError(http.StatusBadRequest, "missing_reward_name", "rewardName is required")
|
||||||
}
|
}
|
||||||
amount := item.RewardAmount
|
amount := item.RewardAmount
|
||||||
if amount <= 0 {
|
if rewardType == rewardTypeNone {
|
||||||
|
amount = 0
|
||||||
|
item.RewardItemID = nil
|
||||||
|
item.ExpireDays = nil
|
||||||
|
} else if amount <= 0 {
|
||||||
amount = 1
|
amount = 1
|
||||||
}
|
}
|
||||||
var row model.VoiceRoomRocketRewardConfig
|
var row model.VoiceRoomRocketRewardConfig
|
||||||
@ -268,12 +298,22 @@ func (s *Service) SaveRewardConfigs(ctx context.Context, req SaveRewardConfigsRe
|
|||||||
if err := tx.Save(&row).Error; err != nil {
|
if err := tx.Save(&row).Error; err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return s.ListRewardConfigs(ctx, sysOrigin, 0)
|
return s.ListRewardConfigs(ctx, sysOrigin, req.Level)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) getConfigResponse(ctx context.Context, sysOrigin string, currentLevel int) (*ConfigResponse, error) {
|
func (s *Service) getConfigResponse(ctx context.Context, sysOrigin string, currentLevel int) (*ConfigResponse, error) {
|
||||||
@ -304,7 +344,7 @@ func (s *Service) loadConfig(ctx context.Context, sysOrigin string) (configSnaps
|
|||||||
return configSnapshot{
|
return configSnapshot{
|
||||||
SysOrigin: sysOrigin,
|
SysOrigin: sysOrigin,
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
Timezone: normalizeTimezone(s.cfg.VoiceRoomRocket.Timezone),
|
Timezone: defaultTimezone,
|
||||||
MaxLevel: defaultMaxLevel,
|
MaxLevel: defaultMaxLevel,
|
||||||
RankingTopLimit: defaultRankingTopLimit,
|
RankingTopLimit: defaultRankingTopLimit,
|
||||||
RewardRecordKeepDays: defaultRewardRecordKeepDays,
|
RewardRecordKeepDays: defaultRewardRecordKeepDays,
|
||||||
@ -320,7 +360,7 @@ func (s *Service) loadConfig(ctx context.Context, sysOrigin string) (configSnaps
|
|||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
SysOrigin: row.SysOrigin,
|
SysOrigin: row.SysOrigin,
|
||||||
Enabled: row.Enabled,
|
Enabled: row.Enabled,
|
||||||
Timezone: normalizeTimezone(row.Timezone),
|
Timezone: defaultTimezone,
|
||||||
MaxLevel: positiveOrDefault(row.MaxLevel, defaultMaxLevel),
|
MaxLevel: positiveOrDefault(row.MaxLevel, defaultMaxLevel),
|
||||||
RankingTopLimit: clampPositive(row.RankingTopLimit, defaultRankingTopLimit, 100),
|
RankingTopLimit: clampPositive(row.RankingTopLimit, defaultRankingTopLimit, 100),
|
||||||
RewardRecordKeepDays: positiveOrDefault(row.RewardRecordKeepDays, defaultRewardRecordKeepDays),
|
RewardRecordKeepDays: positiveOrDefault(row.RewardRecordKeepDays, defaultRewardRecordKeepDays),
|
||||||
|
|||||||
@ -193,7 +193,10 @@ func (s *Service) ProcessGiftEvent(ctx context.Context, event giftEvent, rawPayl
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if newEnergy >= statusRow.NeedEnergy {
|
if newEnergy >= statusRow.NeedEnergy {
|
||||||
launch, err := s.launchLocked(ctx, tx, statusRow, sendUserID, now)
|
if err := s.markLevelReachedLocked(ctx, tx, statusRow, sendUserID, now); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
launch, err := s.scheduleLaunchLocked(ctx, tx, statusRow, sendUserID, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -209,7 +212,7 @@ func (s *Service) ProcessGiftEvent(ctx context.Context, event giftEvent, rawPayl
|
|||||||
if result.Launched {
|
if result.Launched {
|
||||||
_ = s.notifyLaunch(context.Background(), result.Launch)
|
_ = s.notifyLaunch(context.Background(), result.Launch)
|
||||||
} else if resp.Processed {
|
} else if resp.Processed {
|
||||||
_ = s.notifyStatusUpdate(context.Background(), sysOrigin, roomID)
|
_ = s.notifyStatusUpdate(context.Background(), sysOrigin, roomID, sendUserID)
|
||||||
}
|
}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@ -292,6 +295,35 @@ func (s *Service) addContribution(ctx context.Context, tx *gorm.DB, statusRow mo
|
|||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) markLevelReachedLocked(ctx context.Context, tx *gorm.DB, statusRow model.VoiceRoomRocketStatus, igniteUserID int64, now time.Time) error {
|
||||||
|
return tx.WithContext(ctx).Model(&model.VoiceRoomRocketLevelRound{}).
|
||||||
|
Where(
|
||||||
|
"sys_origin = ? AND room_id = ? AND day_key = ? AND round_no = ? AND level = ?",
|
||||||
|
statusRow.SysOrigin, statusRow.RoomID, statusRow.DayKey, statusRow.RoundNo, statusRow.CurrentLevel,
|
||||||
|
).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"final_energy": statusRow.NeedEnergy,
|
||||||
|
"status": levelRoundStatusReached,
|
||||||
|
"ignite_user_id": igniteUserID,
|
||||||
|
"reach_time": now,
|
||||||
|
"update_time": now,
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) scheduleLaunchLocked(ctx context.Context, tx *gorm.DB, statusRow model.VoiceRoomRocketStatus, igniteUserID int64, now time.Time) (launchResult, error) {
|
||||||
|
if s.repo.Redis == nil {
|
||||||
|
return s.launchLocked(ctx, tx, statusRow, igniteUserID, now)
|
||||||
|
}
|
||||||
|
due := now.Add(time.Duration(defaultLaunchDelaySeconds) * time.Second)
|
||||||
|
if err := s.repo.Redis.ZAdd(ctx, s.launchDueZSetKey(), redis.Z{
|
||||||
|
Score: float64(due.Unix()),
|
||||||
|
Member: strconv.FormatInt(statusRow.ID, 10),
|
||||||
|
}).Err(); err != nil {
|
||||||
|
return launchResult{}, err
|
||||||
|
}
|
||||||
|
return launchResult{Launched: false}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) launchLocked(ctx context.Context, tx *gorm.DB, statusRow model.VoiceRoomRocketStatus, igniteUserID int64, now time.Time) (launchResult, error) {
|
func (s *Service) launchLocked(ctx context.Context, tx *gorm.DB, statusRow model.VoiceRoomRocketStatus, igniteUserID int64, now time.Time) (launchResult, error) {
|
||||||
top1UserID, err := s.resolveTop1UserID(ctx, tx, statusRow)
|
top1UserID, err := s.resolveTop1UserID(ctx, tx, statusRow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -326,7 +358,6 @@ func (s *Service) launchLocked(ctx context.Context, tx *gorm.DB, statusRow model
|
|||||||
}
|
}
|
||||||
return launchResult{}, err
|
return launchResult{}, err
|
||||||
}
|
}
|
||||||
reachTime := now
|
|
||||||
if err := tx.Model(&model.VoiceRoomRocketLevelRound{}).
|
if err := tx.Model(&model.VoiceRoomRocketLevelRound{}).
|
||||||
Where(
|
Where(
|
||||||
"sys_origin = ? AND room_id = ? AND day_key = ? AND round_no = ? AND level = ?",
|
"sys_origin = ? AND room_id = ? AND day_key = ? AND round_no = ? AND level = ?",
|
||||||
@ -337,7 +368,7 @@ func (s *Service) launchLocked(ctx context.Context, tx *gorm.DB, statusRow model
|
|||||||
"status": levelRoundStatusLaunched,
|
"status": levelRoundStatusLaunched,
|
||||||
"top1_user_id": top1UserID,
|
"top1_user_id": top1UserID,
|
||||||
"ignite_user_id": igniteUserID,
|
"ignite_user_id": igniteUserID,
|
||||||
"reach_time": reachTime,
|
"reach_time": gorm.Expr("COALESCE(reach_time, ?)", now),
|
||||||
"launch_time": now,
|
"launch_time": now,
|
||||||
"update_time": now,
|
"update_time": now,
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
|
|||||||
@ -2,7 +2,9 @@ package voiceroomrocket
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -199,11 +201,19 @@ func TestInRoomRewardWorkerSnapshotsSelectsAndGrants(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ProcessGiftPayload() error = %v", err)
|
t.Fatalf("ProcessGiftPayload() error = %v", err)
|
||||||
}
|
}
|
||||||
if !resp.Launched || resp.LaunchNo == "" {
|
if !resp.Processed || resp.Launched || resp.LaunchNo != "" {
|
||||||
t.Fatalf("launch response = %+v", resp)
|
t.Fatalf("launch should be delayed, response = %+v", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
processed, err := service.ProcessDueInRoomRewards(ctx, time.Now().Add(20*time.Second), 10)
|
launched, err := service.ProcessDueLaunches(ctx, time.Now().Add(11*time.Second), 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessDueLaunches() error = %v", err)
|
||||||
|
}
|
||||||
|
if launched != 1 {
|
||||||
|
t.Fatalf("due launches = %d, want 1", launched)
|
||||||
|
}
|
||||||
|
|
||||||
|
processed, err := service.ProcessDueInRoomRewards(ctx, time.Now().Add(30*time.Second), 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ProcessDueInRoomRewards() error = %v", err)
|
t.Fatalf("ProcessDueInRoomRewards() error = %v", err)
|
||||||
}
|
}
|
||||||
@ -245,6 +255,87 @@ func TestInRoomRewardWorkerSnapshotsSelectsAndGrants(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProcessGiftPayloadDelaysLaunchWithRedis(t *testing.T) {
|
||||||
|
gateway := newFakeRocketGateway()
|
||||||
|
service, db, _, mr := newTestServiceWithRedis(t, gateway)
|
||||||
|
defer mr.Close()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
seedRocketConfig(t, db)
|
||||||
|
seedLevelConfigs(t, db, []int64{100, 200, 300, 400, 500, 600})
|
||||||
|
seedRewardConfig(t, db, 1, rewardSceneTop1, rewardTypeGold, 10)
|
||||||
|
|
||||||
|
payload := `{
|
||||||
|
"trackId":"track-launch-delay",
|
||||||
|
"sysOrigin":"LIKEI",
|
||||||
|
"sendUserId":"1001",
|
||||||
|
"roomId":"9001",
|
||||||
|
"quantity":1,
|
||||||
|
"giftConfig":{"id":"11","giftCandy":100,"type":"GOLD","giftTab":"NORMAL"},
|
||||||
|
"createTime":` + strconvFormatMillis(time.Now()) + `
|
||||||
|
}`
|
||||||
|
resp, err := service.ProcessGiftPayload(ctx, payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessGiftPayload() error = %v", err)
|
||||||
|
}
|
||||||
|
if !resp.Processed || resp.Launched {
|
||||||
|
t.Fatalf("response = %+v, want processed delayed launch", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var status model.VoiceRoomRocketStatus
|
||||||
|
if err := db.Where("sys_origin = ? AND room_id = ?", "LIKEI", int64(9001)).First(&status).Error; err != nil {
|
||||||
|
t.Fatalf("load status: %v", err)
|
||||||
|
}
|
||||||
|
if status.Status != statusLaunching || status.CurrentEnergy != 100 {
|
||||||
|
t.Fatalf("status = %+v, want launching full energy", status)
|
||||||
|
}
|
||||||
|
var launchCount int64
|
||||||
|
if err := db.Model(&model.VoiceRoomRocketLaunch{}).Count(&launchCount).Error; err != nil {
|
||||||
|
t.Fatalf("count launch before due: %v", err)
|
||||||
|
}
|
||||||
|
if launchCount != 0 {
|
||||||
|
t.Fatalf("launch count before due = %d, want 0", launchCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
processed, err := service.ProcessDueLaunches(ctx, time.Now().Add(9*time.Second), 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessDueLaunches(before due) error = %v", err)
|
||||||
|
}
|
||||||
|
if processed != 0 {
|
||||||
|
t.Fatalf("launches before due = %d, want 0", processed)
|
||||||
|
}
|
||||||
|
processed, err = service.ProcessDueLaunches(ctx, time.Now().Add(11*time.Second), 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessDueLaunches(after due) error = %v", err)
|
||||||
|
}
|
||||||
|
if processed != 1 {
|
||||||
|
t.Fatalf("launches after due = %d, want 1", processed)
|
||||||
|
}
|
||||||
|
if err := db.Model(&model.VoiceRoomRocketLaunch{}).Count(&launchCount).Error; err != nil {
|
||||||
|
t.Fatalf("count launch after due: %v", err)
|
||||||
|
}
|
||||||
|
if launchCount != 1 {
|
||||||
|
t.Fatalf("launch count after due = %d, want 1", launchCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPickInRoomRewardCanBeEmpty(t *testing.T) {
|
||||||
|
service, _ := newTestService(t)
|
||||||
|
launch := model.VoiceRoomRocketLaunch{LaunchNo: "VRR-empty"}
|
||||||
|
rewards := []model.VoiceRoomRocketRewardConfig{
|
||||||
|
{
|
||||||
|
RewardScene: rewardSceneInRoom,
|
||||||
|
RewardType: rewardTypeNone,
|
||||||
|
RewardName: "轮空",
|
||||||
|
Weight: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
got := service.pickInRoomRewardsForUser(launch, rewards, 1001)
|
||||||
|
if len(got) != 0 {
|
||||||
|
t.Fatalf("picked rewards = %+v, want empty", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNotifyLaunchPublishesIMAndRedisStream(t *testing.T) {
|
func TestNotifyLaunchPublishesIMAndRedisStream(t *testing.T) {
|
||||||
gateway := newFakeRocketGateway()
|
gateway := newFakeRocketGateway()
|
||||||
service, db, rdb, mr := newTestServiceWithRedis(t, gateway)
|
service, db, rdb, mr := newTestServiceWithRedis(t, gateway)
|
||||||
@ -278,6 +369,42 @@ func TestNotifyLaunchPublishesIMAndRedisStream(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStatusUpdatePublishesRegionIM(t *testing.T) {
|
||||||
|
gateway := newFakeRocketGateway()
|
||||||
|
service, db, _, mr := newTestServiceWithRedis(t, gateway)
|
||||||
|
defer mr.Close()
|
||||||
|
service.regionResolver = fakeRocketRegionResolver{regions: map[string]string{"LIKEI|SA": "AR"}}
|
||||||
|
regionIM := &fakeRocketRegionBroadcaster{}
|
||||||
|
service.SetRegionBroadcaster(regionIM)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
seedRocketConfig(t, db)
|
||||||
|
seedLevelConfigs(t, db, []int64{100, 200, 300, 400, 500, 600})
|
||||||
|
|
||||||
|
payload := `{
|
||||||
|
"trackId":"track-region-status",
|
||||||
|
"sysOrigin":"LIKEI",
|
||||||
|
"sendUserId":"1001",
|
||||||
|
"roomId":"9001",
|
||||||
|
"quantity":1,
|
||||||
|
"giftConfig":{"id":"11","giftCandy":10,"type":"GOLD","giftTab":"NORMAL"},
|
||||||
|
"createTime":` + strconvFormatMillis(time.Now()) + `
|
||||||
|
}`
|
||||||
|
resp, err := service.ProcessGiftPayload(ctx, payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessGiftPayload() error = %v", err)
|
||||||
|
}
|
||||||
|
if !resp.Processed {
|
||||||
|
t.Fatalf("response = %+v, want processed", resp)
|
||||||
|
}
|
||||||
|
if len(regionIM.messages) != 1 {
|
||||||
|
t.Fatalf("region messages = %+v, want one", regionIM.messages)
|
||||||
|
}
|
||||||
|
if regionIM.messages[0].regionCode != "AR" || regionIM.messages[0].messageType != imTypeStatusUpdate {
|
||||||
|
t.Fatalf("region message = %+v", regionIM.messages[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDayKeyUsesUTCPlus3BusinessDay(t *testing.T) {
|
func TestDayKeyUsesUTCPlus3BusinessDay(t *testing.T) {
|
||||||
location := resolveLocation("Asia/Riyadh")
|
location := resolveLocation("Asia/Riyadh")
|
||||||
before := time.Date(2026, 5, 13, 20, 59, 59, 0, time.UTC)
|
before := time.Date(2026, 5, 13, 20, 59, 59, 0, time.UTC)
|
||||||
@ -290,6 +417,110 @@ func TestDayKeyUsesUTCPlus3BusinessDay(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSaveAdminConfigForcesRiyadhTimezoneAndReturnsEnergyRule(t *testing.T) {
|
||||||
|
service, _ := newTestService(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
resp, err := service.SaveAdminConfig(ctx, SaveConfigRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Enabled: true,
|
||||||
|
Timezone: "UTC",
|
||||||
|
MaxLevel: 6,
|
||||||
|
RankingTopLimit: 50,
|
||||||
|
RewardRecordKeepDays: 35,
|
||||||
|
BroadcastDurationSeconds: 7,
|
||||||
|
InRoomRewardDelaySeconds: 10,
|
||||||
|
EnergyRuleJSON: json.RawMessage(`{"allowGiftIds":[11]}`),
|
||||||
|
RuleText: json.RawMessage(`{"title":"rocket"}`),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveAdminConfig() error = %v", err)
|
||||||
|
}
|
||||||
|
if resp.Timezone != defaultTimezone {
|
||||||
|
t.Fatalf("timezone = %s, want %s", resp.Timezone, defaultTimezone)
|
||||||
|
}
|
||||||
|
if string(resp.EnergyRuleJSON) != `{"allowGiftIds":[11]}` {
|
||||||
|
t.Fatalf("energyRuleJson = %s", string(resp.EnergyRuleJSON))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveRewardConfigsReplacesAndClearsSelectedLevel(t *testing.T) {
|
||||||
|
service, db := newTestService(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
seedRewardConfig(t, db, 1, rewardSceneTop1, rewardTypeGold, 10)
|
||||||
|
seedRewardConfig(t, db, 1, rewardSceneIgnite, rewardTypeGold, 5)
|
||||||
|
seedRewardConfig(t, db, 2, rewardSceneTop1, rewardTypeGold, 20)
|
||||||
|
|
||||||
|
saved, err := service.SaveRewardConfigs(ctx, SaveRewardConfigsRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Level: 1,
|
||||||
|
Rewards: []RewardConfigPayload{
|
||||||
|
{
|
||||||
|
Level: 1,
|
||||||
|
RewardScene: rewardSceneTop1,
|
||||||
|
RewardType: rewardTypeGold,
|
||||||
|
RewardName: "new gold",
|
||||||
|
RewardAmount: 99,
|
||||||
|
Enabled: true,
|
||||||
|
Sort: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveRewardConfigs(replace) error = %v", err)
|
||||||
|
}
|
||||||
|
if len(saved) != 1 || saved[0].RewardAmount != 99 {
|
||||||
|
t.Fatalf("saved rewards = %+v, want one amount 99", saved)
|
||||||
|
}
|
||||||
|
|
||||||
|
var level1Count int64
|
||||||
|
if err := db.Model(&model.VoiceRoomRocketRewardConfig{}).
|
||||||
|
Where("sys_origin = ? AND level = ?", "LIKEI", 1).
|
||||||
|
Count(&level1Count).Error; err != nil {
|
||||||
|
t.Fatalf("count level 1 rewards: %v", err)
|
||||||
|
}
|
||||||
|
if level1Count != 1 {
|
||||||
|
t.Fatalf("level 1 reward count = %d, want 1", level1Count)
|
||||||
|
}
|
||||||
|
var level2Count int64
|
||||||
|
if err := db.Model(&model.VoiceRoomRocketRewardConfig{}).
|
||||||
|
Where("sys_origin = ? AND level = ?", "LIKEI", 2).
|
||||||
|
Count(&level2Count).Error; err != nil {
|
||||||
|
t.Fatalf("count level 2 rewards: %v", err)
|
||||||
|
}
|
||||||
|
if level2Count != 1 {
|
||||||
|
t.Fatalf("level 2 reward count = %d, want 1", level2Count)
|
||||||
|
}
|
||||||
|
|
||||||
|
saved, err = service.SaveRewardConfigs(ctx, SaveRewardConfigsRequest{
|
||||||
|
SysOrigin: "LIKEI",
|
||||||
|
Level: 1,
|
||||||
|
Rewards: []RewardConfigPayload{},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SaveRewardConfigs(clear) error = %v", err)
|
||||||
|
}
|
||||||
|
if len(saved) != 0 {
|
||||||
|
t.Fatalf("saved rewards after clear = %+v, want empty", saved)
|
||||||
|
}
|
||||||
|
if err := db.Model(&model.VoiceRoomRocketRewardConfig{}).
|
||||||
|
Where("sys_origin = ? AND level = ?", "LIKEI", 1).
|
||||||
|
Count(&level1Count).Error; err != nil {
|
||||||
|
t.Fatalf("count cleared level 1 rewards: %v", err)
|
||||||
|
}
|
||||||
|
if level1Count != 0 {
|
||||||
|
t.Fatalf("level 1 reward count after clear = %d, want 0", level1Count)
|
||||||
|
}
|
||||||
|
if err := db.Model(&model.VoiceRoomRocketRewardConfig{}).
|
||||||
|
Where("sys_origin = ? AND level = ?", "LIKEI", 2).
|
||||||
|
Count(&level2Count).Error; err != nil {
|
||||||
|
t.Fatalf("count retained level 2 rewards: %v", err)
|
||||||
|
}
|
||||||
|
if level2Count != 1 {
|
||||||
|
t.Fatalf("level 2 reward count after clear = %d, want 1", level2Count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newTestService(t *testing.T) (*Service, *gorm.DB) {
|
func newTestService(t *testing.T) (*Service, *gorm.DB) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||||
@ -351,6 +582,21 @@ type fakeRocketIMMessage struct {
|
|||||||
body any
|
body any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fakeRocketRegionResolver struct {
|
||||||
|
regions map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeRocketRegionBroadcaster struct {
|
||||||
|
messages []fakeRocketRegionMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeRocketRegionMessage struct {
|
||||||
|
sysOrigin string
|
||||||
|
regionCode string
|
||||||
|
messageType string
|
||||||
|
body any
|
||||||
|
}
|
||||||
|
|
||||||
func newFakeRocketGateway() *fakeRocketGateway {
|
func newFakeRocketGateway() *fakeRocketGateway {
|
||||||
return &fakeRocketGateway{goldEvents: map[string]integration.GoldReceiptCommand{}}
|
return &fakeRocketGateway{goldEvents: map[string]integration.GoldReceiptCommand{}}
|
||||||
}
|
}
|
||||||
@ -364,7 +610,7 @@ func (g *fakeRocketGateway) MapRoomProfiles(ctx context.Context, roomIDs []int64
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *fakeRocketGateway) GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error) {
|
func (g *fakeRocketGateway) GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error) {
|
||||||
return integration.UserProfile{ID: integration.Int64Value(userID), Account: strconv.FormatInt(userID, 10), UserNickname: "user"}, nil
|
return integration.UserProfile{ID: integration.Int64Value(userID), Account: strconv.FormatInt(userID, 10), UserNickname: "user", CountryCode: "SA"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *fakeRocketGateway) ChangeGoldBalance(ctx context.Context, cmd integration.GoldReceiptCommand) error {
|
func (g *fakeRocketGateway) ChangeGoldBalance(ctx context.Context, cmd integration.GoldReceiptCommand) error {
|
||||||
@ -404,6 +650,24 @@ func (g *fakeRocketIM) SendCustomGroupMessage(ctx context.Context, groupID strin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r fakeRocketRegionResolver) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
|
||||||
|
return r.regions[sysOrigin+"|"+countryCode], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *fakeRocketRegionBroadcaster) SendRegionCustomMessage(ctx context.Context, sysOrigin string, regionCode string, body any) (string, error) {
|
||||||
|
messageType := ""
|
||||||
|
if payload, ok := body.(map[string]any); ok {
|
||||||
|
messageType, _ = payload["type"].(string)
|
||||||
|
}
|
||||||
|
b.messages = append(b.messages, fakeRocketRegionMessage{
|
||||||
|
sysOrigin: sysOrigin,
|
||||||
|
regionCode: regionCode,
|
||||||
|
messageType: messageType,
|
||||||
|
body: body,
|
||||||
|
})
|
||||||
|
return "@region-" + strings.ToLower(regionCode), nil
|
||||||
|
}
|
||||||
|
|
||||||
func seedRocketConfig(t *testing.T, db *gorm.DB) {
|
func seedRocketConfig(t *testing.T, db *gorm.DB) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|||||||
@ -232,20 +232,19 @@ func (s *Service) pickInRoomRewardsForUser(launch model.VoiceRoomRocketLaunch, r
|
|||||||
if len(rewards) == 0 {
|
if len(rewards) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fixed := make([]model.VoiceRoomRocketRewardConfig, 0, len(rewards))
|
|
||||||
weighted := make([]model.VoiceRoomRocketRewardConfig, 0, len(rewards))
|
weighted := make([]model.VoiceRoomRocketRewardConfig, 0, len(rewards))
|
||||||
totalWeight := 0
|
totalWeight := 0
|
||||||
for _, reward := range rewards {
|
for _, reward := range rewards {
|
||||||
if reward.Weight <= 0 {
|
weight := reward.Weight
|
||||||
fixed = append(fixed, reward)
|
if weight <= 0 {
|
||||||
continue
|
weight = 1
|
||||||
}
|
}
|
||||||
|
reward.Weight = weight
|
||||||
weighted = append(weighted, reward)
|
weighted = append(weighted, reward)
|
||||||
totalWeight += reward.Weight
|
totalWeight += weight
|
||||||
}
|
}
|
||||||
result := append([]model.VoiceRoomRocketRewardConfig(nil), fixed...)
|
|
||||||
if totalWeight <= 0 {
|
if totalWeight <= 0 {
|
||||||
return result
|
return nil
|
||||||
}
|
}
|
||||||
rng := rand.New(rand.NewSource(seedFromStrings(launch.LaunchNo, strconv.FormatInt(userID, 10), "reward")))
|
rng := rand.New(rand.NewSource(seedFromStrings(launch.LaunchNo, strconv.FormatInt(userID, 10), "reward")))
|
||||||
point := rng.Intn(totalWeight) + 1
|
point := rng.Intn(totalWeight) + 1
|
||||||
@ -253,11 +252,13 @@ func (s *Service) pickInRoomRewardsForUser(launch model.VoiceRoomRocketLaunch, r
|
|||||||
for _, reward := range weighted {
|
for _, reward := range weighted {
|
||||||
acc += reward.Weight
|
acc += reward.Weight
|
||||||
if point <= acc {
|
if point <= acc {
|
||||||
result = append(result, reward)
|
if strings.EqualFold(strings.TrimSpace(reward.RewardType), rewardTypeNone) {
|
||||||
break
|
return nil
|
||||||
|
}
|
||||||
|
return []model.VoiceRoomRocketRewardConfig{reward}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedFromStrings(parts ...string) int64 {
|
func seedFromStrings(parts ...string) int64 {
|
||||||
|
|||||||
142
internal/service/voiceroomrocket/launch_worker.go
Normal file
142
internal/service/voiceroomrocket/launch_worker.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package voiceroomrocket
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chatapp3-golang/internal/model"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProcessDueLaunches 扫描进度满 10 秒后的火箭发射任务。
|
||||||
|
func (s *Service) ProcessDueLaunches(ctx context.Context, now time.Time, limit int64) (int, error) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = int64(defaultWorkerBatchSize)
|
||||||
|
}
|
||||||
|
processed := 0
|
||||||
|
if s.repo.Redis != nil {
|
||||||
|
statusIDs, err := s.repo.Redis.ZRangeByScore(ctx, s.launchDueZSetKey(), &redis.ZRangeBy{
|
||||||
|
Min: "-inf",
|
||||||
|
Max: strconv.FormatInt(now.Unix(), 10),
|
||||||
|
Count: limit,
|
||||||
|
}).Result()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
for _, rawID := range statusIDs {
|
||||||
|
statusID, parseErr := strconv.ParseInt(strings.TrimSpace(rawID), 10, 64)
|
||||||
|
if parseErr != nil || statusID <= 0 {
|
||||||
|
_ = s.repo.Redis.ZRem(context.Background(), s.launchDueZSetKey(), rawID).Err()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
done, err := s.processDueLaunchStatus(ctx, statusID, now)
|
||||||
|
if err != nil {
|
||||||
|
return processed, err
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
if err := s.repo.Redis.ZRem(context.Background(), s.launchDueZSetKey(), rawID).Err(); err != nil {
|
||||||
|
return processed, err
|
||||||
|
}
|
||||||
|
processed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backfilled, err := s.processStaleLaunchingStatuses(ctx, now, limit)
|
||||||
|
if err != nil {
|
||||||
|
return processed, err
|
||||||
|
}
|
||||||
|
return processed + backfilled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) processStaleLaunchingStatuses(ctx context.Context, now time.Time, limit int64) (int, error) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = int64(defaultWorkerBatchSize)
|
||||||
|
}
|
||||||
|
var rows []model.VoiceRoomRocketStatus
|
||||||
|
if err := s.repo.DB.WithContext(ctx).
|
||||||
|
Where("status = ?", statusLaunching).
|
||||||
|
Order("update_time ASC").
|
||||||
|
Limit(int(limit)).
|
||||||
|
Find(&rows).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
processed := 0
|
||||||
|
for _, row := range rows {
|
||||||
|
done, err := s.processDueLaunchStatus(ctx, row.ID, now)
|
||||||
|
if err != nil {
|
||||||
|
return processed, err
|
||||||
|
}
|
||||||
|
if done {
|
||||||
|
processed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return processed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) processDueLaunchStatus(ctx context.Context, statusID int64, now time.Time) (bool, error) {
|
||||||
|
var launch model.VoiceRoomRocketLaunch
|
||||||
|
done := false
|
||||||
|
var launched bool
|
||||||
|
err := s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
var statusRow model.VoiceRoomRocketStatus
|
||||||
|
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
Where("id = ?", statusID).
|
||||||
|
First(&statusRow).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
done = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if statusRow.Status != statusLaunching || statusRow.CurrentEnergy < statusRow.NeedEnergy {
|
||||||
|
done = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var round model.VoiceRoomRocketLevelRound
|
||||||
|
err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
Where(
|
||||||
|
"sys_origin = ? AND room_id = ? AND day_key = ? AND round_no = ? AND level = ?",
|
||||||
|
statusRow.SysOrigin, statusRow.RoomID, statusRow.DayKey, statusRow.RoundNo, statusRow.CurrentLevel,
|
||||||
|
).
|
||||||
|
First(&round).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if round.ReachTime == nil || round.IgniteUserID == nil || *round.IgniteUserID <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
due := round.ReachTime.Add(time.Duration(defaultLaunchDelaySeconds) * time.Second)
|
||||||
|
if now.Before(due) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result, err := s.launchLocked(ctx, tx, statusRow, *round.IgniteUserID, now)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
launch = result.Launch
|
||||||
|
launched = result.Launched
|
||||||
|
done = true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if launched {
|
||||||
|
_ = s.notifyLaunch(context.Background(), launch)
|
||||||
|
}
|
||||||
|
return done, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) launchDueZSetKey() string {
|
||||||
|
return "voice_room:rocket:launch_due"
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@ func (s *Service) Start(ctx context.Context) error {
|
|||||||
if err := s.StartMessageConsumer(ctx); err != nil {
|
if err := s.StartMessageConsumer(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
s.StartLaunchWorker(ctx)
|
||||||
s.StartInRoomRewardWorker(ctx)
|
s.StartInRoomRewardWorker(ctx)
|
||||||
s.StartRewardGrantWorker(ctx)
|
s.StartRewardGrantWorker(ctx)
|
||||||
return nil
|
return nil
|
||||||
@ -38,6 +39,33 @@ func (s *Service) StartMessageConsumer(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartLaunchWorker 启动满进度后延迟发射扫描。
|
||||||
|
func (s *Service) StartLaunchWorker(ctx context.Context) {
|
||||||
|
interval := time.Duration(s.cfg.VoiceRoomRocket.InRoomRewardScanIntervalSeconds) * time.Second
|
||||||
|
if interval <= 0 {
|
||||||
|
interval = defaultWorkerInterval
|
||||||
|
}
|
||||||
|
batchSize := int64(s.cfg.VoiceRoomRocket.InRoomRewardBatchSize)
|
||||||
|
if batchSize <= 0 {
|
||||||
|
batchSize = defaultWorkerBatchSize
|
||||||
|
}
|
||||||
|
log.Printf("voice room rocket launch worker started. interval=%s batch=%d delay=%ds", interval, batchSize, defaultLaunchDelaySeconds)
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
if _, err := s.ProcessDueLaunches(ctx, time.Now(), batchSize); err != nil && ctx.Err() == nil {
|
||||||
|
log.Printf("voice room rocket launch scan failed: %v", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// StartInRoomRewardWorker 启动在房奖励到期扫描。
|
// StartInRoomRewardWorker 启动在房奖励到期扫描。
|
||||||
func (s *Service) StartInRoomRewardWorker(ctx context.Context) {
|
func (s *Service) StartInRoomRewardWorker(ctx context.Context) {
|
||||||
if s.repo.Redis == nil {
|
if s.repo.Redis == nil {
|
||||||
|
|||||||
@ -177,6 +177,9 @@ func rewardPreviewPayload(rows []model.VoiceRoomRocketRewardConfig) RewardPrevie
|
|||||||
})
|
})
|
||||||
resp := RewardPreviewResponse{}
|
resp := RewardPreviewResponse{}
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(row.RewardType), rewardTypeNone) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
payload := rewardPayload(row)
|
payload := rewardPayload(row)
|
||||||
switch strings.ToUpper(strings.TrimSpace(row.RewardScene)) {
|
switch strings.ToUpper(strings.TrimSpace(row.RewardScene)) {
|
||||||
case rewardSceneTop1:
|
case rewardSceneTop1:
|
||||||
@ -211,7 +214,8 @@ func isSupportedRewardType(rewardType string) bool {
|
|||||||
rewardTypeChatBubble,
|
rewardTypeChatBubble,
|
||||||
rewardTypeVIPCard,
|
rewardTypeVIPCard,
|
||||||
rewardTypeGift,
|
rewardTypeGift,
|
||||||
rewardTypeBadge:
|
rewardTypeBadge,
|
||||||
|
rewardTypeNone:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@ -234,6 +238,10 @@ func configResponse(snapshot configSnapshot, levels []model.VoiceRoomRocketLevel
|
|||||||
if strings.TrimSpace(snapshot.RuleText) != "" {
|
if strings.TrimSpace(snapshot.RuleText) != "" {
|
||||||
ruleText = json.RawMessage(snapshot.RuleText)
|
ruleText = json.RawMessage(snapshot.RuleText)
|
||||||
}
|
}
|
||||||
|
var energyRuleJSON json.RawMessage
|
||||||
|
if strings.TrimSpace(snapshot.EnergyRuleJSON) != "" {
|
||||||
|
energyRuleJSON = json.RawMessage(snapshot.EnergyRuleJSON)
|
||||||
|
}
|
||||||
return &ConfigResponse{
|
return &ConfigResponse{
|
||||||
Configured: snapshot.Configured,
|
Configured: snapshot.Configured,
|
||||||
ID: snapshot.ID,
|
ID: snapshot.ID,
|
||||||
@ -245,6 +253,7 @@ func configResponse(snapshot configSnapshot, levels []model.VoiceRoomRocketLevel
|
|||||||
RewardRecordKeepDays: snapshot.RewardRecordKeepDays,
|
RewardRecordKeepDays: snapshot.RewardRecordKeepDays,
|
||||||
BroadcastDurationSeconds: snapshot.BroadcastDurationSeconds,
|
BroadcastDurationSeconds: snapshot.BroadcastDurationSeconds,
|
||||||
InRoomRewardDelaySeconds: snapshot.InRoomRewardDelaySeconds,
|
InRoomRewardDelaySeconds: snapshot.InRoomRewardDelaySeconds,
|
||||||
|
EnergyRuleJSON: energyRuleJSON,
|
||||||
RuleText: ruleText,
|
RuleText: ruleText,
|
||||||
Levels: levelPayloads,
|
Levels: levelPayloads,
|
||||||
RewardPreview: rewardPreviewPayload(rewards),
|
RewardPreview: rewardPreviewPayload(rewards),
|
||||||
|
|||||||
@ -13,12 +13,27 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) notifyStatusUpdate(ctx context.Context, sysOrigin string, roomID int64) error {
|
func (s *Service) notifyStatusUpdate(ctx context.Context, sysOrigin string, roomID int64, senderUserIDs ...int64) error {
|
||||||
|
data := map[string]any{
|
||||||
|
"roomId": strconv.FormatInt(roomID, 10),
|
||||||
|
}
|
||||||
|
if status, err := s.GetStatus(ctx, AuthUser{SysOrigin: sysOrigin}, roomID); err == nil && status != nil {
|
||||||
|
payload, _ := json.Marshal(status)
|
||||||
|
_ = json.Unmarshal(payload, &data)
|
||||||
|
}
|
||||||
|
senderUserID := int64(0)
|
||||||
|
if len(senderUserIDs) > 0 {
|
||||||
|
senderUserID = senderUserIDs[0]
|
||||||
|
}
|
||||||
|
if senderUserID > 0 {
|
||||||
|
data["senderUserId"] = strconv.FormatInt(senderUserID, 10)
|
||||||
|
}
|
||||||
if s.repo.Redis != nil {
|
if s.repo.Redis != nil {
|
||||||
body, _ := json.Marshal(map[string]any{
|
body, _ := json.Marshal(map[string]any{
|
||||||
"type": imTypeStatusUpdate,
|
"type": imTypeStatusUpdate,
|
||||||
"sysOrigin": sysOrigin,
|
"sysOrigin": sysOrigin,
|
||||||
"roomId": strconv.FormatInt(roomID, 10),
|
"roomId": strconv.FormatInt(roomID, 10),
|
||||||
|
"data": data,
|
||||||
})
|
})
|
||||||
streamKey := strings.TrimSpace(s.cfg.VoiceRoomRocket.BroadcastStreamKey)
|
streamKey := strings.TrimSpace(s.cfg.VoiceRoomRocket.BroadcastStreamKey)
|
||||||
if streamKey == "" {
|
if streamKey == "" {
|
||||||
@ -33,18 +48,45 @@ func (s *Service) notifyStatusUpdate(ctx context.Context, sysOrigin string, room
|
|||||||
},
|
},
|
||||||
}).Err()
|
}).Err()
|
||||||
}
|
}
|
||||||
|
_ = s.notifyRegionStatusUpdate(ctx, sysOrigin, senderUserID, data)
|
||||||
if s.im == nil {
|
if s.im == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
groupID := s.resolveRoomAccount(ctx, roomID)
|
groupID := s.resolveRoomAccount(ctx, roomID)
|
||||||
return s.im.SendCustomGroupMessage(ctx, groupID, map[string]any{
|
return s.im.SendCustomGroupMessage(ctx, groupID, map[string]any{
|
||||||
"type": imTypeStatusUpdate,
|
"type": imTypeStatusUpdate,
|
||||||
"data": map[string]any{
|
"data": data,
|
||||||
"roomId": strconv.FormatInt(roomID, 10),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) notifyRegionStatusUpdate(ctx context.Context, sysOrigin string, senderUserID int64, data map[string]any) error {
|
||||||
|
if s.regionBroadcast == nil || s.regionResolver == nil || s.userProfiles == nil || senderUserID <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
profile, err := s.userProfiles.GetUserProfile(ctx, senderUserID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
countryCode := strings.TrimSpace(profile.CountryCode)
|
||||||
|
if countryCode == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
regionCode, err := s.regionResolver.ResolveRegionCodeByCountryCode(ctx, sysOrigin, countryCode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
regionCode = strings.TrimSpace(regionCode)
|
||||||
|
if regionCode == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
body := map[string]any{
|
||||||
|
"type": imTypeStatusUpdate,
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
_, err = s.regionBroadcast.SendRegionCustomMessage(ctx, sysOrigin, regionCode, body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) notifyLaunch(ctx context.Context, launch model.VoiceRoomRocketLaunch) error {
|
func (s *Service) notifyLaunch(ctx context.Context, launch model.VoiceRoomRocketLaunch) error {
|
||||||
triggerUser := map[string]any{}
|
triggerUser := map[string]any{}
|
||||||
if launch.IgniteUserID != nil && s.userProfiles != nil {
|
if launch.IgniteUserID != nil && s.userProfiles != nil {
|
||||||
|
|||||||
@ -46,6 +46,9 @@ func (s *Service) createImmediateRewardRecords(ctx context.Context, tx *gorm.DB,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) createRewardRecord(ctx context.Context, tx *gorm.DB, launch model.VoiceRoomRocketLaunch, reward model.VoiceRoomRocketRewardConfig, userID int64, now time.Time) (model.VoiceRoomRocketRewardRecord, error) {
|
func (s *Service) createRewardRecord(ctx context.Context, tx *gorm.DB, launch model.VoiceRoomRocketLaunch, reward model.VoiceRoomRocketRewardConfig, userID int64, now time.Time) (model.VoiceRoomRocketRewardRecord, error) {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(reward.RewardType), rewardTypeNone) {
|
||||||
|
return model.VoiceRoomRocketRewardRecord{}, nil
|
||||||
|
}
|
||||||
id, err := utils.NextID()
|
id, err := utils.NextID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.VoiceRoomRocketRewardRecord{}, err
|
return model.VoiceRoomRocketRewardRecord{}, err
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package voiceroomrocket
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -43,20 +44,23 @@ func (s *Service) GetStatus(ctx context.Context, user AuthUser, roomID int64) (*
|
|||||||
shake := status.NeedEnergy > 0 &&
|
shake := status.NeedEnergy > 0 &&
|
||||||
float64(status.CurrentEnergy)*100/float64(status.NeedEnergy) >= parsePercent(currentLevelConfig.ShakeThresholdPercent)
|
float64(status.CurrentEnergy)*100/float64(status.NeedEnergy) >= parsePercent(currentLevelConfig.ShakeThresholdPercent)
|
||||||
kings, _ := s.topKingRecords(ctx, snapshot.SysOrigin, roomID, status.DayKey, status.RoundNo, status.CurrentLevel, 3)
|
kings, _ := s.topKingRecords(ctx, snapshot.SysOrigin, roomID, status.DayKey, status.RoundNo, status.CurrentLevel, 3)
|
||||||
|
countdownSeconds := s.launchCountdownSeconds(ctx, status, time.Now())
|
||||||
return &StatusResponse{
|
return &StatusResponse{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
DayKey: status.DayKey,
|
DayKey: status.DayKey,
|
||||||
RoundNo: status.RoundNo,
|
RoundNo: status.RoundNo,
|
||||||
DisplayRound: status.RoundNo > 1,
|
DisplayRound: status.RoundNo > 1,
|
||||||
CurrentLevel: status.CurrentLevel,
|
Status: status.Status,
|
||||||
CurrentEnergy: status.CurrentEnergy,
|
CurrentLevel: status.CurrentLevel,
|
||||||
NeedEnergy: status.NeedEnergy,
|
CurrentEnergy: status.CurrentEnergy,
|
||||||
EnergyPercent: percent,
|
NeedEnergy: status.NeedEnergy,
|
||||||
DisplayPercent: displayPercent,
|
EnergyPercent: percent,
|
||||||
Shake: shake,
|
DisplayPercent: displayPercent,
|
||||||
Levels: buildLevelPayloads(levels),
|
LaunchCountdownSeconds: countdownSeconds,
|
||||||
RewardPreview: rewardPreviewPayload(rewards),
|
Shake: shake,
|
||||||
RocketKings: kings,
|
Levels: buildLevelPayloads(levels),
|
||||||
|
RewardPreview: rewardPreviewPayload(rewards),
|
||||||
|
RocketKings: kings,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +115,28 @@ func (s *Service) loadOrInitStatus(ctx context.Context, tx *gorm.DB, sysOrigin s
|
|||||||
return row, nil
|
return row, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) launchCountdownSeconds(ctx context.Context, status model.VoiceRoomRocketStatus, now time.Time) int {
|
||||||
|
if status.Status != statusLaunching {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var round model.VoiceRoomRocketLevelRound
|
||||||
|
err := s.repo.DB.WithContext(ctx).
|
||||||
|
Where(
|
||||||
|
"sys_origin = ? AND room_id = ? AND day_key = ? AND round_no = ? AND level = ?",
|
||||||
|
status.SysOrigin, status.RoomID, status.DayKey, status.RoundNo, status.CurrentLevel,
|
||||||
|
).
|
||||||
|
First(&round).Error
|
||||||
|
if err != nil || round.ReachTime == nil || round.ReachTime.IsZero() {
|
||||||
|
return defaultLaunchDelaySeconds
|
||||||
|
}
|
||||||
|
due := round.ReachTime.Add(time.Duration(defaultLaunchDelaySeconds) * time.Second)
|
||||||
|
remaining := due.Sub(now).Seconds()
|
||||||
|
if remaining <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int(math.Ceil(remaining))
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) ensureLevelRound(ctx context.Context, tx *gorm.DB, sysOrigin string, roomID int64, day string, roundNo int, level int, needEnergy int64, now time.Time) error {
|
func (s *Service) ensureLevelRound(ctx context.Context, tx *gorm.DB, sysOrigin string, roomID int64, day string, roundNo int, level int, needEnergy int64, now time.Time) error {
|
||||||
db := s.repo.DB.WithContext(ctx)
|
db := s.repo.DB.WithContext(ctx)
|
||||||
if tx != nil {
|
if tx != nil {
|
||||||
|
|||||||
@ -28,6 +28,7 @@ const (
|
|||||||
defaultBroadcastDurationSeconds = 7
|
defaultBroadcastDurationSeconds = 7
|
||||||
defaultInRoomRewardDelaySeconds = 10
|
defaultInRoomRewardDelaySeconds = 10
|
||||||
defaultInRoomRewardUserLimit = 3
|
defaultInRoomRewardUserLimit = 3
|
||||||
|
defaultLaunchDelaySeconds = 10
|
||||||
defaultWorkerBatchSize = 100
|
defaultWorkerBatchSize = 100
|
||||||
defaultWorkerInterval = time.Second
|
defaultWorkerInterval = time.Second
|
||||||
defaultRoomLockTTL = 10 * time.Second
|
defaultRoomLockTTL = 10 * time.Second
|
||||||
@ -36,6 +37,7 @@ const (
|
|||||||
statusLaunching = "LAUNCHING"
|
statusLaunching = "LAUNCHING"
|
||||||
|
|
||||||
levelRoundStatusCharging = "CHARGING"
|
levelRoundStatusCharging = "CHARGING"
|
||||||
|
levelRoundStatusReached = "REACHED"
|
||||||
levelRoundStatusLaunched = "LAUNCHED"
|
levelRoundStatusLaunched = "LAUNCHED"
|
||||||
|
|
||||||
eventStatusIgnored = "IGNORED"
|
eventStatusIgnored = "IGNORED"
|
||||||
@ -66,6 +68,7 @@ const (
|
|||||||
rewardTypeVIPCard = "VIP_CARD"
|
rewardTypeVIPCard = "VIP_CARD"
|
||||||
rewardTypeGift = "GIFT"
|
rewardTypeGift = "GIFT"
|
||||||
rewardTypeBadge = "BADGE"
|
rewardTypeBadge = "BADGE"
|
||||||
|
rewardTypeNone = "NONE"
|
||||||
|
|
||||||
propsTypeNobleVIP = "NOBLE_VIP"
|
propsTypeNobleVIP = "NOBLE_VIP"
|
||||||
propsTypeRide = "RIDE"
|
propsTypeRide = "RIDE"
|
||||||
@ -101,6 +104,14 @@ type userProfileGateway interface {
|
|||||||
GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error)
|
GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type regionResolver interface {
|
||||||
|
ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type regionBroadcaster interface {
|
||||||
|
SendRegionCustomMessage(ctx context.Context, sysOrigin string, regionCode string, body any) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
type walletGateway interface {
|
type walletGateway interface {
|
||||||
ChangeGoldBalance(ctx context.Context, cmd integration.GoldReceiptCommand) error
|
ChangeGoldBalance(ctx context.Context, cmd integration.GoldReceiptCommand) error
|
||||||
ExistsGoldEvent(ctx context.Context, eventID string) (bool, error)
|
ExistsGoldEvent(ctx context.Context, eventID string) (bool, error)
|
||||||
@ -128,16 +139,18 @@ type ports struct {
|
|||||||
|
|
||||||
// Service 承接语音房火箭配置、状态、送礼加能量、发射、榜单和奖励记录。
|
// Service 承接语音房火箭配置、状态、送礼加能量、发射、榜单和奖励记录。
|
||||||
type Service struct {
|
type Service struct {
|
||||||
cfg config.Config
|
cfg config.Config
|
||||||
repo ports
|
repo ports
|
||||||
gateways gatewayBundle
|
gateways gatewayBundle
|
||||||
im imGateway
|
im imGateway
|
||||||
roomProfiles roomProfileGateway
|
roomProfiles roomProfileGateway
|
||||||
userProfiles userProfileGateway
|
userProfiles userProfileGateway
|
||||||
wallet walletGateway
|
wallet walletGateway
|
||||||
rewardGateway rewardGateway
|
rewardGateway rewardGateway
|
||||||
location *time.Location
|
regionResolver regionResolver
|
||||||
rocketConsumer rmq.PushConsumer
|
regionBroadcast regionBroadcaster
|
||||||
|
location *time.Location
|
||||||
|
rocketConsumer rmq.PushConsumer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService 创建语音房火箭服务。
|
// NewService 创建语音房火箭服务。
|
||||||
@ -162,6 +175,9 @@ func NewService(cfg config.Config, db voiceRoomRocketDB, cache redis.Cmdable, ga
|
|||||||
service.userProfiles = gateways
|
service.userProfiles = gateways
|
||||||
service.wallet = gateways
|
service.wallet = gateways
|
||||||
service.rewardGateway = gateways
|
service.rewardGateway = gateways
|
||||||
|
if resolver, ok := any(gateways).(regionResolver); ok {
|
||||||
|
service.regionResolver = resolver
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return service
|
return service
|
||||||
}
|
}
|
||||||
@ -173,6 +189,13 @@ func (s *Service) SetIMGateway(gateway imGateway) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetRegionBroadcaster 允许装配区域 IM 广播服务。
|
||||||
|
func (s *Service) SetRegionBroadcaster(broadcaster regionBroadcaster) {
|
||||||
|
if s != nil {
|
||||||
|
s.regionBroadcast = broadcaster
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type flexibleInt64 int64
|
type flexibleInt64 int64
|
||||||
|
|
||||||
// FlexibleInt64 暴露给本地验证和路由测试,兼容 JSON 字符串形式的 Snowflake ID。
|
// FlexibleInt64 暴露给本地验证和路由测试,兼容 JSON 字符串形式的 Snowflake ID。
|
||||||
@ -331,6 +354,7 @@ type ConfigResponse struct {
|
|||||||
RewardRecordKeepDays int `json:"rewardRecordKeepDays"`
|
RewardRecordKeepDays int `json:"rewardRecordKeepDays"`
|
||||||
BroadcastDurationSeconds int `json:"broadcastDurationSeconds"`
|
BroadcastDurationSeconds int `json:"broadcastDurationSeconds"`
|
||||||
InRoomRewardDelaySeconds int `json:"inRoomRewardDelaySeconds"`
|
InRoomRewardDelaySeconds int `json:"inRoomRewardDelaySeconds"`
|
||||||
|
EnergyRuleJSON json.RawMessage `json:"energyRuleJson,omitempty"`
|
||||||
RuleText json.RawMessage `json:"ruleText,omitempty"`
|
RuleText json.RawMessage `json:"ruleText,omitempty"`
|
||||||
Levels []LevelConfigPayload `json:"levels"`
|
Levels []LevelConfigPayload `json:"levels"`
|
||||||
RewardPreview RewardPreviewResponse `json:"rewardPreview"`
|
RewardPreview RewardPreviewResponse `json:"rewardPreview"`
|
||||||
@ -357,23 +381,26 @@ type SaveLevelConfigsRequest struct {
|
|||||||
|
|
||||||
type SaveRewardConfigsRequest struct {
|
type SaveRewardConfigsRequest struct {
|
||||||
SysOrigin string `json:"sysOrigin"`
|
SysOrigin string `json:"sysOrigin"`
|
||||||
|
Level int `json:"level"`
|
||||||
Rewards []RewardConfigPayload `json:"rewards"`
|
Rewards []RewardConfigPayload `json:"rewards"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatusResponse struct {
|
type StatusResponse struct {
|
||||||
RoomID int64 `json:"roomId,string"`
|
RoomID int64 `json:"roomId,string"`
|
||||||
DayKey string `json:"dayKey"`
|
DayKey string `json:"dayKey"`
|
||||||
RoundNo int `json:"roundNo"`
|
RoundNo int `json:"roundNo"`
|
||||||
DisplayRound bool `json:"displayRound"`
|
DisplayRound bool `json:"displayRound"`
|
||||||
CurrentLevel int `json:"currentLevel"`
|
Status string `json:"status"`
|
||||||
CurrentEnergy int64 `json:"currentEnergy"`
|
CurrentLevel int `json:"currentLevel"`
|
||||||
NeedEnergy int64 `json:"needEnergy"`
|
CurrentEnergy int64 `json:"currentEnergy"`
|
||||||
EnergyPercent string `json:"energyPercent"`
|
NeedEnergy int64 `json:"needEnergy"`
|
||||||
DisplayPercent int `json:"displayPercent"`
|
EnergyPercent string `json:"energyPercent"`
|
||||||
Shake bool `json:"shake"`
|
DisplayPercent int `json:"displayPercent"`
|
||||||
Levels []LevelConfigPayload `json:"levels"`
|
LaunchCountdownSeconds int `json:"launchCountdownSeconds,omitempty"`
|
||||||
RewardPreview RewardPreviewResponse `json:"rewardPreview"`
|
Shake bool `json:"shake"`
|
||||||
RocketKings []KingRecord `json:"rocketKings"`
|
Levels []LevelConfigPayload `json:"levels"`
|
||||||
|
RewardPreview RewardPreviewResponse `json:"rewardPreview"`
|
||||||
|
RocketKings []KingRecord `json:"rocketKings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KingResponse struct {
|
type KingResponse struct {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user