673 lines
21 KiB
Go
673 lines
21 KiB
Go
package livestream
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math/big"
|
|
"time"
|
|
|
|
"bindbox-game/internal/pkg/logger"
|
|
"bindbox-game/internal/repository/mysql"
|
|
"bindbox-game/internal/repository/mysql/model"
|
|
"bindbox-game/internal/service/game"
|
|
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Service 直播间游戏服务接口
|
|
type Service interface {
|
|
// CreateActivity 创建直播间活动
|
|
CreateActivity(ctx context.Context, input CreateActivityInput) (*model.LivestreamActivities, error)
|
|
// UpdateActivity 更新活动
|
|
UpdateActivity(ctx context.Context, id int64, input UpdateActivityInput) error
|
|
// GetActivity 获取活动详情
|
|
GetActivity(ctx context.Context, id int64) (*model.LivestreamActivities, error)
|
|
// GetActivityByAccessCode 根据访问码获取活动
|
|
GetActivityByAccessCode(ctx context.Context, code string) (*model.LivestreamActivities, error)
|
|
// ListActivities 活动列表
|
|
ListActivities(ctx context.Context, page, pageSize int, status *int32) ([]*model.LivestreamActivities, int64, error)
|
|
// DeleteActivity 删除活动
|
|
DeleteActivity(ctx context.Context, id int64) error
|
|
|
|
// CreatePrizes 批量创建奖品
|
|
CreatePrizes(ctx context.Context, activityID int64, prizes []CreatePrizeInput) error
|
|
// ListPrizes 获取活动奖品列表
|
|
ListPrizes(ctx context.Context, activityID int64) ([]*model.LivestreamPrizes, error)
|
|
// UpdatePrize 更新奖品
|
|
UpdatePrize(ctx context.Context, prizeID int64, input UpdatePrizeInput) error
|
|
// DeletePrize 删除奖品
|
|
DeletePrize(ctx context.Context, prizeID int64) error
|
|
|
|
// Draw 执行抽奖
|
|
Draw(ctx context.Context, input DrawInput) (*DrawResult, error)
|
|
// ListDrawLogs 获取中奖记录
|
|
ListDrawLogs(ctx context.Context, activityID int64, page, pageSize int, startTime, endTime *time.Time) ([]*model.LivestreamDrawLogs, int64, error)
|
|
|
|
// GetActivityByProductID 根据抖店商品ID获取活动
|
|
GetActivityByProductID(ctx context.Context, productID string) (*model.LivestreamActivities, error)
|
|
|
|
// GenerateCommitment 为活动生成承诺种子
|
|
GenerateCommitment(ctx context.Context, activityID int64) (int32, error)
|
|
// GetCommitmentSummary 获取活动承诺摘要
|
|
GetCommitmentSummary(ctx context.Context, activityID int64) (*CommitmentSummary, error)
|
|
}
|
|
|
|
type service struct {
|
|
logger logger.CustomLogger
|
|
repo mysql.Repo
|
|
ticketSvc game.TicketService // 新增:游戏资格服务
|
|
}
|
|
|
|
// New 创建直播间服务
|
|
func New(l logger.CustomLogger, repo mysql.Repo, ticketSvc game.TicketService) Service {
|
|
return &service{
|
|
logger: l,
|
|
repo: repo,
|
|
ticketSvc: ticketSvc,
|
|
}
|
|
}
|
|
|
|
// ========== Input/Output 结构体 ==========
|
|
|
|
type CreateActivityInput struct {
|
|
Name string
|
|
StreamerName string
|
|
StreamerContact string
|
|
DouyinProductID string
|
|
OrderRewardType string
|
|
OrderRewardQuantity int32
|
|
TicketPrice int64
|
|
StartTime *time.Time
|
|
EndTime *time.Time
|
|
}
|
|
|
|
type UpdateActivityInput struct {
|
|
Name string
|
|
StreamerName string
|
|
StreamerContact string
|
|
DouyinProductID string
|
|
OrderRewardType string
|
|
OrderRewardQuantity *int32
|
|
TicketPrice *int64
|
|
Status *int32
|
|
StartTime *time.Time
|
|
EndTime *time.Time
|
|
}
|
|
|
|
type CreatePrizeInput struct {
|
|
Name string
|
|
Image string
|
|
Weight int32
|
|
Quantity int32
|
|
Level int32
|
|
ProductID int64 `json:"product_id"`
|
|
CostPrice int64 `json:"cost_price"`
|
|
}
|
|
|
|
type UpdatePrizeInput struct {
|
|
ID int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Level int32 `json:"level"`
|
|
Weight int32 `json:"weight"`
|
|
Quantity int32 `json:"quantity"`
|
|
Image string `json:"image"`
|
|
ProductID int64 `json:"product_id"`
|
|
CostPrice int64 `json:"cost_price"`
|
|
}
|
|
|
|
type DrawInput struct {
|
|
ActivityID int64
|
|
DouyinOrderID int64
|
|
ShopOrderID string
|
|
LocalUserID int64
|
|
DouyinUserID string
|
|
UserNickname string
|
|
}
|
|
|
|
type DrawResult struct {
|
|
Prize *model.LivestreamPrizes
|
|
DrawLog *model.LivestreamDrawLogs
|
|
SeedHash string
|
|
Receipt *DrawReceipt
|
|
}
|
|
|
|
// CommitmentSummary 承诺摘要
|
|
type CommitmentSummary struct {
|
|
SeedVersion int32 `json:"seed_version"`
|
|
Algo string `json:"algo"`
|
|
HasSeed bool `json:"has_seed"`
|
|
LenSeed int `json:"len_seed_master"`
|
|
LenHash int `json:"len_seed_hash"`
|
|
SeedHashHex string `json:"seed_hash_hex"` // 种子哈希的十六进制表示(可公开)
|
|
}
|
|
|
|
// DrawReceipt 抽奖凭证
|
|
type DrawReceipt struct {
|
|
SeedVersion int32 `json:"seed_version"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
Nonce int64 `json:"nonce"`
|
|
Signature string `json:"signature"`
|
|
Algorithm string `json:"algorithm"`
|
|
}
|
|
|
|
// ========== 活动管理 ==========
|
|
|
|
func (s *service) CreateActivity(ctx context.Context, input CreateActivityInput) (*model.LivestreamActivities, error) {
|
|
// 生成唯一访问码
|
|
accessCode := generateAccessCode()
|
|
|
|
activity := &model.LivestreamActivities{
|
|
Name: input.Name,
|
|
StreamerName: input.StreamerName,
|
|
StreamerContact: input.StreamerContact,
|
|
AccessCode: accessCode,
|
|
DouyinProductID: input.DouyinProductID,
|
|
OrderRewardType: input.OrderRewardType,
|
|
OrderRewardQuantity: input.OrderRewardQuantity,
|
|
TicketPrice: int32(input.TicketPrice),
|
|
Status: 1,
|
|
}
|
|
|
|
// 构建要插入的字段列表,排除空的时间字段
|
|
columns := []string{"name", "streamer_name", "streamer_contact", "access_code", "douyin_product_id", "order_reward_type", "order_reward_quantity", "ticket_price", "status"}
|
|
if input.StartTime != nil {
|
|
activity.StartTime = *input.StartTime
|
|
columns = append(columns, "start_time")
|
|
}
|
|
if input.EndTime != nil {
|
|
activity.EndTime = *input.EndTime
|
|
columns = append(columns, "end_time")
|
|
}
|
|
|
|
if err := s.repo.GetDbW().WithContext(ctx).Select(columns).Create(activity).Error; err != nil {
|
|
return nil, fmt.Errorf("创建直播间活动失败: %w", err)
|
|
}
|
|
return activity, nil
|
|
}
|
|
|
|
func (s *service) UpdateActivity(ctx context.Context, id int64, input UpdateActivityInput) error {
|
|
updates := make(map[string]any)
|
|
if input.Name != "" {
|
|
updates["name"] = input.Name
|
|
}
|
|
if input.StreamerName != "" {
|
|
updates["streamer_name"] = input.StreamerName
|
|
}
|
|
if input.StreamerContact != "" {
|
|
updates["streamer_contact"] = input.StreamerContact
|
|
}
|
|
if input.DouyinProductID != "" {
|
|
updates["douyin_product_id"] = input.DouyinProductID
|
|
}
|
|
if input.OrderRewardType != "" {
|
|
updates["order_reward_type"] = input.OrderRewardType
|
|
}
|
|
if input.OrderRewardQuantity != nil {
|
|
updates["order_reward_quantity"] = *input.OrderRewardQuantity
|
|
}
|
|
if input.TicketPrice != nil {
|
|
updates["ticket_price"] = int32(*input.TicketPrice)
|
|
}
|
|
if input.Status != nil {
|
|
updates["status"] = *input.Status
|
|
}
|
|
if input.StartTime != nil {
|
|
updates["start_time"] = *input.StartTime
|
|
}
|
|
if input.EndTime != nil {
|
|
updates["end_time"] = *input.EndTime
|
|
}
|
|
|
|
if len(updates) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return s.repo.GetDbW().WithContext(ctx).Model(&model.LivestreamActivities{}).
|
|
Where("id = ?", id).Updates(updates).Error
|
|
}
|
|
|
|
func (s *service) GetActivity(ctx context.Context, id int64) (*model.LivestreamActivities, error) {
|
|
var activity model.LivestreamActivities
|
|
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", id).First(&activity).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &activity, nil
|
|
}
|
|
|
|
func (s *service) GetActivityByAccessCode(ctx context.Context, code string) (*model.LivestreamActivities, error) {
|
|
var activity model.LivestreamActivities
|
|
if err := s.repo.GetDbR().WithContext(ctx).Where("access_code = ? AND deleted_at IS NULL", code).First(&activity).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &activity, nil
|
|
}
|
|
|
|
func (s *service) GetActivityByProductID(ctx context.Context, productID string) (*model.LivestreamActivities, error) {
|
|
var activity model.LivestreamActivities
|
|
if err := s.repo.GetDbR().WithContext(ctx).
|
|
Where("douyin_product_id = ? AND status = 1 AND deleted_at IS NULL", productID).
|
|
First(&activity).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &activity, nil
|
|
}
|
|
|
|
func (s *service) ListActivities(ctx context.Context, page, pageSize int, status *int32) ([]*model.LivestreamActivities, int64, error) {
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
if pageSize <= 0 {
|
|
pageSize = 20
|
|
}
|
|
|
|
db := s.repo.GetDbR().WithContext(ctx).Model(&model.LivestreamActivities{}).Where("deleted_at IS NULL")
|
|
if status != nil {
|
|
db = db.Where("status = ?", *status)
|
|
}
|
|
|
|
var total int64
|
|
if err := db.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
var list []*model.LivestreamActivities
|
|
if err := db.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&list).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return list, total, nil
|
|
}
|
|
|
|
func (s *service) DeleteActivity(ctx context.Context, id int64) error {
|
|
return s.repo.GetDbW().WithContext(ctx).Delete(&model.LivestreamActivities{}, id).Error
|
|
}
|
|
|
|
// ========== 奖品管理 ==========
|
|
|
|
func (s *service) CreatePrizes(ctx context.Context, activityID int64, prizes []CreatePrizeInput) error {
|
|
if len(prizes) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var models []*model.LivestreamPrizes
|
|
for _, p := range prizes {
|
|
remaining := p.Quantity
|
|
if remaining < 0 {
|
|
remaining = -1
|
|
}
|
|
models = append(models, &model.LivestreamPrizes{
|
|
ActivityID: activityID,
|
|
Name: p.Name,
|
|
Image: p.Image,
|
|
Weight: p.Weight,
|
|
Quantity: p.Quantity,
|
|
Remaining: remaining,
|
|
Level: p.Level,
|
|
ProductID: p.ProductID,
|
|
CostPrice: p.CostPrice,
|
|
Sort: 0, // Default sort value as it's removed from input
|
|
})
|
|
}
|
|
|
|
return s.repo.GetDbW().WithContext(ctx).Create(&models).Error
|
|
}
|
|
|
|
func (s *service) ListPrizes(ctx context.Context, activityID int64) ([]*model.LivestreamPrizes, error) {
|
|
var list []*model.LivestreamPrizes
|
|
if err := s.repo.GetDbR().WithContext(ctx).
|
|
Where("activity_id = ?", activityID).
|
|
Order("sort ASC, id ASC").
|
|
Find(&list).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
func (s *service) UpdatePrize(ctx context.Context, prizeID int64, input UpdatePrizeInput) error {
|
|
updates := make(map[string]any)
|
|
if input.Name != "" {
|
|
updates["name"] = input.Name
|
|
}
|
|
if input.Image != "" {
|
|
updates["image"] = input.Image
|
|
}
|
|
if input.Weight > 0 {
|
|
updates["weight"] = input.Weight
|
|
}
|
|
if input.Quantity >= 0 { // Allow 0 quantity update? Assuming yes. But 0 is zero-value. If user wants to set 0, logic needs distinction. Assuming Partial Update with Value Types is tricky. For now check > -1 if possible or just non-zero if quantity can't be 0? Usually quantity update is explicit.
|
|
// Since struct is Value type, we can't distinguish 0 from "not set".
|
|
// User changed to Value type likely assuming "replace" or front-end sends all fields.
|
|
// However, standard Update usually implies partial.
|
|
// Let's assume > 0 for now or if we treat input as full replacement?
|
|
// Given `UpdatePrizeInput` structure, it likely carries the full desired state or partial.
|
|
// If partial, int fields are problematic.
|
|
// Let's use > 0 for non-zero values, assuming 0 isn't a valid "change to" target for weight/level/quantity unless explicit.
|
|
// Actually, quantity can be 0 (sold out). But if client sends 0 as "no change", we have bug.
|
|
// Since user changed specific fields, they might be relying on frontend logic.
|
|
// I will assume standard non-zero checks for strings and >0 or specific logic for ints.
|
|
// If strictly following "compilation fix", I replacing nil checks with value checks.
|
|
updates["quantity"] = input.Quantity
|
|
updates["remaining"] = input.Quantity
|
|
}
|
|
if input.Level > 0 {
|
|
updates["level"] = input.Level
|
|
}
|
|
if input.ProductID > 0 {
|
|
updates["product_id"] = input.ProductID
|
|
}
|
|
if input.CostPrice > 0 { // Assume cost price is positive? Or allow 0? Updates map approach usually omits if 0.
|
|
updates["cost_price"] = input.CostPrice
|
|
}
|
|
|
|
if len(updates) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return s.repo.GetDbW().WithContext(ctx).Model(&model.LivestreamPrizes{}).
|
|
Where("id = ?", prizeID).Updates(updates).Error
|
|
}
|
|
|
|
func (s *service) DeletePrize(ctx context.Context, prizeID int64) error {
|
|
return s.repo.GetDbW().WithContext(ctx).Delete(&model.LivestreamPrizes{}, prizeID).Error
|
|
}
|
|
|
|
// ========== 抽奖逻辑 ==========
|
|
|
|
// selectPrizeByWeight 根据随机值和权重选择奖品(不考虑库存)
|
|
func selectPrizeByWeight(prizes []*model.LivestreamPrizes, randValue int64) *model.LivestreamPrizes {
|
|
var cumulative int64
|
|
for _, p := range prizes {
|
|
cumulative += int64(p.Weight)
|
|
if randValue < cumulative {
|
|
return p
|
|
}
|
|
}
|
|
if len(prizes) > 0 {
|
|
return prizes[len(prizes)-1]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// findFallbackPrize 找到权重最大的有库存奖品作为兜底
|
|
func findFallbackPrize(prizes []*model.LivestreamPrizes) *model.LivestreamPrizes {
|
|
var fallback *model.LivestreamPrizes
|
|
for _, p := range prizes {
|
|
if p.Remaining == 0 {
|
|
continue
|
|
}
|
|
if fallback == nil || p.Weight > fallback.Weight {
|
|
fallback = p
|
|
}
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// drawWithFallback 抽奖:抽中售罄奖品时,穿透到权重最大的有库存奖品
|
|
func (s *service) drawWithFallback(prizes []*model.LivestreamPrizes, totalWeight int64) (*model.LivestreamPrizes, int64, error) {
|
|
randBig, err := rand.Int(rand.Reader, big.NewInt(totalWeight))
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("生成随机数失败: %w", err)
|
|
}
|
|
randValue := randBig.Int64()
|
|
|
|
selected := selectPrizeByWeight(prizes, randValue)
|
|
if selected == nil {
|
|
return nil, 0, fmt.Errorf("奖品选择失败")
|
|
}
|
|
|
|
// 有库存,直接返回
|
|
if selected.Remaining == -1 || selected.Remaining > 0 {
|
|
return selected, randValue, nil
|
|
}
|
|
|
|
// 售罄,穿透到权重最大的有库存奖品
|
|
fallback := findFallbackPrize(prizes)
|
|
if fallback == nil {
|
|
return nil, 0, fmt.Errorf("没有可用奖品")
|
|
}
|
|
|
|
s.logger.Info("抽中售罄奖品,穿透到兜底奖品",
|
|
zap.Int64("original_prize_id", selected.ID),
|
|
zap.String("original_prize_name", selected.Name),
|
|
zap.Int64("fallback_prize_id", fallback.ID),
|
|
zap.String("fallback_prize_name", fallback.Name),
|
|
)
|
|
|
|
return fallback, randValue, nil
|
|
}
|
|
|
|
func (s *service) Draw(ctx context.Context, input DrawInput) (*DrawResult, error) {
|
|
// 0. 检查黑名单
|
|
if input.DouyinUserID != "" {
|
|
var blacklistCount int64
|
|
if err := s.repo.GetDbR().WithContext(ctx).
|
|
Table("douyin_blacklist").
|
|
Where("douyin_user_id = ? AND status = 1", input.DouyinUserID).
|
|
Count(&blacklistCount).Error; err == nil && blacklistCount > 0 {
|
|
return nil, fmt.Errorf("该用户已被列入黑名单,无法开奖")
|
|
}
|
|
}
|
|
|
|
// 1. 获取所有奖品
|
|
prizes, err := s.ListPrizes(ctx, input.ActivityID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("获取奖品列表失败: %w", err)
|
|
}
|
|
|
|
if len(prizes) == 0 {
|
|
return nil, fmt.Errorf("没有配置奖品")
|
|
}
|
|
|
|
// 2. 计算总权重(所有奖品都参与,保持概率恒定)
|
|
var totalWeight int64
|
|
var hasAvailable bool
|
|
for _, p := range prizes {
|
|
totalWeight += int64(p.Weight)
|
|
if p.Remaining != 0 { // -1 表示无限,>0 表示有库存
|
|
hasAvailable = true
|
|
}
|
|
}
|
|
|
|
if totalWeight == 0 {
|
|
return nil, fmt.Errorf("奖品权重配置异常")
|
|
}
|
|
|
|
if !hasAvailable {
|
|
return nil, fmt.Errorf("没有可用奖品(全部售罄)")
|
|
}
|
|
|
|
// 3. 生成随机种子(用于凭证)
|
|
seedBytes := make([]byte, 32)
|
|
if _, err := rand.Read(seedBytes); err != nil {
|
|
return nil, fmt.Errorf("生成随机种子失败: %w", err)
|
|
}
|
|
seedHash := sha256.Sum256(seedBytes)
|
|
seedHex := hex.EncodeToString(seedHash[:])
|
|
|
|
// 4. 穿透抽奖:抽中售罄奖品时给权重最大的有库存奖品
|
|
selectedPrize, randValue, err := s.drawWithFallback(prizes, totalWeight)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 6. 事务:扣减库存 + 记录中奖
|
|
var drawLog *model.LivestreamDrawLogs
|
|
err = s.repo.GetDbW().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
// 扣减库存(仅当 remaining > 0 时)
|
|
if selectedPrize.Remaining > 0 {
|
|
result := tx.Model(&model.LivestreamPrizes{}).
|
|
Where("id = ? AND remaining > 0", selectedPrize.ID).
|
|
Update("remaining", gorm.Expr("remaining - 1"))
|
|
if result.RowsAffected == 0 {
|
|
return fmt.Errorf("库存不足")
|
|
}
|
|
}
|
|
|
|
// 记录中奖
|
|
drawLog = &model.LivestreamDrawLogs{
|
|
ActivityID: input.ActivityID,
|
|
PrizeID: selectedPrize.ID,
|
|
DouyinOrderID: input.DouyinOrderID,
|
|
ShopOrderID: input.ShopOrderID,
|
|
LocalUserID: input.LocalUserID,
|
|
DouyinUserID: input.DouyinUserID,
|
|
UserNickname: input.UserNickname,
|
|
PrizeName: selectedPrize.Name,
|
|
Level: selectedPrize.Level,
|
|
SeedHash: seedHex,
|
|
RandValue: randValue,
|
|
WeightsTotal: totalWeight,
|
|
}
|
|
return tx.Create(drawLog).Error
|
|
})
|
|
|
|
if err != nil {
|
|
s.logger.Error("直播间抽奖失败", zap.Error(err), zap.Int64("activity_id", input.ActivityID))
|
|
return nil, err
|
|
}
|
|
|
|
s.logger.Info("直播间抽奖成功",
|
|
zap.Int64("activity_id", input.ActivityID),
|
|
zap.Int64("prize_id", selectedPrize.ID),
|
|
zap.String("prize_name", selectedPrize.Name),
|
|
)
|
|
|
|
// 7. 生成可验证凭证
|
|
var receipt *DrawReceipt
|
|
var activity model.LivestreamActivities
|
|
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", input.ActivityID).First(&activity).Error; err == nil {
|
|
if len(activity.CommitmentSeedMaster) > 0 {
|
|
ts := time.Now().UnixMilli()
|
|
nonce := time.Now().UnixNano()
|
|
// 构建签名载荷
|
|
payload := fmt.Sprintf("activity:%d|order:%s|draw:%d|ts:%d|nonce:%d",
|
|
input.ActivityID, input.ShopOrderID, drawLog.ID, ts, nonce)
|
|
// HMAC-SHA256 签名
|
|
mac := hmac.New(sha256.New, activity.CommitmentSeedMaster)
|
|
mac.Write([]byte(payload))
|
|
sig := hex.EncodeToString(mac.Sum(nil))
|
|
|
|
receipt = &DrawReceipt{
|
|
SeedVersion: activity.CommitmentStateVersion,
|
|
Timestamp: ts,
|
|
Nonce: nonce,
|
|
Signature: sig,
|
|
Algorithm: "HMAC-SHA256",
|
|
}
|
|
}
|
|
}
|
|
|
|
return &DrawResult{
|
|
Prize: selectedPrize,
|
|
DrawLog: drawLog,
|
|
SeedHash: seedHex,
|
|
Receipt: receipt,
|
|
}, nil
|
|
}
|
|
|
|
func (s *service) ListDrawLogs(ctx context.Context, activityID int64, page, pageSize int, startTime, endTime *time.Time) ([]*model.LivestreamDrawLogs, int64, error) {
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
if pageSize <= 0 {
|
|
pageSize = 20
|
|
}
|
|
|
|
db := s.repo.GetDbR().WithContext(ctx).Model(&model.LivestreamDrawLogs{}).Where("activity_id = ?", activityID)
|
|
|
|
if startTime != nil {
|
|
db = db.Where("created_at >= ?", startTime)
|
|
}
|
|
if endTime != nil {
|
|
db = db.Where("created_at <= ?", endTime)
|
|
}
|
|
|
|
var total int64
|
|
if err := db.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
var list []*model.LivestreamDrawLogs
|
|
if err := db.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&list).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return list, total, nil
|
|
}
|
|
|
|
// ========== 承诺管理 ==========
|
|
|
|
// GenerateCommitment 为活动生成承诺种子
|
|
func (s *service) GenerateCommitment(ctx context.Context, activityID int64) (int32, error) {
|
|
// 获取当前版本号
|
|
var activity model.LivestreamActivities
|
|
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", activityID).First(&activity).Error; err != nil {
|
|
return 0, fmt.Errorf("活动不存在: %w", err)
|
|
}
|
|
|
|
// 生成 32 字节随机种子
|
|
seed := make([]byte, 32)
|
|
if _, err := rand.Read(seed); err != nil {
|
|
return 0, fmt.Errorf("生成随机种子失败: %w", err)
|
|
}
|
|
|
|
// 计算 SHA256 哈希
|
|
seedHash := sha256.Sum256(seed)
|
|
|
|
// 更新数据库
|
|
newVersion := activity.CommitmentStateVersion + 1
|
|
if err := s.repo.GetDbW().WithContext(ctx).Model(&model.LivestreamActivities{}).
|
|
Where("id = ?", activityID).
|
|
Updates(map[string]any{
|
|
"commitment_algo": "commit-v1",
|
|
"commitment_seed_master": seed,
|
|
"commitment_seed_hash": seedHash[:],
|
|
"commitment_state_version": newVersion,
|
|
}).Error; err != nil {
|
|
return 0, fmt.Errorf("更新承诺失败: %w", err)
|
|
}
|
|
|
|
s.logger.Info("直播间活动承诺已生成",
|
|
zap.Int64("activity_id", activityID),
|
|
zap.Int32("version", newVersion),
|
|
)
|
|
|
|
return newVersion, nil
|
|
}
|
|
|
|
// GetCommitmentSummary 获取活动承诺摘要
|
|
func (s *service) GetCommitmentSummary(ctx context.Context, activityID int64) (*CommitmentSummary, error) {
|
|
var activity model.LivestreamActivities
|
|
if err := s.repo.GetDbR().WithContext(ctx).Where("id = ?", activityID).First(&activity).Error; err != nil {
|
|
return nil, fmt.Errorf("活动不存在: %w", err)
|
|
}
|
|
|
|
// 将种子哈希转为十六进制字符串
|
|
seedHashHex := ""
|
|
if len(activity.CommitmentSeedHash) > 0 {
|
|
seedHashHex = hex.EncodeToString(activity.CommitmentSeedHash)
|
|
}
|
|
|
|
return &CommitmentSummary{
|
|
SeedVersion: activity.CommitmentStateVersion,
|
|
Algo: activity.CommitmentAlgo,
|
|
HasSeed: len(activity.CommitmentSeedMaster) > 0,
|
|
LenSeed: len(activity.CommitmentSeedMaster),
|
|
LenHash: len(activity.CommitmentSeedHash),
|
|
SeedHashHex: seedHashHex,
|
|
}, nil
|
|
}
|
|
|
|
// ========== 辅助函数 ==========
|
|
|
|
func generateAccessCode() string {
|
|
b := make([]byte, 16)
|
|
rand.Read(b)
|
|
return hex.EncodeToString(b)
|
|
}
|