feat(activity): 实现抽奖随机承诺与验证功能
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 41s

新增随机种子生成与验证逻辑,包括:
1. 添加随机承诺生成接口
2. 实现抽奖执行与验证流程
3. 新增批量用户创建与删除功能
4. 添加抽奖收据记录表
5. 完善配置管理与错误码

新增测试用例验证随机算法正确性
This commit is contained in:
邹方成 2025-11-15 20:39:13 +08:00
parent 00452cba59
commit 81e2fb5a75
37 changed files with 2449 additions and 11 deletions

BIN
.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
bindbox-server Executable file

Binary file not shown.

View File

@ -36,6 +36,6 @@ eg :
# 根目录下执行 # 根目录下执行
go run cmd/gormgen/main.go -dsn "root:api2api..@tcp(sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" -tables "admin,log_operation,log_request,activities,activity_categories,activity_draw_logs,activity_issues,activity_reward_settings,guild,guild_boxes,guild_contribute_logs,guild_members,system_coupons,user_coupons,user_inventory,user_inventory_transfers,user_points,user_points_ledger" go run cmd/gormgen/main.go -dsn "root:api2api..@tcp(sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" -tables "admin,log_operation,log_request,activities,activity_categories,activity_draw_logs,activity_issues,activity_reward_settings,guild,guild_boxes,guild_contribute_logs,guild_members,system_coupons,user_coupons,user_inventory,user_inventory_transfers,user_points,user_points_ledger"
go run cmd/gormgen/main.go -dsn "root:api2api..@tcp(sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" -tables "admin,log_operation,log_request,activities,activity_categories,activity_draw_logs,activity_issues,activity_reward_settings,guild,guild_boxes,guild_contribute_logs,guild_members,system_coupons,user_coupons,user_inventory,user_inventory_transfers,user_points,user_points_ledger,users,user_addresses,menu_actions,menus,role_actions,role_menus,role_users,roles,order_items,orders,products,shipping_records,product_categories,user_invites,system_item_cards,user_item_cards,activity_draw_effects,banner" go run cmd/gormgen/main.go -dsn "root:api2api..@tcp(sh-cynosdbmysql-grp-88th45wy.sql.tencentcdb.com:28555)/bindbox_game?charset=utf8mb4&parseTime=True&loc=Local" -tables "admin,log_operation,log_request,activities,activity_categories,activity_draw_logs,activity_issues,activity_reward_settings,guild,guild_boxes,guild_contribute_logs,guild_members,system_coupons,user_coupons,user_inventory,user_inventory_transfers,user_points,user_points_ledger,users,user_addresses,menu_actions,menus,role_actions,role_menus,role_users,roles,order_items,orders,products,shipping_records,product_categories,user_invites,system_item_cards,user_item_cards,activity_draw_effects,banner,issue_random_commitments,activity_draw_receipts"
``` ```

View File

@ -50,6 +50,10 @@ type Config struct {
SecretKey string `mapstructure:"secret_key" toml:"secret_key"` SecretKey string `mapstructure:"secret_key" toml:"secret_key"`
BaseURL string `mapstructure:"base_url" toml:"base_url"` BaseURL string `mapstructure:"base_url" toml:"base_url"`
} `mapstructure:"cos" toml:"cos"` } `mapstructure:"cos" toml:"cos"`
Random struct {
CommitMasterKey string `mapstructure:"commit_master_key" toml:"commit_master_key"`
} `mapstructure:"random" toml:"random"`
} }
var ( var (

View File

@ -0,0 +1,19 @@
[mysql]
[mysql.read]
addr = "127.0.0.1:3306"
user = "root"
pass = "123456"
name = "bindbox_game"
[mysql.write]
addr = "127.0.0.1:3306"
user = "root"
pass = "123456"
name = "bindbox_game"
[redis]
addr = "127.0.0.1:6379"
pass = ""
db = 0
[random]
commit_master_key = "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"

View File

@ -27,3 +27,6 @@ secret_id = "AKIDtjPtAFPNDuR1UnxvoUCoRAnJgw164Zv6"
secret_key = "B0vvjMoMsKcipnJlLnFyWt6A2JRSJ0Wr" secret_key = "B0vvjMoMsKcipnJlLnFyWt6A2JRSJ0Wr"
# 可选:如有 CDN/自定义域名则填写,否则留空 # 可选:如有 CDN/自定义域名则填写,否则留空
base_url = "" base_url = ""
[random]
commit_master_key = "4d7a3b8f9c2e1a5d6b4f8c0e3a7d2b1c6f9e4a5d8c1b3f7a2e5d6c4b8f0e3a7d2b1c"

View File

@ -1,2 +1,19 @@
[mysql]
[mysql.read]
addr = "127.0.0.1:3306"
user = "root"
pass = "123456"
name = "bindbox_game"
[mysql.write]
addr = "127.0.0.1:3306"
user = "root"
pass = "123456"
name = "bindbox_game"
[redis]
addr = "127.0.0.1:6379"
pass = ""
db = 0
[random]
commit_master_key = "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"

View File

@ -1,2 +1,19 @@
[mysql]
[mysql.read]
addr = "127.0.0.1:3306"
user = "root"
pass = "123456"
name = "bindbox_game"
[mysql.write]
addr = "127.0.0.1:3306"
user = "root"
pass = "123456"
name = "bindbox_game"
[redis]
addr = "127.0.0.1:6379"
pass = ""
db = 0
[random]
commit_master_key = "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"

0
data.db Normal file
View File

View File

@ -0,0 +1,35 @@
package app
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type executeDrawResponse struct {
Receipt interface{} `json:"receipt"`
}
func (h *handler) ExecuteDraw() 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)
rec, err := h.activity.ExecuteDraw(ctx.RequestContext(), issueID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ExecuteDrawError, err.Error()))
return
}
ctx.Payload(&executeDrawResponse{Receipt: rec})
}
}

View File

@ -0,0 +1,152 @@
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})
}
}

View File

@ -0,0 +1,82 @@
package admin
import (
"net/http"
"strconv"
"time"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/service/user"
)
type batchUsersRequest struct {
Count int `json:"count" binding:"min=1,max=1000"`
}
type batchUsersResponse struct {
Users []batchUserItem `json:"users"`
}
type batchUserItem struct {
ID int64 `json:"id"`
Nickname string `json:"nickname"`
OpenID string `json:"open_id"`
Avatar string `json:"avatar"`
}
func (h *handler) BatchCreateUsers() core.HandlerFunc {
return func(ctx core.Context) {
var req batchUsersRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
users := make([]batchUserItem, 0, req.Count)
for i := 0; i < req.Count; i++ {
nickname := "用户" + strconv.Itoa(i+1) + "_" + strconv.FormatInt(time.Now().UnixNano()%10000, 10)
openID := "batch_" + strconv.FormatInt(time.Now().UnixNano(), 10)
avatar := "https://trae-api-sg.mchost.guru/api/ide/v1/text_to_image?prompt=avatar&image_size=square"
u, err := h.user.CreateUser(ctx.RequestContext(), user.CreateUserInput{
Nickname: nickname,
OpenID: openID,
Avatar: avatar,
})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateUserError, err.Error()))
return
}
users = append(users, batchUserItem{
ID: u.ID,
Nickname: u.Nickname,
OpenID: u.Openid,
Avatar: u.Avatar,
})
}
ctx.Payload(&batchUsersResponse{Users: users})
}
}
func (h *handler) BatchDeleteUsers() core.HandlerFunc {
return func(ctx core.Context) {
var req struct {
UserIDs []int64 `json:"user_ids" binding:"required,min=1,max=1000,dive,min=1"`
}
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.DeleteUserError, "禁止操作"))
return
}
for _, uid := range req.UserIDs {
if err := h.user.DeleteUser(ctx.RequestContext(), uid); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.DeleteUserError, err.Error()))
return
}
}
ctx.Payload(&simpleMessageResponse{Message: "操作成功"})
}
}

View File

@ -0,0 +1,121 @@
package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
)
type getDrawReceiptResponse struct {
ID int64 `json:"id"`
DrawLogID int64 `json:"draw_log_id"`
AlgoVersion string `json:"algo_version"`
RoundID int64 `json:"round_id"`
DrawID int64 `json:"draw_id"`
ClientID int64 `json:"client_id"`
Timestamp int64 `json:"timestamp"`
ServerSeedHash string `json:"server_seed_hash"`
ServerSubSeed string `json:"server_sub_seed"`
ClientSeed string `json:"client_seed"`
Nonce int64 `json:"nonce"`
ItemsRoot string `json:"items_root"`
WeightsTotal int64 `json:"weights_total"`
SelectedIndex int32 `json:"selected_index"`
RandProof string `json:"rand_proof"`
Signature string `json:"signature"`
ItemsSnapshot string `json:"items_snapshot"`
}
func (h *handler) GetDrawReceipt() core.HandlerFunc {
return func(ctx core.Context) {
drawIDStr := ctx.Param("draw_id")
if drawIDStr == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递抽奖ID"))
return
}
drawID, err := strconv.ParseInt(drawIDStr, 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "抽奖ID格式错误"))
return
}
// 查询抽奖收据
receipt, err := h.readDB.ActivityDrawReceipts.WithContext(ctx.RequestContext()).
Where(h.readDB.ActivityDrawReceipts.DrawID.Eq(drawID)).
First()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusNotFound, code.GetDrawReceiptError, "未找到抽奖收据"))
return
}
ctx.Payload(&getDrawReceiptResponse{
ID: receipt.ID,
DrawLogID: receipt.DrawLogID,
AlgoVersion: receipt.AlgoVersion,
RoundID: receipt.RoundID,
DrawID: receipt.DrawID,
ClientID: receipt.ClientID,
Timestamp: receipt.Timestamp,
ServerSeedHash: receipt.ServerSeedHash,
ServerSubSeed: receipt.ServerSubSeed,
ClientSeed: receipt.ClientSeed,
Nonce: receipt.Nonce,
ItemsRoot: receipt.ItemsRoot,
WeightsTotal: receipt.WeightsTotal,
SelectedIndex: receipt.SelectedIndex,
RandProof: receipt.RandProof,
Signature: receipt.Signature,
ItemsSnapshot: receipt.ItemsSnapshot,
})
}
}
func (h *handler) GetDrawReceiptByLogID() core.HandlerFunc {
return func(ctx core.Context) {
logIDStr := ctx.Param("log_id")
if logIDStr == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递日志ID"))
return
}
logID, err := strconv.ParseInt(logIDStr, 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "日志ID格式错误"))
return
}
// 查询抽奖收据
receipt, err := h.readDB.ActivityDrawReceipts.WithContext(ctx.RequestContext()).
Where(h.readDB.ActivityDrawReceipts.DrawLogID.Eq(logID)).
First()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusNotFound, code.GetDrawReceiptError, "未找到抽奖收据"))
return
}
ctx.Payload(&getDrawReceiptResponse{
ID: receipt.ID,
DrawLogID: receipt.DrawLogID,
AlgoVersion: receipt.AlgoVersion,
RoundID: receipt.RoundID,
DrawID: receipt.DrawID,
ClientID: receipt.ClientID,
Timestamp: receipt.Timestamp,
ServerSeedHash: receipt.ServerSeedHash,
ServerSubSeed: receipt.ServerSubSeed,
ClientSeed: receipt.ClientSeed,
Nonce: receipt.Nonce,
ItemsRoot: receipt.ItemsRoot,
WeightsTotal: receipt.WeightsTotal,
SelectedIndex: receipt.SelectedIndex,
RandProof: receipt.RandProof,
Signature: receipt.Signature,
ItemsSnapshot: receipt.ItemsSnapshot,
})
}
}

