Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 39s
- 新增系统称号模板与效果配置表及相关CRUD接口 - 实现用户称号分配与抽奖效果应用逻辑 - 优化抽奖接口支持用户ID参数以应用称号效果 - 新增称号管理前端页面与分配功能 - 修复Windows时区错误与JSON字段初始化问题 - 移除无用管理接口代码并更新文档说明
233 lines
8.3 KiB
Go
233 lines
8.3 KiB
Go
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
|
||
} |