邹方成 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

158 lines
4.9 KiB
Go

package activity
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"time"
)
// ExecuteDraw 执行一次抽奖并生成可验证收据
// 参数: issueID 期ID
// 返回: 收据对象(包含随机性证明、选择索引等)与错误
func (s *service) ExecuteDraw(ctx context.Context, issueID int64) (*Receipt, error) {
cm, err := s.GetIssueRandomCommit(ctx, issueID)
if err != nil {
return nil, err
}
if cm == nil {
return nil, nil
}
master := unmaskSeed(cm.ServerSeedMaster, cm.IssueID, cm.StateVersion)
items, err := s.readDB.ActivityRewardSettings.WithContext(ctx).
Where(s.readDB.ActivityRewardSettings.IssueID.Eq(issueID)).
Order(s.readDB.ActivityRewardSettings.Sort).
Find()
if err != nil {
return nil, err
}
var snapshot []ReceiptItem
var total int64
for _, it := range items {
snapshot = append(snapshot, ReceiptItem{ID: it.ID, Name: it.Name, Weight: it.Weight, QuantityBefore: it.Quantity})
if it.Weight > 0 && (it.Quantity == -1 || it.Quantity > 0) {
total += int64(it.Weight)
}
}
if total <= 0 {
return nil, nil
}
drawId := time.Now().UnixNano()
clientSeed := make([]byte, 32)
_, _ = rand.Read(clientSeed)
nonce := uint64(1)
subInput := make([]byte, 16)
binary.BigEndian.PutUint64(subInput[:8], uint64(issueID))
binary.BigEndian.PutUint64(subInput[8:16], uint64(drawId))
mac := hmac.New(sha256.New, master)
mac.Write(subInput)
serverSubSeed := mac.Sum(nil)
enc := encodeMessage(cm.AlgoVersion, issueID, drawId, 0, clientSeed, nonce, cm.ItemsRoot[:], uint64(total))
entropy := hmacSha256(serverSubSeed, enc)
pos, proof := rejectSample(entropy, serverSubSeed, enc, uint64(total))
var acc uint64
var selIndex int
var selID int64
for i, it := range items {
if it.Weight <= 0 || !(it.Quantity == -1 || it.Quantity > 0) {
continue
}
w := uint64(it.Weight)
if pos < acc+w {
selIndex = i
selID = it.ID
break
}
acc += w
}
rec := &Receipt{
AlgoVersion: cm.AlgoVersion,
RoundId: issueID,
DrawId: drawId,
ClientId: 0,
Timestamp: time.Now().UnixMilli(),
ServerSeedHash: cm.ServerSeedHash[:],
ServerSubSeed: serverSubSeed,
ClientSeed: clientSeed,
Nonce: nonce,
Items: snapshot,
ItemsRoot: cm.ItemsRoot[:],
WeightsTotal: uint64(total),
SelectedIndex: selIndex,
SelectedItemId: selID,
RandProof: proof,
Signature: nil,
}
return rec, nil
}
// encodeMessage 按协议编码随机消息
// 参数: 算法版本、期ID、抽奖ID、客户端ID、客户端种子、nonce、奖项根、总权重
// 返回: 编码后的消息字节
func encodeMessage(algo string, roundId int64, drawId int64, clientId int64, clientSeed []byte, nonce uint64, itemsRoot []byte, weightsTotal uint64) []byte {
var buf []byte
buf = appendUint32String(buf, algo)
buf = appendUint64(buf, uint64(roundId))
buf = appendUint64(buf, uint64(drawId))
buf = appendUint64(buf, uint64(clientId))
buf = appendUint32Bytes(buf, clientSeed)
buf = appendUint64(buf, nonce)
buf = append(buf, itemsRoot...)
buf = appendUint64(buf, weightsTotal)
return buf
}
// appendUint32String 追加长度前缀的字符串
func appendUint32String(b []byte, s string) []byte {
bs := []byte(s)
nb := make([]byte, 4)
binary.BigEndian.PutUint32(nb, uint32(len(bs)))
b = append(b, nb...)
b = append(b, bs...)
return b
}
// appendUint32Bytes 追加长度前缀的字节数组
func appendUint32Bytes(b []byte, bs []byte) []byte {
nb := make([]byte, 4)
binary.BigEndian.PutUint32(nb, uint32(len(bs)))
b = append(b, nb...)
b = append(b, bs...)
return b
}
// appendUint64 追加 uint64
func appendUint64(b []byte, v uint64) []byte {
nb := make([]byte, 8)
binary.BigEndian.PutUint64(nb, v)
b = append(b, nb...)
return b
}
// hmacSha256 计算 HMAC-SHA256
func hmacSha256(key []byte, msg []byte) []byte {
m := hmac.New(sha256.New, key)
m.Write(msg)
return m.Sum(nil)
}
// rejectSample 执行拒绝采样避免模偏差
// 参数: 初始熵、子种子、编码消息、总权重
// 返回: 采样位置与最终证明
func rejectSample(entropy []byte, subSeed []byte, enc []byte, W uint64) (uint64, []byte) {
var counter uint64
for {
R := binary.BigEndian.Uint64(entropy[:8])
M := (uint64(^uint64(0)) / W) * W
if R < M {
return R % W, entropy
}
counter++
cenc := make([]byte, len(enc)+8)
copy(cenc, enc)
binary.BigEndian.PutUint64(cenc[len(enc):], counter)
entropy = hmacSha256(subSeed, cenc)
}
}