邹方成 e2782a69d3 feat: 添加对对碰游戏功能与Redis支持
refactor: 重构抽奖逻辑以支持可验证凭据
feat(redis): 集成Redis客户端并添加配置支持
fix: 修复订单取消时的优惠券和库存处理逻辑
docs: 添加对对碰游戏前端对接指南和示例JSON
test: 添加对对碰游戏模拟测试和验证逻辑
2025-12-21 17:31:32 +08:00

90 lines
2.6 KiB
Go

package strategy
import (
"bindbox-game/internal/repository/mysql/dao"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
)
type ichibanStrategy struct {
read *dao.Query
write *dao.Query
}
func NewIchiban(read *dao.Query, write *dao.Query) *ichibanStrategy {
return &ichibanStrategy{read: read, write: write}
}
func (s *ichibanStrategy) SelectItemBySlot(ctx context.Context, activityID int64, issueID int64, slotIndex int64) (int64, map[string]any, error) {
act, err := s.read.Activities.WithContext(ctx).Where(s.read.Activities.ID.Eq(activityID)).First()
if err != nil || act == nil || len(act.CommitmentSeedMaster) == 0 {
return 0, nil, errors.New("commitment not found")
}
rewards, err := s.read.ActivityRewardSettings.WithContext(ctx).ReadDB().Where(s.read.ActivityRewardSettings.IssueID.Eq(issueID)).Order(
s.read.ActivityRewardSettings.Level.Desc(),
s.read.ActivityRewardSettings.Sort.Asc(),
s.read.ActivityRewardSettings.ID.Asc(),
).Find()
if err != nil || len(rewards) == 0 {
return 0, nil, errors.New("no rewards")
}
var totalSlots int64
for _, r := range rewards {
if r.OriginalQty > 0 {
totalSlots += r.OriginalQty
}
}
if totalSlots <= 0 {
return 0, nil, errors.New("no slots")
}
if slotIndex < 0 || slotIndex >= totalSlots {
return 0, nil, errors.New("slot out of range")
}
// build list
slots := make([]int64, 0, totalSlots)
for _, r := range rewards {
for i := int64(0); i < r.OriginalQty; i++ {
slots = append(slots, r.ID)
}
}
// deterministic shuffle by server seed
mac := hmac.New(sha256.New, act.CommitmentSeedMaster)
for i := int(totalSlots - 1); i > 0; i-- {
mac.Reset()
mac.Write([]byte(fmt.Sprintf("shuffle:%d|issue:%d", i, issueID)))
sum := mac.Sum(nil)
rnd := int(binary.BigEndian.Uint64(sum[:8]) % uint64(i+1))
slots[i], slots[rnd] = slots[rnd], slots[i]
}
picked := slots[slotIndex]
// Calculate seed hash for proof
sha := sha256.Sum256(act.CommitmentSeedMaster)
seedHash := fmt.Sprintf("%x", sha)
proof := map[string]any{
"total_slots": totalSlots,
"slot_index": slotIndex,
"seed_hash": seedHash,
}
return picked, proof, nil
}
func (s *ichibanStrategy) GrantReward(ctx context.Context, userID int64, rewardID int64) error {
result, err := s.write.ActivityRewardSettings.WithContext(ctx).Where(
s.write.ActivityRewardSettings.ID.Eq(rewardID),
s.write.ActivityRewardSettings.Quantity.Gt(0),
).UpdateSimple(s.write.ActivityRewardSettings.Quantity.Add(-1))
if err != nil {
return err
}
if result.RowsAffected == 0 {
return errors.New("sold out or reward not found")
}
return nil
}