View File

@ -0,0 +1,131 @@
package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type simulateDrawRequest struct {
Samples int `json:"samples"`
}
type simulateItemStat struct {
RewardID int64 `json:"reward_id"`
Name string `json:"name"`
Weight int32 `json:"weight"`
Quantity int64 `json:"quantity"`
ExpectedRate float64 `json:"expected_rate"`
Count int64 `json:"count"`
ObservedRate float64 `json:"observed_rate"`
}
type simulateDrawResponse struct {
AlgoVersion string `json:"algo_version"`
IssueID int64 `json:"issue_id"`
ServerSeedHash string `json:"server_seed_hash"`
ItemsRoot string `json:"items_root"`
WeightsTotal int64 `json:"weights_total"`
Samples int `json:"samples"`
Stats []simulateItemStat `json:"stats"`
}
func (h *handler) SimulateIssueDraw() 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 simulateDrawRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if req.Samples <= 0 {
req.Samples = 1000
}
rewards, err := h.activity.ListIssueRewards(ctx.RequestContext(), issueID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ExecuteDrawError, err.Error()))
return
}
cm, err := h.activity.GetIssueRandomCommit(ctx.RequestContext(), 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
}
counts := make(map[int64]int64)
var eligibleTotal int64
for _, it := range rewards {
if it.Weight > 0 && (it.Quantity == -1 || it.Quantity > 0) {
eligibleTotal += int64(it.Weight)
}
}
for i := 0; i < req.Samples; i++ {
rec, err := h.activity.ExecuteDraw(ctx.RequestContext(), issueID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ExecuteDrawError, err.Error()))
return
}
counts[rec.SelectedItemId]++
}
stats := make([]simulateItemStat, 0, len(rewards))
for _, it := range rewards {
var expected float64
if eligibleTotal > 0 && it.Weight > 0 && (it.Quantity == -1 || it.Quantity > 0) {
expected = float64(it.Weight) / float64(eligibleTotal)
}
c := counts[it.ID]
var observed float64
if req.Samples > 0 {
observed = float64(c) / float64(req.Samples)
}
stats = append(stats, simulateItemStat{
RewardID: it.ID,
Name: it.Name,
Weight: it.Weight,
Quantity: it.Quantity,
ExpectedRate: expected,
Count: c,
ObservedRate: observed,
})
}
ctx.Payload(&simulateDrawResponse{
AlgoVersion: cm.AlgoVersion,
IssueID: cm.IssueID,
ServerSeedHash: hexStr2(cm.ServerSeedHash[:]),
ItemsRoot: hexStr2(cm.ItemsRoot[:]),
WeightsTotal: cm.WeightsTotal,
Samples: req.Samples,
Stats: stats,
})
}
}
func hexStr2(b []byte) string {
const hextable = "0123456789abcdef"
dst := make([]byte, len(b)*2)
j := 0
for _, v := range b {
dst[j] = hextable[v>>4]
dst[j+1] = hextable[v&0x0f]
j += 2
}
return string(dst)
}

View File

@ -0,0 +1,88 @@
package admin
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"bindbox-game/configs"
)
func unmaskSeed(enc []byte, issueID int64, version int32) []byte {
key := masterKey()
if key == nil {
return enc
}
ks := deriveMask(key, issueID, version)
out := make([]byte, len(enc))
for i := range enc {
out[i] = enc[i] ^ ks[i%len(ks)]
}
return out
}
func masterKey() []byte {
s := configs.Get().Random.CommitMasterKey
if s == "" {
return nil
}
b, err := hex.DecodeString(s)
if err != nil || len(b) == 0 {
return nil
}
return b
}
func deriveMask(key []byte, issueID int64, version int32) []byte {
m := hmac.New(sha256.New, key)
buf := make([]byte, 12)
binary.BigEndian.PutUint64(buf[:8], uint64(issueID))
binary.BigEndian.PutUint32(buf[8:12], uint32(version))
m.Write(buf)
sum := m.Sum(nil)
return sum[:32]
}
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
}
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
}
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
}
func appendUint64(b []byte, v uint64) []byte {
nb := make([]byte, 8)
binary.BigEndian.PutUint64(nb, v)
b = append(b, nb...)
return b
}
func hmacSha256(key []byte, msg []byte) []byte {
m := hmac.New(sha256.New, key)
m.Write(msg)
return m.Sum(nil)
}

View File

@ -0,0 +1,124 @@
package admin
import (
"net/http"
"strconv"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
)
type issueCommitResponse struct {
AlgoVersion string `json:"algo_version"`
IssueID int64 `json:"issue_id"`
ServerSeedHash string `json:"server_seed_hash"`
ItemsRoot string `json:"items_root"`
WeightsTotal int64 `json:"weights_total"`
StateVersion int32 `json:"state_version"`
}
func (h *handler) CommitIssueRandom() 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)
cm, err := h.activity.CommitIssueRandom(ctx.RequestContext(), issueID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CommitIssueRandomError, err.Error()))
return
}
ctx.Payload(&issueCommitResponse{
AlgoVersion: cm.AlgoVersion,
IssueID: cm.IssueID,
ServerSeedHash: hexStr(cm.ServerSeedHash[:]),
ItemsRoot: hexStr(cm.ItemsRoot[:]),
WeightsTotal: cm.WeightsTotal,
})
}
}
func (h *handler) GetIssueRandomCommit() 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)
cm, err := h.activity.GetIssueRandomCommit(ctx.RequestContext(), 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
}
ctx.Payload(&issueCommitResponse{
AlgoVersion: cm.AlgoVersion,
IssueID: cm.IssueID,
ServerSeedHash: hexStr(cm.ServerSeedHash[:]),
ItemsRoot: hexStr(cm.ItemsRoot[:]),
WeightsTotal: cm.WeightsTotal,
StateVersion: cm.StateVersion,
})
}
}
func (h *handler) GetIssueRandomCommitHistory() 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)
history, err := h.activity.GetIssueRandomCommitHistory(ctx.RequestContext(), issueID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.GetIssueRandomCommitError, err.Error()))
return
}
// 转换响应格式
response := make([]*issueCommitResponse, 0, len(history))
for _, commit := range history {
response = append(response, &issueCommitResponse{
AlgoVersion: commit.AlgoVersion,
IssueID: commit.IssueID,
ServerSeedHash: hexStr(commit.ServerSeedHash[:]),
ItemsRoot: hexStr(commit.ItemsRoot[:]),
WeightsTotal: commit.WeightsTotal,
StateVersion: commit.StateVersion,
})
}
ctx.Payload(response)
}
}
func hexStr(b []byte) string {
const hextable = "0123456789abcdef"
dst := make([]byte, len(b)*2)
j := 0
for _, v := range b {
dst[j] = hextable[v>>4]
dst[j+1] = hextable[v&0x0f]
j += 2
}
return string(dst)
}

View File

@ -0,0 +1,140 @@
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"`
}
func (h *handler) VerifyDrawReceipt() core.HandlerFunc {
return func(ctx core.Context) {
var req 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: "验证通过"})
}
}

View File

