bindbox-game/internal/service/activity/draw_with_effects.go
邹方成 8141a47690
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 39s
feat(称号系统): 新增称号管理功能与抽奖效果集成
- 新增系统称号模板与效果配置表及相关CRUD接口
- 实现用户称号分配与抽奖效果应用逻辑
- 优化抽奖接口支持用户ID参数以应用称号效果
- 新增称号管理前端页面与分配功能
- 修复Windows时区错误与JSON字段初始化问题
- 移除无用管理接口代码并更新文档说明
2025-11-16 11:37:40 +08:00

233 lines
8.3 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 activity
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/json"
"time"
)
// ExecuteDrawWithEffects 执行抽奖(应用用户头衔效果:概率加成/双倍奖励)
// Params:
// - ctx: 上下文
// - issueID: 期ID
// - userID: 用户ID
// Returns:
// - 抽奖收据(已按效果调整权重并处理双倍奖励)
func (s *service) ExecuteDrawWithEffects(ctx context.Context, issueID int64, userID int64) (*Receipt, error) {
cm, err := s.GetIssueRandomCommit(ctx, issueID)
if err != nil {
return nil, err
}
if cm == nil {
return nil, nil
}
master := unmaskSeed(cm.ServerSeedMaster, cm.IssueID, cm.StateVersion)
items, err := s.readDB.ActivityRewardSettings.WithContext(ctx).
Where(s.readDB.ActivityRewardSettings.IssueID.Eq(issueID)).
Order(s.readDB.ActivityRewardSettings.Sort).
Find()
if err != nil {
return nil, err
}
// 初始权重与快照
var snapshot []ReceiptItem
baseWeights := make(map[int64]int32, len(items))
for _, it := range items {
snapshot = append(snapshot, ReceiptItem{ID: it.ID, Name: it.Name, Weight: it.Weight, QuantityBefore: it.Quantity})
if it.Weight > 0 && (it.Quantity == -1 || it.Quantity > 0) {
baseWeights[it.ID] = it.Weight
}
}
// 解析用户头衔效果仅过滤到当前issue
effects, err := s.readDB.SystemTitleEffects.WithContext(ctx).
Where(s.readDB.SystemTitleEffects.Status.Eq(1)).
Order(s.readDB.SystemTitleEffects.Sort).
Find()
if err != nil {
return nil, err
}
// 仅保留用户激活的头衔效果
now := time.Now()
uts, err := s.readDB.UserTitles.WithContext(ctx).
Where(s.readDB.UserTitles.UserID.Eq(userID)).
Where(s.readDB.UserTitles.Active.Eq(1)).
Find()
if err != nil {
return nil, err
}
titleSet := make(map[int64]struct{}, len(uts))
for _, ut := range uts {
if ut.ExpiresAt.IsZero() || ut.ExpiresAt.After(now) {
titleSet[ut.TitleID] = struct{}{}
}
}
// 作用域过滤issueID
type scopePayload struct {
IssueIDs []int64 `json:"issue_ids"`
Exclude struct{ IssueIDs []int64 `json:"issue_ids"` } `json:"exclude"`
}
// 累计概率加成与双倍奖励参数
boostPerItemX1000 := make(map[int64]int32)
var doubleChanceX1000 int32
var doubleTargets map[int64]struct{}
for _, ef := range effects {
if _, ok := titleSet[ef.TitleID]; !ok {
continue
}
// scope过滤
if ef.ScopesJSON != "" {
var sc scopePayload
if err := json.Unmarshal([]byte(ef.ScopesJSON), &sc); err == nil {
if len(sc.Exclude.IssueIDs) > 0 {
for _, ex := range sc.Exclude.IssueIDs {
if ex == issueID {
continue
}
}
}
if len(sc.IssueIDs) > 0 {
matched := false
for _, id := range sc.IssueIDs {
if id == issueID { matched = true; break }
}
if !matched { continue }
}
}
}
switch ef.EffectType {
case 5: // 概率加成
var p struct {
TargetPrizeIDs []int64 `json:"target_prize_ids"`
BoostX1000 int32 `json:"boost_x1000"`
CapX1000 *int32 `json:"cap_x1000"`
}
if err := json.Unmarshal([]byte(ef.ParamsJSON), &p); err != nil {
continue
}
// 累加或取最大1累加封顶0最大值2首个匹配
for _, tid := range p.TargetPrizeIDs {
curr := boostPerItemX1000[tid]
switch ef.StackingStrategy {
case 0: // max_only
if p.BoostX1000 > curr { boostPerItemX1000[tid] = p.BoostX1000 }
case 1: // sum_with_cap
nxt := curr + p.BoostX1000
if p.CapX1000 != nil && nxt > *p.CapX1000 { nxt = *p.CapX1000 }
boostPerItemX1000[tid] = nxt
case 2: // first_match
if curr == 0 { boostPerItemX1000[tid] = p.BoostX1000 }
default:
nxt := curr + p.BoostX1000
cap := ef.CapValueX1000
if cap > 0 && nxt > cap { nxt = cap }
boostPerItemX1000[tid] = nxt
}
}
case 6: // 双倍奖励卡
var p struct {
TargetPrizeIDs []int64 `json:"target_prize_ids"`
ChanceX1000 int32 `json:"chance_x1000"`
PeriodCapTimes *int32 `json:"period_cap_times"`
}
if err := json.Unmarshal([]byte(ef.ParamsJSON), &p); err != nil {
continue
}
// 合并双倍奖励命中率sum_with_cap 缺省)
if doubleTargets == nil { doubleTargets = make(map[int64]struct{}) }
for _, tid := range p.TargetPrizeIDs { doubleTargets[tid] = struct{}{} }
// 累加并封顶
doubleChanceX1000 += p.ChanceX1000
cap := ef.CapValueX1000
if cap > 0 && doubleChanceX1000 > cap { doubleChanceX1000 = cap }
}
}
// 应用概率加成:调整权重
var total int64
adjWeights := make(map[int64]int32, len(baseWeights))
for _, it := range items {
if it.Weight <= 0 || !(it.Quantity == -1 || it.Quantity > 0) { continue }
w := baseWeights[it.ID]
if inc, ok := boostPerItemX1000[it.ID]; ok && inc > 0 {
// w * (1 + inc/1000)
w = int32(int64(w) * (1000 + int64(inc)) / 1000)
if w < 1 { w = 1 }
}
adjWeights[it.ID] = w
total += int64(w)
}
if total <= 0 { return nil, nil }
// 随机选择(按调整后权重)
drawId := time.Now().UnixNano()
clientSeed := make([]byte, 32)
_, _ = rand.Read(clientSeed)
nonce := uint64(1)
subInput := make([]byte, 16)
binary.BigEndian.PutUint64(subInput[:8], uint64(issueID))
binary.BigEndian.PutUint64(subInput[8:16], uint64(drawId))
mac := hmac.New(sha256.New, master)
mac.Write(subInput)
serverSubSeed := mac.Sum(nil)
enc := encodeMessage(cm.AlgoVersion, issueID, drawId, 0, clientSeed, nonce, cm.ItemsRoot[:], uint64(total))
entropy := hmacSha256(serverSubSeed, enc)
pos, proof := rejectSample(entropy, serverSubSeed, enc, uint64(total))
var acc uint64
var selIndex int
var selID int64
var iIdx int
for i, it := range items {
if it.Weight <= 0 || !(it.Quantity == -1 || it.Quantity > 0) { continue }
w := uint64(adjWeights[it.ID])
if pos < acc+w {
selIndex = i
selID = it.ID
iIdx = i
break
}
acc += w
}
// 双倍奖励判定(若目标命中)
rewardMultiplierX1000 := int32(1000)
if selID > 0 && doubleChanceX1000 > 0 {
_, eligible := doubleTargets[selID]
if eligible {
// 使用另一次哈希派生随机判定
check := hmacSha256(serverSubSeed, []byte("double:"))
rv := int32(binary.BigEndian.Uint32(check[:4]) % 1000)
if rv < doubleChanceX1000 {
rewardMultiplierX1000 = 2000
}
}
}
rec := &Receipt{
AlgoVersion: cm.AlgoVersion,
RoundId: issueID,
DrawId: drawId,
ClientId: 0,
Timestamp: time.Now().UnixMilli(),
ServerSeedHash: cm.ServerSeedHash[:],
ServerSubSeed: serverSubSeed,
ClientSeed: clientSeed,
Nonce: nonce,
Items: snapshot,
ItemsRoot: cm.ItemsRoot[:],
WeightsTotal: uint64(total),
SelectedIndex: selIndex,
SelectedItemId: selID,
RandProof: proof,
Signature: nil,
}
// 将倍数编码回选中项的名称后缀以便上层识别(非侵入式)
if iIdx >= 0 && iIdx < len(rec.Items) && rewardMultiplierX1000 > 1000 {
rec.Items[iIdx].Name = rec.Items[iIdx].Name + "(x2)"
}
return rec, nil
}