321 lines
8.9 KiB
Go
321 lines
8.9 KiB
Go
package voiceroomrocket
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"hash/fnv"
|
|
"math/rand"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"chatapp3-golang/internal/model"
|
|
"chatapp3-golang/internal/utils"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
// ProcessDueInRoomRewards 扫描到期的在房间奖励任务,写入在线用户快照并生成中奖记录。
|
|
func (s *Service) ProcessDueInRoomRewards(ctx context.Context, now time.Time, limit int64) (int, error) {
|
|
if s.repo.Redis == nil {
|
|
return 0, nil
|
|
}
|
|
if limit <= 0 {
|
|
limit = int64(defaultWorkerBatchSize)
|
|
}
|
|
key := s.inRoomDueZSetKey()
|
|
launchNos, err := s.repo.Redis.ZRangeByScore(ctx, key, &redis.ZRangeBy{
|
|
Min: "-inf",
|
|
Max: strconv.FormatInt(now.Unix(), 10),
|
|
Count: limit,
|
|
}).Result()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
processed := 0
|
|
for _, launchNo := range launchNos {
|
|
launchNo = strings.TrimSpace(launchNo)
|
|
if launchNo == "" {
|
|
continue
|
|
}
|
|
done, err := s.processInRoomRewardLaunch(ctx, launchNo, now)
|
|
if err != nil {
|
|
return processed, err
|
|
}
|
|
if done {
|
|
if err := s.repo.Redis.ZRem(context.Background(), key, launchNo).Err(); err != nil {
|
|
return processed, err
|
|
}
|
|
processed++
|
|
}
|
|
}
|
|
return processed, nil
|
|
}
|
|
|
|
func (s *Service) processInRoomRewardLaunch(ctx context.Context, launchNo string, now time.Time) (bool, error) {
|
|
var launch model.VoiceRoomRocketLaunch
|
|
var selectedRecords []model.VoiceRoomRocketRewardRecord
|
|
done := false
|
|
err := s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
Where("launch_no = ?", launchNo).
|
|
First(&launch).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
done = true
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
switch launch.InRoomSnapshotStatus {
|
|
case inRoomSnapshotSuccess, inRoomSnapshotEmpty:
|
|
done = true
|
|
return nil
|
|
}
|
|
if err := tx.Model(&model.VoiceRoomRocketLaunch{}).
|
|
Where("id = ?", launch.ID).
|
|
Updates(map[string]any{
|
|
"in_room_snapshot_status": inRoomSnapshotProcessing,
|
|
"update_time": now,
|
|
}).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
audience, err := s.listOnlineRoomUsers(ctx, launch.SysOrigin, launch.RoomID, now)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rewards, err := s.loadInRoomRewardConfigs(ctx, launch.SysOrigin, launch.Level)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
selected := s.selectInRoomRewardUsers(launch, audience, rewards)
|
|
if err := s.saveAudienceSnapshot(ctx, tx, launch, audience, selected, now); err != nil {
|
|
return err
|
|
}
|
|
if len(selected) == 0 {
|
|
if err := s.markLaunchInRoomStatus(ctx, tx, launch.ID, inRoomSnapshotEmpty, now); err != nil {
|
|
return err
|
|
}
|
|
done = true
|
|
return nil
|
|
}
|
|
|
|
pickedRewards := s.pickInRoomRewardsForUsers(launch, rewards, selected)
|
|
for _, userID := range selected {
|
|
for _, reward := range pickedRewards[userID] {
|
|
record, err := s.createRewardRecord(ctx, tx, launch, reward, userID, now)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if record.ID > 0 {
|
|
selectedRecords = append(selectedRecords, record)
|
|
}
|
|
}
|
|
}
|
|
if err := s.markLaunchInRoomStatus(ctx, tx, launch.ID, inRoomSnapshotSuccess, now); err != nil {
|
|
return err
|
|
}
|
|
done = true
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
_ = s.repo.DB.WithContext(context.Background()).
|
|
Model(&model.VoiceRoomRocketLaunch{}).
|
|
Where("launch_no = ?", launchNo).
|
|
Updates(map[string]any{
|
|
"in_room_snapshot_status": inRoomSnapshotFailed,
|
|
"update_time": time.Now(),
|
|
}).Error
|
|
return false, err
|
|
}
|
|
if len(selectedRecords) > 0 {
|
|
_ = s.notifyRewardRecords(context.Background(), launch, selectedRecords)
|
|
}
|
|
return done, nil
|
|
}
|
|
|
|
func (s *Service) saveAudienceSnapshot(
|
|
ctx context.Context,
|
|
tx *gorm.DB,
|
|
launch model.VoiceRoomRocketLaunch,
|
|
audience []roomAudienceUser,
|
|
selected []int64,
|
|
now time.Time,
|
|
) error {
|
|
selectedSet := make(map[int64]struct{}, len(selected))
|
|
for _, userID := range selected {
|
|
selectedSet[userID] = struct{}{}
|
|
}
|
|
rows := make([]model.VoiceRoomRocketAudienceSnapshot, 0, len(audience))
|
|
for _, user := range audience {
|
|
id, err := utils.NextID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, isSelected := selectedSet[user.UserID]
|
|
rows = append(rows, model.VoiceRoomRocketAudienceSnapshot{
|
|
ID: id,
|
|
LaunchID: launch.ID,
|
|
SysOrigin: launch.SysOrigin,
|
|
RoomID: launch.RoomID,
|
|
UserID: user.UserID,
|
|
LastSeenTime: user.LastSeenTime,
|
|
Selected: isSelected,
|
|
CreateTime: now,
|
|
})
|
|
}
|
|
if len(rows) == 0 {
|
|
return nil
|
|
}
|
|
return tx.WithContext(ctx).
|
|
Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "launch_id"}, {Name: "user_id"}},
|
|
DoUpdates: clause.AssignmentColumns([]string{"last_seen_time", "selected"}),
|
|
}).
|
|
Create(&rows).Error
|
|
}
|
|
|
|
func (s *Service) markLaunchInRoomStatus(ctx context.Context, tx *gorm.DB, launchID int64, status string, now time.Time) error {
|
|
return tx.WithContext(ctx).Model(&model.VoiceRoomRocketLaunch{}).
|
|
Where("id = ?", launchID).
|
|
Updates(map[string]any{
|
|
"in_room_snapshot_status": status,
|
|
"update_time": now,
|
|
}).Error
|
|
}
|
|
|
|
func (s *Service) loadInRoomRewardConfigs(ctx context.Context, sysOrigin string, level int) ([]model.VoiceRoomRocketRewardConfig, error) {
|
|
rewards, err := s.loadRewardConfigs(ctx, sysOrigin, level)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]model.VoiceRoomRocketRewardConfig, 0, len(rewards))
|
|
for _, reward := range rewards {
|
|
if strings.EqualFold(strings.TrimSpace(reward.RewardScene), rewardSceneInRoom) {
|
|
result = append(result, reward)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *Service) selectInRoomRewardUsers(launch model.VoiceRoomRocketLaunch, audience []roomAudienceUser, rewards []model.VoiceRoomRocketRewardConfig) []int64 {
|
|
limit := s.inRoomRewardSelectionLimit(rewards)
|
|
if len(audience) == 0 {
|
|
return nil
|
|
}
|
|
userIDs := make([]int64, 0, len(audience))
|
|
for _, user := range audience {
|
|
if user.UserID > 0 {
|
|
userIDs = append(userIDs, user.UserID)
|
|
}
|
|
}
|
|
sort.Slice(userIDs, func(i, j int) bool { return userIDs[i] < userIDs[j] })
|
|
if len(userIDs) <= limit {
|
|
return userIDs
|
|
}
|
|
rng := rand.New(rand.NewSource(seedFromStrings(launch.LaunchNo, "audience")))
|
|
rng.Shuffle(len(userIDs), func(i, j int) {
|
|
userIDs[i], userIDs[j] = userIDs[j], userIDs[i]
|
|
})
|
|
selected := append([]int64(nil), userIDs[:limit]...)
|
|
sort.Slice(selected, func(i, j int) bool { return selected[i] < selected[j] })
|
|
return selected
|
|
}
|
|
|
|
func (s *Service) inRoomRewardSelectionLimit(rewards []model.VoiceRoomRocketRewardConfig) int {
|
|
totalCopies := 0
|
|
for _, reward := range rewards {
|
|
if !strings.EqualFold(strings.TrimSpace(reward.RewardScene), rewardSceneInRoom) {
|
|
continue
|
|
}
|
|
totalCopies += positiveOrDefault(reward.RewardCopies, 1)
|
|
}
|
|
if totalCopies > 0 {
|
|
return totalCopies
|
|
}
|
|
limit := s.cfg.VoiceRoomRocket.InRoomRewardUserLimit
|
|
if limit <= 0 {
|
|
limit = defaultInRoomRewardUserLimit
|
|
}
|
|
return limit
|
|
}
|
|
|
|
func (s *Service) pickInRoomRewardsForUser(launch model.VoiceRoomRocketLaunch, rewards []model.VoiceRoomRocketRewardConfig, userID int64) []model.VoiceRoomRocketRewardConfig {
|
|
picked := s.pickInRoomRewardsForUsers(launch, rewards, []int64{userID})
|
|
return picked[userID]
|
|
}
|
|
|
|
func (s *Service) pickInRoomRewardsForUsers(launch model.VoiceRoomRocketLaunch, rewards []model.VoiceRoomRocketRewardConfig, userIDs []int64) map[int64][]model.VoiceRoomRocketRewardConfig {
|
|
result := make(map[int64][]model.VoiceRoomRocketRewardConfig, len(userIDs))
|
|
if len(rewards) == 0 {
|
|
return result
|
|
}
|
|
|
|
type rewardCandidate struct {
|
|
reward model.VoiceRoomRocketRewardConfig
|
|
remaining int
|
|
}
|
|
weighted := make([]rewardCandidate, 0, len(rewards))
|
|
for _, reward := range rewards {
|
|
weight := reward.Weight
|
|
if weight <= 0 {
|
|
weight = 1
|
|
}
|
|
reward.Weight = weight
|
|
weighted = append(weighted, rewardCandidate{
|
|
reward: reward,
|
|
remaining: positiveOrDefault(reward.RewardCopies, 1),
|
|
})
|
|
}
|
|
|
|
for _, userID := range userIDs {
|
|
totalWeight := 0
|
|
for _, candidate := range weighted {
|
|
if candidate.remaining > 0 {
|
|
totalWeight += candidate.reward.Weight
|
|
}
|
|
}
|
|
if totalWeight <= 0 {
|
|
continue
|
|
}
|
|
rng := rand.New(rand.NewSource(seedFromStrings(launch.LaunchNo, strconv.FormatInt(userID, 10), "reward")))
|
|
point := rng.Intn(totalWeight) + 1
|
|
acc := 0
|
|
for idx := range weighted {
|
|
if weighted[idx].remaining <= 0 {
|
|
continue
|
|
}
|
|
acc += weighted[idx].reward.Weight
|
|
if point <= acc {
|
|
weighted[idx].remaining--
|
|
if !strings.EqualFold(strings.TrimSpace(weighted[idx].reward.RewardType), rewardTypeNone) {
|
|
result[userID] = []model.VoiceRoomRocketRewardConfig{weighted[idx].reward}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func seedFromStrings(parts ...string) int64 {
|
|
hash := fnv.New64a()
|
|
for _, part := range parts {
|
|
_, _ = hash.Write([]byte(part))
|
|
_, _ = hash.Write([]byte{0})
|
|
}
|
|
return int64(hash.Sum64() & 0x7fffffffffffffff)
|
|
}
|
|
|
|
func (s *Service) inRoomDueZSetKey() string {
|
|
key := strings.TrimSpace(s.cfg.VoiceRoomRocket.InRoomRewardDueZSetKey)
|
|
if key == "" {
|
|
return "voice_room:rocket:in_room_due"
|
|
}
|
|
return key
|
|
}
|