2026-01-27 01:33:32 +08:00

117 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

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

package strategy
import (
"bindbox-game/internal/repository/mysql/dao"
"bindbox-game/internal/repository/mysql/model"
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
)
// DefaultStrategy 默认抽奖策略(无限赏模式)
type DefaultStrategy struct {
read *dao.Query
write *dao.Query
}
func NewDefault(read *dao.Query, write *dao.Query) ActivityDrawStrategy {
return &DefaultStrategy{read: read, write: write}
}
func (s *DefaultStrategy) PreChecks(ctx context.Context, activityID int64, issueID int64, userID int64) error {
issue, err := s.read.ActivityIssues.WithContext(ctx).Where(s.read.ActivityIssues.ID.Eq(issueID), s.read.ActivityIssues.ActivityID.Eq(activityID)).First()
if err != nil || issue == nil {
return errors.New("issue not found")
}
return nil
}
func (s *DefaultStrategy) SelectItem(ctx context.Context, activityID int64, issueID int64, userID int64) (int64, map[string]any, error) {
rewards, err := s.read.ActivityRewardSettings.WithContext(ctx).ReadDB().Where(s.read.ActivityRewardSettings.IssueID.Eq(issueID)).Find()
if err != nil || len(rewards) == 0 {
return 0, nil, errors.New("no rewards")
}
act, _ := s.read.Activities.WithContext(ctx).Where(s.read.Activities.ID.Eq(activityID)).First()
if act == nil || len(act.CommitmentSeedMaster) == 0 {
return 0, nil, errors.New("活动尚未生成承诺,无法抽奖")
}
return s.selectItemInternal(rewards, act.CommitmentSeedMaster, issueID, userID)
}
// SelectItemFromCache 使用预加载的数据进行选品,避免每次循环都查询数据库
func (s *DefaultStrategy) SelectItemFromCache(rewards []*model.ActivityRewardSettings, seedKey []byte, issueID int64, userID int64) (int64, map[string]any, error) {
if len(rewards) == 0 {
return 0, nil, errors.New("no rewards")
}
if len(seedKey) == 0 {
return 0, nil, errors.New("活动尚未生成承诺,无法抽奖")
}
return s.selectItemInternal(rewards, seedKey, issueID, userID)
}
func (s *DefaultStrategy) selectItemInternal(rewards []*model.ActivityRewardSettings, seedKey []byte, issueID int64, userID int64) (int64, map[string]any, error) {
// 统计所有奖品权重不再过滤库存为0的项
var total int64
var validCount int
for _, r := range rewards {
total += int64(r.Weight)
validCount++
}
if total <= 0 {
return 0, nil, fmt.Errorf("no weight: total_rewards=%d, valid_with_stock=%d", len(rewards), validCount)
}
// To ensure uniqueness per draw when using a fixed CommitmentSeedMaster, mix in a random salt
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
return 0, nil, errors.New("crypto rand salt failed")
}
// Use HMAC-SHA256 to generate random number derived from seed + context + salt
mac := hmac.New(sha256.New, seedKey)
mac.Write([]byte(fmt.Sprintf("draw:issue:%d|user:%d|salt:%x", issueID, userID, salt)))
sum := mac.Sum(nil)
rnd := int64(binary.BigEndian.Uint64(sum[:8]) % uint64(total))
var acc int64
var picked int64
for _, r := range rewards {
acc += int64(r.Weight)
if rnd < acc {
picked = r.ID
break
}
}
if picked == 0 {
return 0, nil, fmt.Errorf("pick failed: rnd=%d, total=%d, acc=%d", rnd, total, acc)
}
proof := map[string]any{
"weights_total": total,
"rand": rnd,
"seed_hash": fmt.Sprintf("%x", sha256.Sum256(seedKey)),
"salt": fmt.Sprintf("%x", salt),
"seed_type": "commitment",
}
return picked, proof, nil
}
func (s *DefaultStrategy) SelectItemBySlot(ctx context.Context, activityID int64, issueID int64, slotIndex int64) (int64, map[string]any, error) {
return 0, nil, errors.New("default strategy does not support SelectItemBySlot")
}
func (s *DefaultStrategy) GrantReward(ctx context.Context, userID int64, rewardID int64) error {
// 默认策略(纯权重模式)不再执行库存扣减
return nil
}
func (s *DefaultStrategy) PostEffects(ctx context.Context, userID int64, activityID int64, issueID int64, rewardID int64) error {
return nil
}