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 }