bindbox-game/internal/api/admin/verify_draw.go
邹方成 6ee627139c
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 40s
feat: 新增支付测试小程序与微信支付集成
feat(pay): 添加支付API基础结构
feat(miniapp): 创建支付测试小程序页面与配置
feat(wechatpay): 配置微信支付参数与证书
fix(guild): 修复成员列表查询条件
docs: 更新代码规范文档与需求文档
style: 统一前后端枚举显示与注释格式
refactor(admin): 重构用户奖励发放接口参数处理
test(title): 添加称号效果参数验证测试
2025-11-17 00:42:08 +08:00

153 lines
5.8 KiB
Go

package admin
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"net/http"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type verifyDrawRequest struct {
AlgoVersion string `json:"algo_version" binding:"required"`
IssueID int64 `json:"issue_id" binding:"required"`
DrawID int64 `json:"draw_id" binding:"required"`
ClientSeed string `json:"client_seed" binding:"required"`
ServerSeedHash string `json:"server_seed_hash" binding:"required"`
ItemsRoot string `json:"items_root" binding:"required"`
WeightsTotal uint64 `json:"weights_total" binding:"required"`
SelectedIndex int `json:"selected_index" binding:"required,min=0"`
RandProof string `json:"rand_proof" binding:"required"`
}
type verifyDrawResponse struct {
Valid bool `json:"valid"`
Message string `json:"message"`
ServerSeedHashExpected string `json:"server_seed_hash_expected,omitempty"`
ItemsRootExpected string `json:"items_root_expected,omitempty"`
}
// VerifyDrawReceipt 验证抽奖收据
// @Summary 验证抽奖收据
// @Description 根据收据内容与期承诺,验证随机性证明与索引合法性
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param issue_id path integer true "期ID"
// @Param RequestBody body verifyDrawRequest true "请求参数"
// @Success 200 {object} verifyDrawResponse
// @Failure 400 {object} code.Failure
// @Security LoginVerifyToken
// @Router /api/admin/activities/{activity_id}/issues/{issue_id}/verify_draw [post]
func (h *handler) VerifyDrawReceipt() core.HandlerFunc {
return func(ctx core.Context) {
req := new(verifyDrawRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
cm, err := h.activity.GetIssueRandomCommit(ctx.RequestContext(), req.IssueID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.GetIssueRandomCommitError, err.Error()))
return
}
if cm == nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.GetIssueRandomCommitError, "未找到承诺"))
return
}
serverSeedHashBytes := cm.ServerSeedHash[:]
itemsRootBytes := cm.ItemsRoot[:]
if hex.EncodeToString(serverSeedHashBytes) != req.ServerSeedHash {
ctx.Payload(&verifyDrawResponse{
Valid: false,
Message: "server_seed_hash 不匹配",
ServerSeedHashExpected: hex.EncodeToString(serverSeedHashBytes),
})
return
}
if hex.EncodeToString(itemsRootBytes) != req.ItemsRoot {
ctx.Payload(&verifyDrawResponse{
Valid: false,
Message: "items_root 不匹配",
ItemsRootExpected: hex.EncodeToString(itemsRootBytes),
})
return
}
rewards, err := h.activity.ListIssueRewards(ctx.RequestContext(), req.IssueID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListIssueRewardsError, err.Error()))
return
}
if req.SelectedIndex >= len(rewards) {
ctx.Payload(&verifyDrawResponse{
Valid: false,
Message: "selected_index 超出奖励范围",
})
return
}
sel := rewards[req.SelectedIndex]
if sel.Weight <= 0 || (sel.Quantity != -1 && sel.Quantity <= 0) {
ctx.Payload(&verifyDrawResponse{
Valid: false,
Message: "selected_index 对应奖励不可抽",
})
return
}
proofBytes, err := hex.DecodeString(req.RandProof)
if err != nil {
ctx.Payload(&verifyDrawResponse{
Valid: false,
Message: "rand_proof 格式错误",
})
return
}
clientSeedBytes, err := hex.DecodeString(req.ClientSeed)
if err != nil {
ctx.Payload(&verifyDrawResponse{
Valid: false,
Message: "client_seed 格式错误",
})
return
}
master := unmaskSeed(cm.ServerSeedMaster, cm.IssueID, cm.StateVersion)
subInput := make([]byte, 16)
binary.BigEndian.PutUint64(subInput[:8], uint64(req.IssueID))
binary.BigEndian.PutUint64(subInput[8:], uint64(req.DrawID))
mac := hmac.New(sha256.New, master)
mac.Write(subInput)
serverSubSeed := mac.Sum(nil)
enc := encodeMessage(req.AlgoVersion, req.IssueID, req.DrawID, 0, clientSeedBytes, 1, itemsRootBytes, req.WeightsTotal)
entropy := hmacSha256(serverSubSeed, enc)
var final []byte
{
W := req.WeightsTotal
var counter uint64
for {
R := binary.BigEndian.Uint64(entropy[:8])
M := (uint64(^uint64(0)) / W) * W
if R < M {
final = entropy
break
}
counter++
cenc := make([]byte, len(enc)+8)
copy(cenc, enc)
binary.BigEndian.PutUint64(cenc[len(enc):], counter)
entropy = hmacSha256(serverSubSeed, cenc)
}
}
if !hmac.Equal(final, proofBytes) {
ctx.Payload(&verifyDrawResponse{
Valid: false,
Message: "rand_proof 验证失败",
})
return
}
ctx.Payload(&verifyDrawResponse{Valid: true, Message: "验证通过"})
}
}