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 }