@ -15,11 +15,11 @@ type Failure struct {
Message string `json:"message"` // 描述信息 Message string `json:"message"` // 描述信息
} }
const ( const (
ServerError = 10101 ServerError = 10101
ParamBindError = 10102 ParamBindError = 10102
JWTAuthVerifyError = 10103 JWTAuthVerifyError = 10103
UploadError = 10104 UploadError = 10104
AdminLoginError = 20101 AdminLoginError = 20101
CreateAppError = 20201 CreateAppError = 20201
@ -69,8 +69,14 @@ const (
DeleteActivityIssueError = 20509 DeleteActivityIssueError = 20509
CreateIssueRewardsError = 20510 CreateIssueRewardsError = 20510
ListIssueRewardsError = 20511 ListIssueRewardsError = 20511
ListDrawLogsError = 20512 ListDrawLogsError = 20512
) ExecuteDrawError = 20513
CommitIssueRandomError = 20514
GetIssueRandomCommitError= 20515
CreateUserError = 20516
DeleteUserError = 20517
GetDrawReceiptError = 20518
)
const ( const (
CreateGuildError = 20601 CreateGuildError = 20601

View File

@ -41,8 +41,12 @@ var zhCNText = map[int]string{
ModifyActivityIssueError: "修改活动期数失败", ModifyActivityIssueError: "修改活动期数失败",
DeleteActivityIssueError: "删除活动期数失败", DeleteActivityIssueError: "删除活动期数失败",
CreateIssueRewardsError: "创建期数奖品失败", CreateIssueRewardsError: "创建期数奖品失败",
ListIssueRewardsError: "获取期数奖品失败", ListIssueRewardsError: "获取期数奖品失败",
ListDrawLogsError: "获取抽奖记录失败", ListDrawLogsError: "获取抽奖记录失败",
ExecuteDrawError: "执行抽奖失败",
CommitIssueRandomError: "生成期随机承诺失败",
GetIssueRandomCommitError:"获取期随机承诺失败",
GetDrawReceiptError: "获取抽奖收据失败",
CreateGuildError: "创建工会失败", CreateGuildError: "创建工会失败",
ModifyGuildError: "修改工会失败", ModifyGuildError: "修改工会失败",

View File

@ -0,0 +1,388 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package dao
import (
"context"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"bindbox-game/internal/repository/mysql/model"
)
func newActivityDrawReceipts(db *gorm.DB, opts ...gen.DOOption) activityDrawReceipts {
_activityDrawReceipts := activityDrawReceipts{}
_activityDrawReceipts.activityDrawReceiptsDo.UseDB(db, opts...)
_activityDrawReceipts.activityDrawReceiptsDo.UseModel(&model.ActivityDrawReceipts{})
tableName := _activityDrawReceipts.activityDrawReceiptsDo.TableName()
_activityDrawReceipts.ALL = field.NewAsterisk(tableName)
_activityDrawReceipts.ID = field.NewInt64(tableName, "id")
_activityDrawReceipts.CreatedAt = field.NewTime(tableName, "created_at")
_activityDrawReceipts.DrawLogID = field.NewInt64(tableName, "draw_log_id")
_activityDrawReceipts.AlgoVersion = field.NewString(tableName, "algo_version")
_activityDrawReceipts.RoundID = field.NewInt64(tableName, "round_id")
_activityDrawReceipts.DrawID = field.NewInt64(tableName, "draw_id")
_activityDrawReceipts.ClientID = field.NewInt64(tableName, "client_id")
_activityDrawReceipts.Timestamp = field.NewInt64(tableName, "timestamp")
_activityDrawReceipts.ServerSeedHash = field.NewString(tableName, "server_seed_hash")
_activityDrawReceipts.ServerSubSeed = field.NewString(tableName, "server_sub_seed")
_activityDrawReceipts.ClientSeed = field.NewString(tableName, "client_seed")
_activityDrawReceipts.Nonce = field.NewInt64(tableName, "nonce")
_activityDrawReceipts.ItemsRoot = field.NewString(tableName, "items_root")
_activityDrawReceipts.WeightsTotal = field.NewInt64(tableName, "weights_total")
_activityDrawReceipts.SelectedIndex = field.NewInt32(tableName, "selected_index")
_activityDrawReceipts.RandProof = field.NewString(tableName, "rand_proof")
_activityDrawReceipts.Signature = field.NewString(tableName, "signature")
_activityDrawReceipts.ItemsSnapshot = field.NewString(tableName, "items_snapshot")
_activityDrawReceipts.fillFieldMap()
return _activityDrawReceipts
}
// activityDrawReceipts 活动抽奖凭据表
type activityDrawReceipts struct {
activityDrawReceiptsDo
ALL field.Asterisk
ID field.Int64 // 主键ID
CreatedAt field.Time // 创建时间
DrawLogID field.Int64 // 抽奖日志IDactivity_draw_logs.id
AlgoVersion field.String // 算法版本
RoundID field.Int64 // 期ID
DrawID field.Int64 // 抽奖ID
ClientID field.Int64 // 客户端ID
Timestamp field.Int64 // 时间戳(毫秒)
ServerSeedHash field.String // 服务器种子哈希(十六进制)
ServerSubSeed field.String // 服务器子种子(十六进制)
ClientSeed field.String // 客户端种子(十六进制)
Nonce field.Int64 // 随机数
ItemsRoot field.String // 项目根哈希(十六进制)
WeightsTotal field.Int64 // 权重总和
SelectedIndex field.Int32 // 选中索引
RandProof field.String // 随机证明(十六进制)
Signature field.String // 签名(十六进制,可选)
ItemsSnapshot field.String // 项目快照JSON格式
fieldMap map[string]field.Expr
}
func (a activityDrawReceipts) Table(newTableName string) *activityDrawReceipts {
a.activityDrawReceiptsDo.UseTable(newTableName)
return a.updateTableName(newTableName)
}
func (a activityDrawReceipts) As(alias string) *activityDrawReceipts {
a.activityDrawReceiptsDo.DO = *(a.activityDrawReceiptsDo.As(alias).(*gen.DO))
return a.updateTableName(alias)
}
func (a *activityDrawReceipts) updateTableName(table string) *activityDrawReceipts {
a.ALL = field.NewAsterisk(table)
a.ID = field.NewInt64(table, "id")
a.CreatedAt = field.NewTime(table, "created_at")
a.DrawLogID = field.NewInt64(table, "draw_log_id")
a.AlgoVersion = field.NewString(table, "algo_version")
a.RoundID = field.NewInt64(table, "round_id")
a.DrawID = field.NewInt64(table, "draw_id")
a.ClientID = field.NewInt64(table, "client_id")
a.Timestamp = field.NewInt64(table, "timestamp")
a.ServerSeedHash = field.NewString(table, "server_seed_hash")
a.ServerSubSeed = field.NewString(table, "server_sub_seed")
a.ClientSeed = field.NewString(table, "client_seed")
a.Nonce = field.NewInt64(table, "nonce")
a.ItemsRoot = field.NewString(table, "items_root")
a.WeightsTotal = field.NewInt64(table, "weights_total")
a.SelectedIndex = field.NewInt32(table, "selected_index")
a.RandProof = field.NewString(table, "rand_proof")
a.Signature = field.NewString(table, "signature")
a.ItemsSnapshot = field.NewString(table, "items_snapshot")
a.fillFieldMap()
return a
}
func (a *activityDrawReceipts) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := a.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (a *activityDrawReceipts) fillFieldMap() {
a.fieldMap = make(map[string]field.Expr, 18)
a.fieldMap["id"] = a.ID
a.fieldMap["created_at"] = a.CreatedAt
a.fieldMap["draw_log_id"] = a.DrawLogID
a.fieldMap["algo_version"] = a.AlgoVersion
a.fieldMap["round_id"] = a.RoundID
a.fieldMap["draw_id"] = a.DrawID
a.fieldMap["client_id"] = a.ClientID
a.fieldMap["timestamp"] = a.Timestamp
a.fieldMap["server_seed_hash"] = a.ServerSeedHash
a.fieldMap["server_sub_seed"] = a.ServerSubSeed
a.fieldMap["client_seed"] = a.ClientSeed
a.fieldMap["nonce"] = a.Nonce
a.fieldMap["items_root"] = a.ItemsRoot
a.fieldMap["weights_total"] = a.WeightsTotal
a.fieldMap["selected_index"] = a.SelectedIndex
a.fieldMap["rand_proof"] = a.RandProof
a.fieldMap["signature"] = a.Signature
a.fieldMap["items_snapshot"] = a.ItemsSnapshot
}
func (a activityDrawReceipts) clone(db *gorm.DB) activityDrawReceipts {
a.activityDrawReceiptsDo.ReplaceConnPool(db.Statement.ConnPool)
return a
}
func (a activityDrawReceipts) replaceDB(db *gorm.DB) activityDrawReceipts {
a.activityDrawReceiptsDo.ReplaceDB(db)
return a
}
type activityDrawReceiptsDo struct{ gen.DO }
func (a activityDrawReceiptsDo) Debug() *activityDrawReceiptsDo {
return a.withDO(a.DO.Debug())
}
func (a activityDrawReceiptsDo) WithContext(ctx context.Context) *activityDrawReceiptsDo {
return a.withDO(a.DO.WithContext(ctx))
}
func (a activityDrawReceiptsDo) ReadDB() *activityDrawReceiptsDo {
return a.Clauses(dbresolver.Read)
}
func (a activityDrawReceiptsDo) WriteDB() *activityDrawReceiptsDo {
return a.Clauses(dbresolver.Write)
}
func (a activityDrawReceiptsDo) Session(config *gorm.Session) *activityDrawReceiptsDo {
return a.withDO(a.DO.Session(config))
}
func (a activityDrawReceiptsDo) Clauses(conds ...clause.Expression) *activityDrawReceiptsDo {
return a.withDO(a.DO.Clauses(conds...))
}
func (a activityDrawReceiptsDo) Returning(value interface{}, columns ...string) *activityDrawReceiptsDo {
return a.withDO(a.DO.Returning(value, columns...))
}
func (a activityDrawReceiptsDo) Not(conds ...gen.Condition) *activityDrawReceiptsDo {
return a.withDO(a.DO.Not(conds...))
}
func (a activityDrawReceiptsDo) Or(conds ...gen.Condition) *activityDrawReceiptsDo {
return a.withDO(a.DO.Or(conds...))
}
func (a activityDrawReceiptsDo) Select(conds ...field.Expr) *activityDrawReceiptsDo {
return a.withDO(a.DO.Select(conds...))
}
func (a activityDrawReceiptsDo) Where(conds ...gen.Condition) *activityDrawReceiptsDo {
return a.withDO(a.DO.Where(conds...))
}
func (a activityDrawReceiptsDo) Order(conds ...field.Expr) *activityDrawReceiptsDo {
return a.withDO(a.DO.Order(conds...))
}
func (a activityDrawReceiptsDo) Distinct(cols ...field.Expr) *activityDrawReceiptsDo {
return a.withDO(a.DO.Distinct(cols...))
}
func (a activityDrawReceiptsDo) Omit(cols ...field.Expr) *activityDrawReceiptsDo {
return a.withDO(a.DO.Omit(cols...))
}
func (a activityDrawReceiptsDo) Join(table schema.Tabler, on ...field.Expr) *activityDrawReceiptsDo {
return a.withDO(a.DO.Join(table, on...))
}
func (a activityDrawReceiptsDo) LeftJoin(table schema.Tabler, on ...field.Expr) *activityDrawReceiptsDo {
return a.withDO(a.DO.LeftJoin(table, on...))
}
func (a activityDrawReceiptsDo) RightJoin(table schema.Tabler, on ...field.Expr) *activityDrawReceiptsDo {
return a.withDO(a.DO.RightJoin(table, on...))
}
func (a activityDrawReceiptsDo) Group(cols ...field.Expr) *activityDrawReceiptsDo {
return a.withDO(a.DO.Group(cols...))
}
func (a activityDrawReceiptsDo) Having(conds ...gen.Condition) *activityDrawReceiptsDo {
return a.withDO(a.DO.Having(conds...))
}
func (a activityDrawReceiptsDo) Limit(limit int) *activityDrawReceiptsDo {
return a.withDO(a.DO.Limit(limit))
}
func (a activityDrawReceiptsDo) Offset(offset int) *activityDrawReceiptsDo {
return a.withDO(a.DO.Offset(offset))
}
func (a activityDrawReceiptsDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *activityDrawReceiptsDo {
return a.withDO(a.DO.Scopes(funcs...))
}
func (a activityDrawReceiptsDo) Unscoped() *activityDrawReceiptsDo {
return a.withDO(a.DO.Unscoped())
}
func (a activityDrawReceiptsDo) Create(values ...*model.ActivityDrawReceipts) error {
if len(values) == 0 {
return nil
}
return a.DO.Create(values)
}
func (a activityDrawReceiptsDo) CreateInBatches(values []*model.ActivityDrawReceipts, batchSize int) error {
return a.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (a activityDrawReceiptsDo) Save(values ...*model.ActivityDrawReceipts) error {
if len(values) == 0 {
return nil
}
return a.DO.Save(values)
}
func (a activityDrawReceiptsDo) First() (*model.ActivityDrawReceipts, error) {
if result, err := a.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.ActivityDrawReceipts), nil
}
}
func (a activityDrawReceiptsDo) Take() (*model.ActivityDrawReceipts, error) {
if result, err := a.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.ActivityDrawReceipts), nil
}
}
func (a activityDrawReceiptsDo) Last() (*model.ActivityDrawReceipts, error) {
if result, err := a.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.ActivityDrawReceipts), nil
}
}
func (a activityDrawReceiptsDo) Find() ([]*model.ActivityDrawReceipts, error) {
result, err := a.DO.Find()
return result.([]*model.ActivityDrawReceipts), err
}
func (a activityDrawReceiptsDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.ActivityDrawReceipts, err error) {
buf := make([]*model.ActivityDrawReceipts, 0, batchSize)
err = a.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (a activityDrawReceiptsDo) FindInBatches(result *[]*model.ActivityDrawReceipts, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return a.DO.FindInBatches(result, batchSize, fc)
}
func (a activityDrawReceiptsDo) Attrs(attrs ...field.AssignExpr) *activityDrawReceiptsDo {
return a.withDO(a.DO.Attrs(attrs...))
}
func (a activityDrawReceiptsDo) Assign(attrs ...field.AssignExpr) *activityDrawReceiptsDo {
return a.withDO(a.DO.Assign(attrs...))
}
func (a activityDrawReceiptsDo) Joins(fields ...field.RelationField) *activityDrawReceiptsDo {
for _, _f := range fields {
a = *a.withDO(a.DO.Joins(_f))
}
return &a
}
func (a activityDrawReceiptsDo) Preload(fields ...field.RelationField) *activityDrawReceiptsDo {
for _, _f := range fields {
a = *a.withDO(a.DO.Preload(_f))
}
return &a
}
func (a activityDrawReceiptsDo) FirstOrInit() (*model.ActivityDrawReceipts, error) {
if result, err := a.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.ActivityDrawReceipts), nil
}
}
func (a activityDrawReceiptsDo) FirstOrCreate() (*model.ActivityDrawReceipts, error) {
if result, err := a.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.ActivityDrawReceipts), nil
}
}
func (a activityDrawReceiptsDo) FindByPage(offset int, limit int) (result []*model.ActivityDrawReceipts, count int64, err error) {
result, err = a.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = a.Offset(-1).Limit(-1).Count()
return
}
func (a activityDrawReceiptsDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = a.Count()
if err != nil {
return
}
err = a.Offset(offset).Limit(limit).Scan(result)
return
}
func (a activityDrawReceiptsDo) Scan(result interface{}) (err error) {
return a.DO.Scan(result)
}
func (a activityDrawReceiptsDo) Delete(models ...*model.ActivityDrawReceipts) (result gen.ResultInfo, err error) {
return a.DO.Delete(models)
}
func (a *activityDrawReceiptsDo) withDO(do gen.Dao) *activityDrawReceiptsDo {
a.DO = *do.(*gen.DO)
return a
}

