package activity import ( "bindbox-game/internal/repository/mysql" "bindbox-game/internal/repository/mysql/dao" "context" "crypto/hmac" "crypto/sha256" "encoding/binary" "encoding/hex" "encoding/json" "errors" "fmt" ) type IchibanSlotsService struct { read *dao.Query repo mysql.Repo } func NewIchibanSlotsService(read *dao.Query, repo mysql.Repo) *IchibanSlotsService { return &IchibanSlotsService{read: read, repo: repo} } func (s *IchibanSlotsService) buildMapping(ctx context.Context, activityID int64, issueID int64) ([]int64, error) { var seedHex string if err := s.repo.GetDbR().Raw("SELECT HEX(commitment_seed_master) FROM activities WHERE id=?", activityID).Scan(&seedHex).Error; err != nil || seedHex == "" { return nil, errors.New("commitment not found") } seed, _ := hex.DecodeString(seedHex) rewards, err := s.read.ActivityRewardSettings.WithContext(ctx).ReadDB().Where(s.read.ActivityRewardSettings.IssueID.Eq(issueID)).Order( s.read.ActivityRewardSettings.IsBoss.Desc(), s.read.ActivityRewardSettings.Level.Asc(), s.read.ActivityRewardSettings.Sort.Asc(), s.read.ActivityRewardSettings.ID.Asc(), ).Find() if err != nil || len(rewards) == 0 { return nil, errors.New("no rewards") } // 一番赏:每种奖品 = 1个格位 total := int64(len(rewards)) if total <= 0 { return nil, errors.New("no slots") } slots := make([]int64, total) for i, r := range rewards { slots[i] = r.ID } mac := hmac.New(sha256.New, seed) for i := int(total - 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] } return slots, nil } type SlotItem struct { SlotIndex int64 RewardID int64 RewardName string Level int32 ProductImage string Claimed bool } func (s *IchibanSlotsService) Page(ctx context.Context, activityID int64, issueID int64, page int, pageSize int, claimedFilter *bool) (int64, []SlotItem, error) { mapping, err := s.buildMapping(ctx, activityID, issueID) if err != nil { return 0, nil, err } total := int64(len(mapping)) if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 50 } start := int64((page - 1) * pageSize) end := start + int64(pageSize) if start > total { return total, []SlotItem{}, nil } if end > total { end = total } var claimedIdx []int64 if err := s.repo.GetDbR().Raw("SELECT slot_index FROM issue_position_claims WHERE issue_id = ?", issueID).Scan(&claimedIdx).Error; err != nil { claimedIdx = []int64{} } used := map[int64]struct{}{} for _, x := range claimedIdx { used[x] = struct{}{} } items := make([]SlotItem, 0, end-start) for i := start; i < end; i++ { rid := mapping[i] rw, _ := s.read.ActivityRewardSettings.WithContext(ctx).Where(s.read.ActivityRewardSettings.ID.Eq(rid)).First() if rw == nil { continue } var img string var productName string if rw.ProductID > 0 { p, _ := s.read.Products.WithContext(ctx).Where(s.read.Products.ID.Eq(rw.ProductID)).First() if p != nil { productName = p.Name if p.ImagesJSON != "" { var arr []string _ = json.Unmarshal([]byte(p.ImagesJSON), &arr) if len(arr) > 0 { img = arr[0] } } } } c := false if _, ok := used[i]; ok { c = true } if claimedFilter != nil { if *claimedFilter && !c { continue } if !*claimedFilter && c { continue } } items = append(items, SlotItem{SlotIndex: i + 1, RewardID: rid, RewardName: productName, Level: rw.Level, ProductImage: img, Claimed: c}) } return total, items, nil } func (s *IchibanSlotsService) SlotDetail(ctx context.Context, activityID int64, issueID int64, slotIndex int64) (SlotItem, error) { mapping, err := s.buildMapping(ctx, activityID, issueID) if err != nil { return SlotItem{}, err } idx0 := slotIndex - 1 if idx0 < 0 || idx0 >= int64(len(mapping)) { return SlotItem{}, errors.New("out of range") } rid := mapping[idx0] rw, _ := s.read.ActivityRewardSettings.WithContext(ctx).Where(s.read.ActivityRewardSettings.ID.Eq(rid)).First() if rw == nil { return SlotItem{}, errors.New("reward not found") } var img string var productName string if rw.ProductID > 0 { p, _ := s.read.Products.WithContext(ctx).Where(s.read.Products.ID.Eq(rw.ProductID)).First() if p != nil { productName = p.Name if p.ImagesJSON != "" { var arr []string _ = json.Unmarshal([]byte(p.ImagesJSON), &arr) if len(arr) > 0 { img = arr[0] } } } } var claimed int64 _ = s.repo.GetDbR().Raw("SELECT COUNT(1) FROM issue_position_claims WHERE issue_id=? AND slot_index=?", issueID, idx0).Scan(&claimed) return SlotItem{SlotIndex: slotIndex, RewardID: rid, RewardName: productName, Level: rw.Level, ProductImage: img, Claimed: claimed > 0}, nil }