bindbox-game/internal/api/admin/batch_draw.go
邹方成 81e2fb5a75
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 41s
feat(activity): 实现抽奖随机承诺与验证功能
新增随机种子生成与验证逻辑,包括:
1. 添加随机承诺生成接口
2. 实现抽奖执行与验证流程
3. 新增批量用户创建与删除功能
4. 添加抽奖收据记录表
5. 完善配置管理与错误码

新增测试用例验证随机算法正确性
2025-11-15 20:39:13 +08:00

153 lines
4.3 KiB
Go

package admin
import (
"encoding/hex"
"encoding/json"
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql/model"
)
type batchDrawRequest struct {
UserIDs []int64 `json:"user_ids" binding:"required,min=1,max=1000,dive,min=1"`
}
type batchDrawResponse struct {
Draws []drawResultItem `json:"draws"`
}
type drawResultItem struct {
UserID int64 `json:"user_id"`
DrawID int64 `json:"draw_id"`
RewardID int64 `json:"reward_id"`
RewardName string `json:"reward_name"`
IsWinner bool `json:"is_winner"`
Receipt interface{} `json:"receipt"`
}
func (h *handler) BatchDrawForUsers() core.HandlerFunc {
return func(ctx core.Context) {
issueIDStr := ctx.Param("issue_id")
if issueIDStr == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递期ID"))
return
}
if _, err := strconv.ParseInt(issueIDStr, 10, 64); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
issueID, _ := strconv.ParseInt(issueIDStr, 10, 64)
var req batchDrawRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
rewards, err := h.activity.ListIssueRewards(ctx.RequestContext(), issueID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ExecuteDrawError, err.Error()))
return
}
rewardMap := make(map[int64]string, len(rewards))
for _, r := range rewards {
rewardMap[r.ID] = r.Name
}
// 开始事务处理
tx := h.writeDB.Begin()
if tx.Error != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ExecuteDrawError, "启动事务失败"))
return
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
}
}()
draws := make([]drawResultItem, 0, len(req.UserIDs))
for _, uid := range req.UserIDs {
// 执行抽奖
rec, err := h.activity.ExecuteDraw(ctx.RequestContext(), issueID)
if err != nil {
tx.Rollback()
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ExecuteDrawError, err.Error()))
return
}
// 创建抽奖日志
isWinner := int32(0)
if rec.SelectedItemId > 0 {
isWinner = 1
}
drawLog := &model.ActivityDrawLogs{
UserID: uid,
IssueID: issueID,
RewardID: rec.SelectedItemId,
IsWinner: isWinner,
Level: 1, // 默认等级
}
if err := tx.ActivityDrawLogs.Create(drawLog); err != nil {
tx.Rollback()
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ExecuteDrawError, "创建抽奖日志失败"))
return
}
// 保存抽奖收据
itemsSnapshot, _ := json.Marshal(rec.Items)
receipt := &model.ActivityDrawReceipts{
DrawLogID: drawLog.ID,
AlgoVersion: rec.AlgoVersion,
RoundID: rec.RoundId,
DrawID: rec.DrawId,
ClientID: rec.ClientId,
Timestamp: rec.Timestamp,
ServerSeedHash: hex.EncodeToString(rec.ServerSeedHash),
ServerSubSeed: hex.EncodeToString(rec.ServerSubSeed),
ClientSeed: hex.EncodeToString(rec.ClientSeed),
Nonce: int64(rec.Nonce),
ItemsRoot: hex.EncodeToString(rec.ItemsRoot),
WeightsTotal: int64(rec.WeightsTotal),
SelectedIndex: int32(rec.SelectedIndex),
RandProof: hex.EncodeToString(rec.RandProof),
Signature: "", // 可以后续添加签名
ItemsSnapshot: string(itemsSnapshot),
}
if err := tx.ActivityDrawReceipts.Create(receipt); err != nil {
tx.Rollback()
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ExecuteDrawError, "保存抽奖收据失败"))
return
}
name := ""
if rec.SelectedItemId > 0 {
name = rewardMap[rec.SelectedItemId]
}
draws = append(draws, drawResultItem{
UserID: uid,
DrawID: rec.DrawId,
RewardID: rec.SelectedItemId,
RewardName: name,
IsWinner: rec.SelectedItemId > 0,
Receipt: rec,
})
}
// 提交事务
if err := tx.Commit(); err != nil {
tx.Rollback()
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ExecuteDrawError, "提交事务失败"))
return
}
ctx.Payload(&batchDrawResponse{Draws: draws})
}
}