View File

@ -21,6 +21,7 @@ var (
ActivityCategories *activityCategories ActivityCategories *activityCategories
ActivityDrawEffects *activityDrawEffects ActivityDrawEffects *activityDrawEffects
ActivityDrawLogs *activityDrawLogs ActivityDrawLogs *activityDrawLogs
ActivityDrawReceipts *activityDrawReceipts
ActivityIssues *activityIssues ActivityIssues *activityIssues
ActivityRewardSettings *activityRewardSettings ActivityRewardSettings *activityRewardSettings
Admin *admin Admin *admin
@ -29,6 +30,7 @@ var (
GuildBoxes *guildBoxes GuildBoxes *guildBoxes
GuildContributeLogs *guildContributeLogs GuildContributeLogs *guildContributeLogs
GuildMembers *guildMembers GuildMembers *guildMembers
IssueRandomCommitments *issueRandomCommitments
LogOperation *logOperation LogOperation *logOperation
LogRequest *logRequest LogRequest *logRequest
MenuActions *menuActions MenuActions *menuActions
@ -61,6 +63,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
ActivityCategories = &Q.ActivityCategories ActivityCategories = &Q.ActivityCategories
ActivityDrawEffects = &Q.ActivityDrawEffects ActivityDrawEffects = &Q.ActivityDrawEffects
ActivityDrawLogs = &Q.ActivityDrawLogs ActivityDrawLogs = &Q.ActivityDrawLogs
ActivityDrawReceipts = &Q.ActivityDrawReceipts
ActivityIssues = &Q.ActivityIssues ActivityIssues = &Q.ActivityIssues
ActivityRewardSettings = &Q.ActivityRewardSettings ActivityRewardSettings = &Q.ActivityRewardSettings
Admin = &Q.Admin Admin = &Q.Admin
@ -69,6 +72,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
GuildBoxes = &Q.GuildBoxes GuildBoxes = &Q.GuildBoxes
GuildContributeLogs = &Q.GuildContributeLogs GuildContributeLogs = &Q.GuildContributeLogs
GuildMembers = &Q.GuildMembers GuildMembers = &Q.GuildMembers
IssueRandomCommitments = &Q.IssueRandomCommitments
LogOperation = &Q.LogOperation LogOperation = &Q.LogOperation
LogRequest = &Q.LogRequest LogRequest = &Q.LogRequest
MenuActions = &Q.MenuActions MenuActions = &Q.MenuActions
@ -102,6 +106,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
ActivityCategories: newActivityCategories(db, opts...), ActivityCategories: newActivityCategories(db, opts...),
ActivityDrawEffects: newActivityDrawEffects(db, opts...), ActivityDrawEffects: newActivityDrawEffects(db, opts...),
ActivityDrawLogs: newActivityDrawLogs(db, opts...), ActivityDrawLogs: newActivityDrawLogs(db, opts...),
ActivityDrawReceipts: newActivityDrawReceipts(db, opts...),
ActivityIssues: newActivityIssues(db, opts...), ActivityIssues: newActivityIssues(db, opts...),
ActivityRewardSettings: newActivityRewardSettings(db, opts...), ActivityRewardSettings: newActivityRewardSettings(db, opts...),
Admin: newAdmin(db, opts...), Admin: newAdmin(db, opts...),
@ -110,6 +115,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
GuildBoxes: newGuildBoxes(db, opts...), GuildBoxes: newGuildBoxes(db, opts...),
GuildContributeLogs: newGuildContributeLogs(db, opts...), GuildContributeLogs: newGuildContributeLogs(db, opts...),
GuildMembers: newGuildMembers(db, opts...), GuildMembers: newGuildMembers(db, opts...),
IssueRandomCommitments: newIssueRandomCommitments(db, opts...),
LogOperation: newLogOperation(db, opts...), LogOperation: newLogOperation(db, opts...),
LogRequest: newLogRequest(db, opts...), LogRequest: newLogRequest(db, opts...),
MenuActions: newMenuActions(db, opts...), MenuActions: newMenuActions(db, opts...),
@ -144,6 +150,7 @@ type Query struct {
ActivityCategories activityCategories ActivityCategories activityCategories
ActivityDrawEffects activityDrawEffects ActivityDrawEffects activityDrawEffects
ActivityDrawLogs activityDrawLogs ActivityDrawLogs activityDrawLogs
ActivityDrawReceipts activityDrawReceipts
ActivityIssues activityIssues ActivityIssues activityIssues
ActivityRewardSettings activityRewardSettings ActivityRewardSettings activityRewardSettings
Admin admin Admin admin
@ -152,6 +159,7 @@ type Query struct {
GuildBoxes guildBoxes GuildBoxes guildBoxes
GuildContributeLogs guildContributeLogs GuildContributeLogs guildContributeLogs
GuildMembers guildMembers GuildMembers guildMembers
IssueRandomCommitments issueRandomCommitments
LogOperation logOperation LogOperation logOperation
LogRequest logRequest LogRequest logRequest
MenuActions menuActions MenuActions menuActions
@ -187,6 +195,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
ActivityCategories: q.ActivityCategories.clone(db), ActivityCategories: q.ActivityCategories.clone(db),
ActivityDrawEffects: q.ActivityDrawEffects.clone(db), ActivityDrawEffects: q.ActivityDrawEffects.clone(db),
ActivityDrawLogs: q.ActivityDrawLogs.clone(db), ActivityDrawLogs: q.ActivityDrawLogs.clone(db),
ActivityDrawReceipts: q.ActivityDrawReceipts.clone(db),
ActivityIssues: q.ActivityIssues.clone(db), ActivityIssues: q.ActivityIssues.clone(db),
ActivityRewardSettings: q.ActivityRewardSettings.clone(db), ActivityRewardSettings: q.ActivityRewardSettings.clone(db),
Admin: q.Admin.clone(db), Admin: q.Admin.clone(db),
@ -195,6 +204,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
GuildBoxes: q.GuildBoxes.clone(db), GuildBoxes: q.GuildBoxes.clone(db),
GuildContributeLogs: q.GuildContributeLogs.clone(db), GuildContributeLogs: q.GuildContributeLogs.clone(db),
GuildMembers: q.GuildMembers.clone(db), GuildMembers: q.GuildMembers.clone(db),
IssueRandomCommitments: q.IssueRandomCommitments.clone(db),
LogOperation: q.LogOperation.clone(db), LogOperation: q.LogOperation.clone(db),
LogRequest: q.LogRequest.clone(db), LogRequest: q.LogRequest.clone(db),
MenuActions: q.MenuActions.clone(db), MenuActions: q.MenuActions.clone(db),
@ -237,6 +247,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
ActivityCategories: q.ActivityCategories.replaceDB(db), ActivityCategories: q.ActivityCategories.replaceDB(db),
ActivityDrawEffects: q.ActivityDrawEffects.replaceDB(db), ActivityDrawEffects: q.ActivityDrawEffects.replaceDB(db),
ActivityDrawLogs: q.ActivityDrawLogs.replaceDB(db), ActivityDrawLogs: q.ActivityDrawLogs.replaceDB(db),
ActivityDrawReceipts: q.ActivityDrawReceipts.replaceDB(db),
ActivityIssues: q.ActivityIssues.replaceDB(db), ActivityIssues: q.ActivityIssues.replaceDB(db),
ActivityRewardSettings: q.ActivityRewardSettings.replaceDB(db), ActivityRewardSettings: q.ActivityRewardSettings.replaceDB(db),
Admin: q.Admin.replaceDB(db), Admin: q.Admin.replaceDB(db),
@ -245,6 +256,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
GuildBoxes: q.GuildBoxes.replaceDB(db), GuildBoxes: q.GuildBoxes.replaceDB(db),
GuildContributeLogs: q.GuildContributeLogs.replaceDB(db), GuildContributeLogs: q.GuildContributeLogs.replaceDB(db),
GuildMembers: q.GuildMembers.replaceDB(db), GuildMembers: q.GuildMembers.replaceDB(db),
IssueRandomCommitments: q.IssueRandomCommitments.replaceDB(db),
LogOperation: q.LogOperation.replaceDB(db), LogOperation: q.LogOperation.replaceDB(db),
LogRequest: q.LogRequest.replaceDB(db), LogRequest: q.LogRequest.replaceDB(db),
MenuActions: q.MenuActions.replaceDB(db), MenuActions: q.MenuActions.replaceDB(db),
@ -277,6 +289,7 @@ type queryCtx struct {
ActivityCategories *activityCategoriesDo ActivityCategories *activityCategoriesDo
ActivityDrawEffects *activityDrawEffectsDo ActivityDrawEffects *activityDrawEffectsDo
ActivityDrawLogs *activityDrawLogsDo ActivityDrawLogs *activityDrawLogsDo
ActivityDrawReceipts *activityDrawReceiptsDo
ActivityIssues *activityIssuesDo ActivityIssues *activityIssuesDo
ActivityRewardSettings *activityRewardSettingsDo ActivityRewardSettings *activityRewardSettingsDo
Admin *adminDo Admin *adminDo
@ -285,6 +298,7 @@ type queryCtx struct {
GuildBoxes *guildBoxesDo GuildBoxes *guildBoxesDo
GuildContributeLogs *guildContributeLogsDo GuildContributeLogs *guildContributeLogsDo
GuildMembers *guildMembersDo GuildMembers *guildMembersDo
IssueRandomCommitments *issueRandomCommitmentsDo
LogOperation *logOperationDo LogOperation *logOperationDo
LogRequest *logRequestDo LogRequest *logRequestDo
MenuActions *menuActionsDo MenuActions *menuActionsDo
@ -317,6 +331,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
ActivityCategories: q.ActivityCategories.WithContext(ctx), ActivityCategories: q.ActivityCategories.WithContext(ctx),
ActivityDrawEffects: q.ActivityDrawEffects.WithContext(ctx), ActivityDrawEffects: q.ActivityDrawEffects.WithContext(ctx),
ActivityDrawLogs: q.ActivityDrawLogs.WithContext(ctx), ActivityDrawLogs: q.ActivityDrawLogs.WithContext(ctx),
ActivityDrawReceipts: q.ActivityDrawReceipts.WithContext(ctx),
ActivityIssues: q.ActivityIssues.WithContext(ctx), ActivityIssues: q.ActivityIssues.WithContext(ctx),
ActivityRewardSettings: q.ActivityRewardSettings.WithContext(ctx), ActivityRewardSettings: q.ActivityRewardSettings.WithContext(ctx),
Admin: q.Admin.WithContext(ctx), Admin: q.Admin.WithContext(ctx),
@ -325,6 +340,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
GuildBoxes: q.GuildBoxes.WithContext(ctx), GuildBoxes: q.GuildBoxes.WithContext(ctx),
GuildContributeLogs: q.GuildContributeLogs.WithContext(ctx), GuildContributeLogs: q.GuildContributeLogs.WithContext(ctx),
GuildMembers: q.GuildMembers.WithContext(ctx), GuildMembers: q.GuildMembers.WithContext(ctx),
IssueRandomCommitments: q.IssueRandomCommitments.WithContext(ctx),
LogOperation: q.LogOperation.WithContext(ctx), LogOperation: q.LogOperation.WithContext(ctx),
LogRequest: q.LogRequest.WithContext(ctx), LogRequest: q.LogRequest.WithContext(ctx),
MenuActions: q.MenuActions.WithContext(ctx), MenuActions: q.MenuActions.WithContext(ctx),

View File

@ -0,0 +1,355 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package dao
import (
"context"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"bindbox-game/internal/repository/mysql/model"
)
func newIssueRandomCommitments(db *gorm.DB, opts ...gen.DOOption) issueRandomCommitments {
_issueRandomCommitments := issueRandomCommitments{}
_issueRandomCommitments.issueRandomCommitmentsDo.UseDB(db, opts...)
_issueRandomCommitments.issueRandomCommitmentsDo.UseModel(&model.IssueRandomCommitments{})
tableName := _issueRandomCommitments.issueRandomCommitmentsDo.TableName()
_issueRandomCommitments.ALL = field.NewAsterisk(tableName)
_issueRandomCommitments.ID = field.NewInt64(tableName, "id")
_issueRandomCommitments.CreatedAt = field.NewTime(tableName, "created_at")
_issueRandomCommitments.UpdatedAt = field.NewTime(tableName, "updated_at")
_issueRandomCommitments.IssueID = field.NewInt64(tableName, "issue_id")
_issueRandomCommitments.AlgoVersion = field.NewString(tableName, "algo_version")
_issueRandomCommitments.ServerSeedMaster = field.NewBytes(tableName, "server_seed_master")
_issueRandomCommitments.ServerSeedHash = field.NewBytes(tableName, "server_seed_hash")
_issueRandomCommitments.ItemsRoot = field.NewBytes(tableName, "items_root")
_issueRandomCommitments.WeightsTotal = field.NewInt64(tableName, "weights_total")
_issueRandomCommitments.StateVersion = field.NewInt32(tableName, "state_version")
_issueRandomCommitments.fillFieldMap()
return _issueRandomCommitments
}
type issueRandomCommitments struct {
issueRandomCommitmentsDo
ALL field.Asterisk
ID field.Int64
CreatedAt field.Time
UpdatedAt field.Time
IssueID field.Int64
AlgoVersion field.String
ServerSeedMaster field.Bytes
ServerSeedHash field.Bytes
ItemsRoot field.Bytes
WeightsTotal field.Int64
StateVersion field.Int32
fieldMap map[string]field.Expr
}
func (i issueRandomCommitments) Table(newTableName string) *issueRandomCommitments {
i.issueRandomCommitmentsDo.UseTable(newTableName)
return i.updateTableName(newTableName)
}
func (i issueRandomCommitments) As(alias string) *issueRandomCommitments {
i.issueRandomCommitmentsDo.DO = *(i.issueRandomCommitmentsDo.As(alias).(*gen.DO))
return i.updateTableName(alias)
}
func (i *issueRandomCommitments) updateTableName(table string) *issueRandomCommitments {
i.ALL = field.NewAsterisk(table)
i.ID = field.NewInt64(table, "id")
i.CreatedAt = field.NewTime(table, "created_at")
i.UpdatedAt = field.NewTime(table, "updated_at")
i.IssueID = field.NewInt64(table, "issue_id")
i.AlgoVersion = field.NewString(table, "algo_version")
i.ServerSeedMaster = field.NewBytes(table, "server_seed_master")
i.ServerSeedHash = field.NewBytes(table, "server_seed_hash")
i.ItemsRoot = field.NewBytes(table, "items_root")
i.WeightsTotal = field.NewInt64(table, "weights_total")
i.StateVersion = field.NewInt32(table, "state_version")
i.fillFieldMap()
return i
}
func (i *issueRandomCommitments) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := i.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (i *issueRandomCommitments) fillFieldMap() {
i.fieldMap = make(map[string]field.Expr, 10)
i.fieldMap["id"] = i.ID
i.fieldMap["created_at"] = i.CreatedAt
i.fieldMap["updated_at"] = i.UpdatedAt
i.fieldMap["issue_id"] = i.IssueID
i.fieldMap["algo_version"] = i.AlgoVersion
i.fieldMap["server_seed_master"] = i.ServerSeedMaster
i.fieldMap["server_seed_hash"] = i.ServerSeedHash
i.fieldMap["items_root"] = i.ItemsRoot
i.fieldMap["weights_total"] = i.WeightsTotal
i.fieldMap["state_version"] = i.StateVersion
}
func (i issueRandomCommitments) clone(db *gorm.DB) issueRandomCommitments {
i.issueRandomCommitmentsDo.ReplaceConnPool(db.Statement.ConnPool)
return i
}
func (i issueRandomCommitments) replaceDB(db *gorm.DB) issueRandomCommitments {
i.issueRandomCommitmentsDo.ReplaceDB(db)
return i
}
type issueRandomCommitmentsDo struct{ gen.DO }
func (i issueRandomCommitmentsDo) Debug() *issueRandomCommitmentsDo {
return i.withDO(i.DO.Debug())
}
func (i issueRandomCommitmentsDo) WithContext(ctx context.Context) *issueRandomCommitmentsDo {
return i.withDO(i.DO.WithContext(ctx))
}
func (i issueRandomCommitmentsDo) ReadDB() *issueRandomCommitmentsDo {
return i.Clauses(dbresolver.Read)
}
func (i issueRandomCommitmentsDo) WriteDB() *issueRandomCommitmentsDo {
return i.Clauses(dbresolver.Write)
}
func (i issueRandomCommitmentsDo) Session(config *gorm.Session) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Session(config))
}
func (i issueRandomCommitmentsDo) Clauses(conds ...clause.Expression) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Clauses(conds...))
}
func (i issueRandomCommitmentsDo) Returning(value interface{}, columns ...string) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Returning(value, columns...))
}
func (i issueRandomCommitmentsDo) Not(conds ...gen.Condition) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Not(conds...))
}
func (i issueRandomCommitmentsDo) Or(conds ...gen.Condition) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Or(conds...))
}
func (i issueRandomCommitmentsDo) Select(conds ...field.Expr) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Select(conds...))
}
func (i issueRandomCommitmentsDo) Where(conds ...gen.Condition) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Where(conds...))
}
func (i issueRandomCommitmentsDo) Order(conds ...field.Expr) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Order(conds...))
}
func (i issueRandomCommitmentsDo) Distinct(cols ...field.Expr) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Distinct(cols...))
}
func (i issueRandomCommitmentsDo) Omit(cols ...field.Expr) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Omit(cols...))
}
func (i issueRandomCommitmentsDo) Join(table schema.Tabler, on ...field.Expr) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Join(table, on...))
}
func (i issueRandomCommitmentsDo) LeftJoin(table schema.Tabler, on ...field.Expr) *issueRandomCommitmentsDo {
return i.withDO(i.DO.LeftJoin(table, on...))
}
func (i issueRandomCommitmentsDo) RightJoin(table schema.Tabler, on ...field.Expr) *issueRandomCommitmentsDo {
return i.withDO(i.DO.RightJoin(table, on...))
}
func (i issueRandomCommitmentsDo) Group(cols ...field.Expr) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Group(cols...))
}
func (i issueRandomCommitmentsDo) Having(conds ...gen.Condition) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Having(conds...))
}
func (i issueRandomCommitmentsDo) Limit(limit int) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Limit(limit))
}
func (i issueRandomCommitmentsDo) Offset(offset int) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Offset(offset))
}
func (i issueRandomCommitmentsDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Scopes(funcs...))
}
func (i issueRandomCommitmentsDo) Unscoped() *issueRandomCommitmentsDo {
return i.withDO(i.DO.Unscoped())
}
func (i issueRandomCommitmentsDo) Create(values ...*model.IssueRandomCommitments) error {
if len(values) == 0 {
return nil
}
return i.DO.Create(values)
}
func (i issueRandomCommitmentsDo) CreateInBatches(values []*model.IssueRandomCommitments, batchSize int) error {
return i.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (i issueRandomCommitmentsDo) Save(values ...*model.IssueRandomCommitments) error {
if len(values) == 0 {
return nil
}
return i.DO.Save(values)
}
func (i issueRandomCommitmentsDo) First() (*model.IssueRandomCommitments, error) {
if result, err := i.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.IssueRandomCommitments), nil
}
}
func (i issueRandomCommitmentsDo) Take() (*model.IssueRandomCommitments, error) {
if result, err := i.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.IssueRandomCommitments), nil
}
}
func (i issueRandomCommitmentsDo) Last() (*model.IssueRandomCommitments, error) {
if result, err := i.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.IssueRandomCommitments), nil
}
}
func (i issueRandomCommitmentsDo) Find() ([]*model.IssueRandomCommitments, error) {
result, err := i.DO.Find()
return result.([]*model.IssueRandomCommitments), err
}
func (i issueRandomCommitmentsDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.IssueRandomCommitments, err error) {
buf := make([]*model.IssueRandomCommitments, 0, batchSize)
err = i.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (i issueRandomCommitmentsDo) FindInBatches(result *[]*model.IssueRandomCommitments, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return i.DO.FindInBatches(result, batchSize, fc)
}
func (i issueRandomCommitmentsDo) Attrs(attrs ...field.AssignExpr) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Attrs(attrs...))
}
func (i issueRandomCommitmentsDo) Assign(attrs ...field.AssignExpr) *issueRandomCommitmentsDo {
return i.withDO(i.DO.Assign(attrs...))
}
func (i issueRandomCommitmentsDo) Joins(fields ...field.RelationField) *issueRandomCommitmentsDo {
for _, _f := range fields {
i = *i.withDO(i.DO.Joins(_f))
}
return &i
}
func (i issueRandomCommitmentsDo) Preload(fields ...field.RelationField) *issueRandomCommitmentsDo {
for _, _f := range fields {
i = *i.withDO(i.DO.Preload(_f))
}
return &i
}
func (i issueRandomCommitmentsDo) FirstOrInit() (*model.IssueRandomCommitments, error) {
if result, err := i.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.IssueRandomCommitments), nil
}
}
func (i issueRandomCommitmentsDo) FirstOrCreate() (*model.IssueRandomCommitments, error) {
if result, err := i.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.IssueRandomCommitments), nil
}
}
func (i issueRandomCommitmentsDo) FindByPage(offset int, limit int) (result []*model.IssueRandomCommitments, count int64, err error) {
result, err = i.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = i.Offset(-1).Limit(-1).Count()
return
}
func (i issueRandomCommitmentsDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = i.Count()
if err != nil {
return
}
err = i.Offset(offset).Limit(limit).Scan(result)
return
}
func (i issueRandomCommitmentsDo) Scan(result interface{}) (err error) {
return i.DO.Scan(result)
}
func (i issueRandomCommitmentsDo) Delete(models ...*model.IssueRandomCommitments) (result gen.ResultInfo, err error) {
return i.DO.Delete(models)
}
func (i *issueRandomCommitmentsDo) withDO(do gen.Dao) *issueRandomCommitmentsDo {
i.DO = *do.(*gen.DO)
return i
}

