refactor(utils): 修复密码哈希比较逻辑错误 feat(user): 新增按状态筛选优惠券接口 docs: 添加虚拟发货与任务中心相关文档 fix(wechat): 修正Code2Session上下文传递问题 test: 补充订单折扣与积分转换测试用例 build: 更新配置文件与构建脚本 style: 清理多余的空行与注释
168 lines
4.8 KiB
Go
168 lines
4.8 KiB
Go
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.Level.Desc(),
|
|
s.read.ActivityRewardSettings.Sort.Asc(),
|
|
s.read.ActivityRewardSettings.ID.Asc(),
|
|
).Find()
|
|
if err != nil || len(rewards) == 0 {
|
|
return nil, errors.New("no rewards")
|
|
}
|
|
var total int64
|
|
for _, r := range rewards {
|
|
if r.OriginalQty > 0 {
|
|
total += r.OriginalQty
|
|
}
|
|
}
|
|
if total <= 0 {
|
|
return nil, errors.New("no slots")
|
|
}
|
|
slots := make([]int64, 0, total)
|
|
for _, r := range rewards {
|
|
for i := int64(0); i < r.OriginalQty; i++ {
|
|
slots = append(slots, 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
|
|
OriginalQty int64
|
|
RemainingQty int64
|
|
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
|
|
if rw.ProductID > 0 {
|
|
p, _ := s.read.Products.WithContext(ctx).Where(s.read.Products.ID.Eq(rw.ProductID)).First()
|
|
if p != nil && 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: rw.Name, Level: rw.Level, ProductImage: img, OriginalQty: rw.OriginalQty, RemainingQty: rw.Quantity, 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
|
|
if rw.ProductID > 0 {
|
|
p, _ := s.read.Products.WithContext(ctx).Where(s.read.Products.ID.Eq(rw.ProductID)).First()
|
|
if p != nil && 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: rw.Name, Level: rw.Level, ProductImage: img, OriginalQty: rw.OriginalQty, RemainingQty: rw.Quantity, Claimed: claimed > 0}, nil
|
|
}
|