2026-05-15 12:19:42 +08:00

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
}