View File

@ -40,6 +40,7 @@ func newSystemCoupons(db *gorm.DB, opts ...gen.DOOption) systemCoupons {
_systemCoupons.ValidStart = field.NewTime(tableName, "valid_start") _systemCoupons.ValidStart = field.NewTime(tableName, "valid_start")
_systemCoupons.ValidEnd = field.NewTime(tableName, "valid_end") _systemCoupons.ValidEnd = field.NewTime(tableName, "valid_end")
_systemCoupons.Status = field.NewInt32(tableName, "status") _systemCoupons.Status = field.NewInt32(tableName, "status")
_systemCoupons.TotalQuantity = field.NewInt64(tableName, "total_quantity")
_systemCoupons.fillFieldMap() _systemCoupons.fillFieldMap()
@ -64,6 +65,7 @@ type systemCoupons struct {
ValidStart field.Time // 有效期开始 ValidStart field.Time // 有效期开始
ValidEnd field.Time // 有效期结束 ValidEnd field.Time // 有效期结束
Status field.Int32 // 状态1启用 2停用 Status field.Int32 // 状态1启用 2停用
TotalQuantity field.Int64
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@ -93,6 +95,7 @@ func (s *systemCoupons) updateTableName(table string) *systemCoupons {
s.ValidStart = field.NewTime(table, "valid_start") s.ValidStart = field.NewTime(table, "valid_start")
s.ValidEnd = field.NewTime(table, "valid_end") s.ValidEnd = field.NewTime(table, "valid_end")
s.Status = field.NewInt32(table, "status") s.Status = field.NewInt32(table, "status")
s.TotalQuantity = field.NewInt64(table, "total_quantity")
s.fillFieldMap() s.fillFieldMap()
@ -109,7 +112,7 @@ func (s *systemCoupons) GetFieldByName(fieldName string) (field.OrderExpr, bool)
} }
func (s *systemCoupons) fillFieldMap() { func (s *systemCoupons) fillFieldMap() {
s.fieldMap = make(map[string]field.Expr, 13) s.fieldMap = make(map[string]field.Expr, 14)
s.fieldMap["id"] = s.ID s.fieldMap["id"] = s.ID
s.fieldMap["created_at"] = s.CreatedAt s.fieldMap["created_at"] = s.CreatedAt
s.fieldMap["updated_at"] = s.UpdatedAt s.fieldMap["updated_at"] = s.UpdatedAt
@ -123,6 +126,7 @@ func (s *systemCoupons) fillFieldMap() {
s.fieldMap["valid_start"] = s.ValidStart s.fieldMap["valid_start"] = s.ValidStart
s.fieldMap["valid_end"] = s.ValidEnd s.fieldMap["valid_end"] = s.ValidEnd
s.fieldMap["status"] = s.Status s.fieldMap["status"] = s.Status
s.fieldMap["total_quantity"] = s.TotalQuantity
} }
func (s systemCoupons) clone(db *gorm.DB) systemCoupons { func (s systemCoupons) clone(db *gorm.DB) systemCoupons {

View File

@ -0,0 +1,38 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameActivityDrawReceipts = "activity_draw_receipts"
// ActivityDrawReceipts 活动抽奖凭据表
type ActivityDrawReceipts struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3);comment:创建时间" json:"created_at"` // 创建时间
DrawLogID int64 `gorm:"column:draw_log_id;not null;comment:抽奖日志IDactivity_draw_logs.id" json:"draw_log_id"` // 抽奖日志IDactivity_draw_logs.id
AlgoVersion string `gorm:"column:algo_version;not null;comment:算法版本" json:"algo_version"` // 算法版本
RoundID int64 `gorm:"column:round_id;not null;comment:期ID" json:"round_id"` // 期ID
DrawID int64 `gorm:"column:draw_id;not null;comment:抽奖ID" json:"draw_id"` // 抽奖ID
ClientID int64 `gorm:"column:client_id;not null;comment:客户端ID" json:"client_id"` // 客户端ID
Timestamp int64 `gorm:"column:timestamp;not null;comment:时间戳(毫秒)" json:"timestamp"` // 时间戳(毫秒)
ServerSeedHash string `gorm:"column:server_seed_hash;not null;comment:服务器种子哈希(十六进制)" json:"server_seed_hash"` // 服务器种子哈希(十六进制)
ServerSubSeed string `gorm:"column:server_sub_seed;not null;comment:服务器子种子(十六进制)" json:"server_sub_seed"` // 服务器子种子(十六进制)
ClientSeed string `gorm:"column:client_seed;not null;comment:客户端种子(十六进制)" json:"client_seed"` // 客户端种子(十六进制)
Nonce int64 `gorm:"column:nonce;not null;comment:随机数" json:"nonce"` // 随机数
ItemsRoot string `gorm:"column:items_root;not null;comment:项目根哈希(十六进制)" json:"items_root"` // 项目根哈希(十六进制)
WeightsTotal int64 `gorm:"column:weights_total;not null;comment:权重总和" json:"weights_total"` // 权重总和
SelectedIndex int32 `gorm:"column:selected_index;not null;comment:选中索引" json:"selected_index"` // 选中索引
RandProof string `gorm:"column:rand_proof;not null;comment:随机证明(十六进制)" json:"rand_proof"` // 随机证明(十六进制)
Signature string `gorm:"column:signature;comment:签名(十六进制,可选)" json:"signature"` // 签名(十六进制,可选)
ItemsSnapshot string `gorm:"column:items_snapshot;comment:项目快照JSON格式" json:"items_snapshot"` // 项目快照JSON格式
}
// TableName ActivityDrawReceipts's table name
func (*ActivityDrawReceipts) TableName() string {
return TableNameActivityDrawReceipts
}

View File

@ -0,0 +1,30 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameIssueRandomCommitments = "issue_random_commitments"
// IssueRandomCommitments mapped from table <issue_random_commitments>
type IssueRandomCommitments struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP(3)" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP(3)" json:"updated_at"`
IssueID int64 `gorm:"column:issue_id;not null" json:"issue_id"`
AlgoVersion string `gorm:"column:algo_version;not null" json:"algo_version"`
ServerSeedMaster []byte `gorm:"column:server_seed_master;not null" json:"server_seed_master"`
ServerSeedHash []byte `gorm:"column:server_seed_hash;not null" json:"server_seed_hash"`
ItemsRoot []byte `gorm:"column:items_root;not null" json:"items_root"`
WeightsTotal int64 `gorm:"column:weights_total;not null" json:"weights_total"`
StateVersion int32 `gorm:"column:state_version;not null" json:"state_version"`
}
// TableName IssueRandomCommitments's table name
func (*IssueRandomCommitments) TableName() string {
return TableNameIssueRandomCommitments
}

View File

@ -25,6 +25,7 @@ type SystemCoupons struct {
ValidStart time.Time `gorm:"column:valid_start;comment:有效期开始" json:"valid_start"` // 有效期开始 ValidStart time.Time `gorm:"column:valid_start;comment:有效期开始" json:"valid_start"` // 有效期开始
ValidEnd time.Time `gorm:"column:valid_end;comment:有效期结束" json:"valid_end"` // 有效期结束 ValidEnd time.Time `gorm:"column:valid_end;comment:有效期结束" json:"valid_end"` // 有效期结束
Status int32 `gorm:"column:status;not null;default:1;comment:状态1启用 2停用" json:"status"` // 状态1启用 2停用 Status int32 `gorm:"column:status;not null;default:1;comment:状态1启用 2停用" json:"status"` // 状态1启用 2停用
TotalQuantity int64 `gorm:"column:total_quantity" json:"total_quantity"`
} }
// TableName SystemCoupons's table name // TableName SystemCoupons's table name

View File

@ -82,6 +82,17 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) {
adminAuthApiRouter.PUT("/activities/:activity_id/issues/:issue_id/rewards/:reward_id", adminHandler.ModifyIssueReward()) adminAuthApiRouter.PUT("/activities/:activity_id/issues/:issue_id/rewards/:reward_id", adminHandler.ModifyIssueReward())
adminAuthApiRouter.DELETE("/activities/:activity_id/issues/:issue_id/rewards/:reward_id", adminHandler.DeleteIssueReward()) adminAuthApiRouter.DELETE("/activities/:activity_id/issues/:issue_id/rewards/:reward_id", adminHandler.DeleteIssueReward())
adminAuthApiRouter.POST("/activities/:activity_id/issues/:issue_id/commit_random", adminHandler.CommitIssueRandom())
adminAuthApiRouter.GET("/activities/:activity_id/issues/:issue_id/commit_random", adminHandler.GetIssueRandomCommit())
adminAuthApiRouter.GET("/activities/:activity_id/issues/:issue_id/commit_random/history", adminHandler.GetIssueRandomCommitHistory())
adminAuthApiRouter.POST("/activities/:activity_id/issues/:issue_id/simulate_draw", adminHandler.SimulateIssueDraw())
adminAuthApiRouter.POST("/activities/:activity_id/issues/:issue_id/batch_draw", adminHandler.BatchDrawForUsers())
adminAuthApiRouter.POST("/activities/:activity_id/issues/:issue_id/verify_draw", adminHandler.VerifyDrawReceipt())
adminAuthApiRouter.GET("/draw_receipts/:draw_id", adminHandler.GetDrawReceipt())
adminAuthApiRouter.GET("/draw_receipts/log/:log_id", adminHandler.GetDrawReceiptByLogID())
adminAuthApiRouter.POST("/batch_users", adminHandler.BatchCreateUsers())
adminAuthApiRouter.DELETE("/batch_users", adminHandler.BatchDeleteUsers())
adminAuthApiRouter.POST("/guilds", adminHandler.CreateGuild()) adminAuthApiRouter.POST("/guilds", adminHandler.CreateGuild())
adminAuthApiRouter.PUT("/guilds/:guild_id", adminHandler.ModifyGuild()) adminAuthApiRouter.PUT("/guilds/:guild_id", adminHandler.ModifyGuild())
adminAuthApiRouter.DELETE("/guilds/:guild_id", adminHandler.DeleteGuild()) adminAuthApiRouter.DELETE("/guilds/:guild_id", adminHandler.DeleteGuild())
@ -192,6 +203,8 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, error) {
appAuthApiRouter.GET("/users/:user_id/addresses", userHandler.ListUserAddresses()) appAuthApiRouter.GET("/users/:user_id/addresses", userHandler.ListUserAddresses())
appAuthApiRouter.PUT("/users/:user_id/addresses/:address_id/default", userHandler.SetDefaultUserAddress()) appAuthApiRouter.PUT("/users/:user_id/addresses/:address_id/default", userHandler.SetDefaultUserAddress())
appAuthApiRouter.DELETE("/users/:user_id/addresses/:address_id", userHandler.DeleteUserAddress()) appAuthApiRouter.DELETE("/users/:user_id/addresses/:address_id", userHandler.DeleteUserAddress())
appAuthApiRouter.POST("/activities/:activity_id/issues/:issue_id/draw", activityHandler.ExecuteDraw())
} }
return mux, nil return mux, nil

View File

@ -29,9 +29,51 @@ type Service interface {
ListDrawLogs(ctx context.Context, issueID int64, page, pageSize int) (items []*model.ActivityDrawLogs, total int64, err error) ListDrawLogs(ctx context.Context, issueID int64, page, pageSize int) (items []*model.ActivityDrawLogs, total int64, err error)
CommitIssueRandom(ctx context.Context, issueID int64) (*IssueRandomCommitment, error)
GetIssueRandomCommit(ctx context.Context, issueID int64) (*IssueRandomCommitment, error)
GetIssueRandomCommitHistory(ctx context.Context, issueID int64) ([]*IssueRandomCommitment, error)
ExecuteDraw(ctx context.Context, issueID int64) (*Receipt, error)
GetCategoryNames(ctx context.Context, ids []int64) (map[int64]string, error) GetCategoryNames(ctx context.Context, ids []int64) (map[int64]string, error)
} }
type IssueRandomCommitment struct {
AlgoVersion string
IssueID int64
ServerSeedMaster []byte
ServerSeedHash []byte
ItemsRoot []byte
WeightsTotal int64
StateVersion int32
}
type Receipt struct {
AlgoVersion string
RoundId int64
DrawId int64
ClientId int64
Timestamp int64
ServerSeedHash []byte
ServerSubSeed []byte
ClientSeed []byte
Nonce uint64
Items []ReceiptItem
ItemsRoot []byte
WeightsTotal uint64
SelectedIndex int
SelectedItemId int64
RandProof []byte
Signature []byte
}
type ReceiptItem struct {
ID int64
Name string
Weight int32
QuantityBefore int64
}
type service struct { type service struct {
logger logger.CustomLogger logger logger.CustomLogger
readDB *dao.Query readDB *dao.Query

View File

@ -0,0 +1,145 @@
package activity
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"time"
)
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
}
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
}
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
}
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
}
func appendUint64(b []byte, v uint64) []byte {
nb := make([]byte, 8)
binary.BigEndian.PutUint64(nb, v)
b = append(b, nb...)
return b
}
func hmacSha256(key []byte, msg []byte) []byte {
m := hmac.New(sha256.New, key)
m.Write(msg)
return m.Sum(nil)
}
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)
}
}

View File

@ -0,0 +1,146 @@
package activity
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"errors"
"sort"
"bindbox-game/internal/repository/mysql/model"
"gorm.io/gorm"
)
var algoVersion = "v1-hmac-256"
func (s *service) CommitIssueRandom(ctx context.Context, issueID int64) (*IssueRandomCommitment, error) {
// 检查活动期数状态
issue, err := s.readDB.ActivityIssues.WithContext(ctx).
Where(s.readDB.ActivityIssues.ID.Eq(issueID)).
First()
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New("活动期数不存在")
}
return nil, err
}
// 只允许在未开始状态(3)生成承诺
if issue.Status != 3 {
return nil, errors.New("只能在期数未开始状态下生成随机承诺")
}
// 检查是否已存在承诺(防止重复生成)
existing, _ := s.readDB.IssueRandomCommitments.WithContext(ctx).
Where(s.readDB.IssueRandomCommitments.IssueID.Eq(issueID)).
First()
if existing != nil {
return nil, errors.New("该期数已存在随机承诺,不可重复生成")
}
items, err := s.readDB.ActivityRewardSettings.WithContext(ctx).
Where(s.readDB.ActivityRewardSettings.IssueID.Eq(issueID)).
Order(s.readDB.ActivityRewardSettings.ID).
Find()
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, errors.New("该期数未配置奖励,无法生成随机承诺")
}
canonical := make([]ReceiptItem, 0, len(items))
var weightsTotal int64
for _, it := range items {
canonical = append(canonical, ReceiptItem{ID: it.ID, Name: it.Name, Weight: it.Weight, QuantityBefore: it.Quantity})
if it.Weight > 0 && (it.Quantity == -1 || it.Quantity > 0) {
weightsTotal += int64(it.Weight)
}
}
// 检查奖池权重是否有效
if weightsTotal <= 0 {
return nil, errors.New("奖池配置无效总权重必须大于0")
}
sort.Slice(canonical, func(i, j int) bool { return canonical[i].ID < canonical[j].ID })
b, _ := json.Marshal(canonical)
itemsRoot := sha256.Sum256(b)
master := make([]byte, 32)
_, _ = rand.Read(master)
serverHash := sha256.Sum256(master)
nextVer := int32(1)
enc, _ := maskSeed(master, issueID, nextVer)
rec := &model.IssueRandomCommitments{
IssueID: issueID,
AlgoVersion: algoVersion,
ServerSeedMaster: enc,
ServerSeedHash: serverHash[:],
ItemsRoot: itemsRoot[:],
WeightsTotal: weightsTotal,
StateVersion: nextVer,
}
if err := s.writeDB.IssueRandomCommitments.WithContext(ctx).Create(rec); err != nil {
return nil, err
}
return &IssueRandomCommitment{
AlgoVersion: rec.AlgoVersion,
IssueID: rec.IssueID,
ServerSeedMaster: rec.ServerSeedMaster,
ServerSeedHash: rec.ServerSeedHash,
ItemsRoot: rec.ItemsRoot,
WeightsTotal: rec.WeightsTotal,
StateVersion: rec.StateVersion,
}, nil
}
func (s *service) GetIssueRandomCommit(ctx context.Context, issueID int64) (*IssueRandomCommitment, error) {
latest, err := s.readDB.IssueRandomCommitments.WithContext(ctx).
Where(s.readDB.IssueRandomCommitments.IssueID.Eq(issueID)).
Order(s.readDB.IssueRandomCommitments.StateVersion.Desc()).
Take()
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil // 没有找到记录是正常的,表示尚未生成承诺
}
return nil, err
}
return &IssueRandomCommitment{
AlgoVersion: latest.AlgoVersion,
IssueID: latest.IssueID,
ServerSeedMaster: latest.ServerSeedMaster,
ServerSeedHash: latest.ServerSeedHash,
ItemsRoot: latest.ItemsRoot,
WeightsTotal: latest.WeightsTotal,
StateVersion: latest.StateVersion,
}, nil
}
func (s *service) GetIssueRandomCommitHistory(ctx context.Context, issueID int64) ([]*IssueRandomCommitment, error) {
commitments, err := s.readDB.IssueRandomCommitments.WithContext(ctx).
Where(s.readDB.IssueRandomCommitments.IssueID.Eq(issueID)).
Order(s.readDB.IssueRandomCommitments.StateVersion.Desc()).
Find()
if err != nil {
if err == gorm.ErrRecordNotFound {
return []*IssueRandomCommitment{}, nil // 返回空数组而不是错误
}
return nil, err
}
result := make([]*IssueRandomCommitment, 0, len(commitments))
for _, commit := range commitments {
result = append(result, &IssueRandomCommitment{
AlgoVersion: commit.AlgoVersion,
IssueID: commit.IssueID,
ServerSeedMaster: commit.ServerSeedMaster,
ServerSeedHash: commit.ServerSeedHash,
ItemsRoot: commit.ItemsRoot,
WeightsTotal: commit.WeightsTotal,
StateVersion: commit.StateVersion,
})
}
return result, nil
}

View File

@ -0,0 +1,58 @@
package activity
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"bindbox-game/configs"
)
func masterKey() []byte {
s := configs.Get().Random.CommitMasterKey
if s == "" {
return nil
}
b, err := hex.DecodeString(s)
if err != nil || len(b) == 0 {
return nil
}
return b
}
func deriveMask(key []byte, issueID int64, version int32) []byte {
m := hmac.New(sha256.New, key)
buf := make([]byte, 12)
binary.BigEndian.PutUint64(buf[:8], uint64(issueID))
binary.BigEndian.PutUint32(buf[8:12], uint32(version))
m.Write(buf)
sum := m.Sum(nil)
return sum[:32]
}
func maskSeed(master []byte, issueID int64, version int32) ([]byte, bool) {
key := masterKey()
if key == nil {
return master, false
}
ks := deriveMask(key, issueID, version)
out := make([]byte, len(master))
for i := range master {
out[i] = master[i] ^ ks[i%len(ks)]
}
return out, true
}
func unmaskSeed(enc []byte, issueID int64, version int32) []byte {
key := masterKey()
if key == nil {
return enc
}
ks := deriveMask(key, issueID, version)
out := make([]byte, len(enc))
for i := range enc {
out[i] = enc[i] ^ ks[i%len(ks)]
}
return out
}

View File

@ -0,0 +1,35 @@
package user
import (
"context"
"time"
"bindbox-game/internal/repository/mysql/model"
)
type CreateUserInput struct {
Nickname string
OpenID string
Avatar string
}
func (s *service) CreateUser(ctx context.Context, in CreateUserInput) (*model.Users, error) {
now := time.Now()
u := &model.Users{
Nickname: in.Nickname,
Openid: in.OpenID,
Avatar: in.Avatar,
Status: 1,
CreatedAt: now,
UpdatedAt: now,
}
if err := s.writeDB.Users.WithContext(ctx).Create(u); err != nil {
return nil, err
}
return u, nil
}
func (s *service) DeleteUser(ctx context.Context, userID int64) error {
_, err := s.writeDB.Users.WithContext(ctx).Where(s.writeDB.Users.ID.Eq(userID)).Delete()
return err
}

View File

@ -31,6 +31,8 @@ type Service interface {
ListAddresses(ctx context.Context, userID int64, page, pageSize int) (items []*model.UserAddresses, total int64, err error) ListAddresses(ctx context.Context, userID int64, page, pageSize int) (items []*model.UserAddresses, total int64, err error)
SetDefaultAddress(ctx context.Context, userID int64, addressID int64) error SetDefaultAddress(ctx context.Context, userID int64, addressID int64) error
DeleteAddress(ctx context.Context, userID int64, addressID int64) error DeleteAddress(ctx context.Context, userID int64, addressID int64) error
CreateUser(ctx context.Context, in CreateUserInput) (*model.Users, error)
DeleteUser(ctx context.Context, userID int64) error
} }
type service struct { type service struct {

View File

@ -3,3 +3,7 @@
{"level":"fatal","time":"2025-11-14 15:16:49","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"} {"level":"fatal","time":"2025-11-14 15:16:49","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}
{"level":"fatal","time":"2025-11-14 16:52:42","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"} {"level":"fatal","time":"2025-11-14 16:52:42","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}
{"level":"fatal","time":"2025-11-14 18:32:56","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"} {"level":"fatal","time":"2025-11-14 18:32:56","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}
{"level":"fatal","time":"2025-11-15 12:25:30","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}
{"level":"fatal","time":"2025-11-15 12:26:06","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}
{"level":"fatal","time":"2025-11-15 12:28:06","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}
{"level":"fatal","time":"2025-11-15 13:05:26","caller":"logger/logger.go:333","msg":"http server startup err","domain":"mini-chat[fat]","error":"listen tcp :9991: bind: address already in use"}

218
test_lottery_profit.js Normal file
View File

@ -0,0 +1,218 @@
// 抽奖盈亏计算测试示例
// 基于实际业务逻辑的模拟数据
// 模拟一个抽奖活动的配置
const mockActivity = {
id: 1,
name: "春节抽奖活动",
price_draw: 1000, // 门票价格10元 = 1000分
status: 1
};
// 模拟奖项配置
const mockRewards = [
{
id: 1,
name: "iPhone 15 Pro",
weight: 1,
quantity: 10,
product_id: 101,
cost: 800000 // 成本8000元 = 800000分
},
{
id: 2,
name: "AirPods Pro",
weight: 10,
quantity: 50,
product_id: 102,
cost: 200000 // 成本2000元 = 200000分
},
{
id: 3,
name: "小米手环",
weight: 100,
quantity: 200,
product_id: 103,
cost: 20000 // 成本200元 = 20000分
},
{
id: 4,
name: "优惠券10元",
weight: 1000,
quantity: -1, // -1 表示不限量
product_id: null,
cost: 1000 // 成本10元 = 1000分平台补贴
},
{
id: 5,
name: "谢谢参与",
weight: 5000,
quantity: -1,
product_id: null,
cost: 0 // 无成本
}
];
// 计算理论概率和期望支出
function calculateTheoreticalData(rewards) {
// 筛选有效奖项weight > 0 且 quantity != 0
const validRewards = rewards.filter(r => r.weight > 0 && r.quantity !== 0);
// 计算总权重
const totalWeight = validRewards.reduce((sum, r) => sum + r.weight, 0);
// 计算每个奖项的理论概率和期望支出
const results = validRewards.map(reward => {
const probability = reward.weight / totalWeight;
const expectedCost = probability * reward.cost; // 单次抽奖的期望支出
return {
...reward,
probability,
expectedCost
};
});
// 总期望支出(单次抽奖)
const totalExpectedCost = results.reduce((sum, r) => sum + r.expectedCost, 0);
return {
rewards: results,
totalWeight,
totalExpectedCost
};
}
// 模拟抽奖结果
function simulateDraws(rewards, sampleSize) {
const theoretical = calculateTheoreticalData(rewards);
const results = {};
// 初始化结果统计
theoretical.rewards.forEach(reward => {
results[reward.id] = {
...reward,
count: 0,
simulatedCost: 0
};
});
// 模拟抽奖
for (let i = 0; i < sampleSize; i++) {
const random = Math.random();
let cumulativeProbability = 0;
// 根据概率选择奖项
for (const reward of theoretical.rewards) {
cumulativeProbability += reward.probability;
if (random <= cumulativeProbability) {
results[reward.id].count++;
results[reward.id].simulatedCost += reward.cost;
break;
}
}
}
return {
theoretical,
simulated: Object.values(results),
sampleSize
};
}
// 计算盈亏指标
function calculateProfitMetrics(activity, simulation) {
const { theoretical, simulated, sampleSize } = simulation;
const ticketPrice = activity.price_draw;
// 理论计算
const theoreticalRevenue = ticketPrice * sampleSize;
const theoreticalPayout = theoretical.totalExpectedCost * sampleSize;
const theoreticalProfit = theoreticalRevenue - theoreticalPayout;
const theoreticalMargin = theoreticalProfit / theoreticalRevenue;
// 模拟计算
const simulatedRevenue = ticketPrice * sampleSize;
const simulatedPayout = simulated.reduce((sum, r) => sum + r.simulatedCost, 0);
const simulatedProfit = simulatedRevenue - simulatedPayout;
const simulatedMargin = simulatedProfit / simulatedRevenue;
return {
theoretical: {
revenue: theoreticalRevenue,
payout: theoreticalPayout,
profit: theoreticalProfit,
margin: theoreticalMargin
},
simulated: {
revenue: simulatedRevenue,
payout: simulatedPayout,
profit: simulatedProfit,
margin: simulatedMargin
}
};
}
// 运行测试
function runLotteryProfitTest() {
console.log("=== 抽奖盈亏分析测试 ===");
console.log(`活动: ${mockActivity.name}`);
console.log(`门票价格: ¥${(mockActivity.price_draw / 100).toFixed(2)}`);
console.log("");
// 理论计算
const theoreticalData = calculateTheoreticalData(mockRewards);
console.log("--- 理论概率分析 ---");
console.log(`总权重: ${theoreticalData.totalWeight}`);
console.log(`单次期望支出: ¥${(theoreticalData.totalExpectedCost / 100).toFixed(4)}`);
console.log(`单次期望利润: ¥${((mockActivity.price_draw - theoreticalData.totalExpectedCost) / 100).toFixed(4)}`);
console.log("");
theoreticalData.rewards.forEach(reward => {
console.log(`${reward.name}:`);
console.log(` 权重: ${reward.weight}, 概率: ${(reward.probability * 100).toFixed(4)}%`);
console.log(` 成本: ¥${(reward.cost / 100).toFixed(2)}, 期望支出: ¥${(reward.expectedCost / 100).toFixed(4)}`);
});
console.log("\n--- 模拟结果 (样本数: 10000) ---");
const simulation = simulateDraws(mockRewards, 10000);
const metrics = calculateProfitMetrics(mockActivity, simulation);
console.log("理论值:");
console.log(` 总收入: ¥${(metrics.theoretical.revenue / 100).toFixed(2)}`);
console.log(` 总支出: ¥${(metrics.theoretical.payout / 100).toFixed(2)}`);
console.log(` 总利润: ¥${(metrics.theoretical.profit / 100).toFixed(2)}`);
console.log(` 利润率: ${(metrics.theoretical.margin * 100).toFixed(2)}%`);
console.log("\n模拟值:");
console.log(` 总收入: ¥${(metrics.simulated.revenue / 100).toFixed(2)}`);
console.log(` 总支出: ¥${(metrics.simulated.payout / 100).toFixed(2)}`);
console.log(` 总利润: ¥${(metrics.simulated.profit / 100).toFixed(2)}`);
console.log(` 利润率: ${(metrics.simulated.margin * 100).toFixed(2)}%`);
console.log("\n--- 各奖项模拟结果 ---");
simulation.simulated.forEach(result => {
const simulatedRate = result.count / simulation.sampleSize;
const theoreticalRate = result.probability;
const diff = Math.abs(simulatedRate - theoreticalRate) * 100;
console.log(`${result.name}:`);
console.log(` 模拟次数: ${result.count}, 模拟概率: ${(simulatedRate * 100).toFixed(3)}%`);
console.log(` 理论概率: ${(theoreticalRate * 100).toFixed(3)}%, 差异: ${diff.toFixed(3)}%`);
console.log(` 模拟支出: ¥${(result.simulatedCost / 100).toFixed(2)}`);
});
// 验证大数定律
console.log("\n--- 大数定律验证 ---");
const largeSample = simulateDraws(mockRewards, 100000);
largeSample.simulated.forEach(result => {
const simulatedRate = result.count / largeSample.sampleSize;
const theoreticalRate = result.probability;
const diff = Math.abs(simulatedRate - theoreticalRate) * 100;
console.log(`${result.name}: 模拟概率 ${(simulatedRate * 100).toFixed(4)}% vs 理论概率 ${(theoreticalRate * 100).toFixed(4)}%, 差异 ${diff.toFixed(4)}%`);
});
}
// 运行测试
runLotteryProfitTest();