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,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"`
BaseURL string `mapstructure:"base_url" toml:"base_url"`
} `mapstructure:"cos" toml:"cos"`
Random struct {
CommitMasterKey string `mapstructure:"commit_master_key" toml:"commit_master_key"`
} `mapstructure:"random" toml:"random"`
}
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"
# 可选:如有 CDN/自定义域名则填写,否则留空
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,7 +15,7 @@ type Failure struct {
Message string `json:"message"` // 描述信息
}
const (
const (
ServerError = 10101
ParamBindError = 10102
JWTAuthVerifyError = 10103
@ -70,7 +70,13 @@ const (
CreateIssueRewardsError = 20510
ListIssueRewardsError = 20511
ListDrawLogsError = 20512
)
ExecuteDrawError = 20513
CommitIssueRandomError = 20514
GetIssueRandomCommitError= 20515
CreateUserError = 20516
DeleteUserError = 20517
GetDrawReceiptError = 20518
)
const (
CreateGuildError = 20601

View File

@ -43,6 +43,10 @@ var zhCNText = map[int]string{
CreateIssueRewardsError: "创建期数奖品失败",
ListIssueRewardsError: "获取期数奖品失败",
ListDrawLogsError: "获取抽奖记录失败",
ExecuteDrawError: "执行抽奖失败",
CommitIssueRandomError: "生成期随机承诺失败",
GetIssueRandomCommitError:"获取期随机承诺失败",
GetDrawReceiptError: "获取抽奖收据失败",
CreateGuildError: "创建工会失败",
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
ActivityDrawEffects *activityDrawEffects
ActivityDrawLogs *activityDrawLogs
ActivityDrawReceipts *activityDrawReceipts
ActivityIssues *activityIssues
ActivityRewardSettings *activityRewardSettings
Admin *admin
@ -29,6 +30,7 @@ var (
GuildBoxes *guildBoxes
GuildContributeLogs *guildContributeLogs
GuildMembers *guildMembers
IssueRandomCommitments *issueRandomCommitments
LogOperation *logOperation
LogRequest *logRequest
MenuActions *menuActions
@ -61,6 +63,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
ActivityCategories = &Q.ActivityCategories
ActivityDrawEffects = &Q.ActivityDrawEffects
ActivityDrawLogs = &Q.ActivityDrawLogs
ActivityDrawReceipts = &Q.ActivityDrawReceipts
ActivityIssues = &Q.ActivityIssues
ActivityRewardSettings = &Q.ActivityRewardSettings
Admin = &Q.Admin
@ -69,6 +72,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
GuildBoxes = &Q.GuildBoxes
GuildContributeLogs = &Q.GuildContributeLogs
GuildMembers = &Q.GuildMembers
IssueRandomCommitments = &Q.IssueRandomCommitments
LogOperation = &Q.LogOperation
LogRequest = &Q.LogRequest
MenuActions = &Q.MenuActions
@ -102,6 +106,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
ActivityCategories: newActivityCategories(db, opts...),
ActivityDrawEffects: newActivityDrawEffects(db, opts...),
ActivityDrawLogs: newActivityDrawLogs(db, opts...),
ActivityDrawReceipts: newActivityDrawReceipts(db, opts...),
ActivityIssues: newActivityIssues(db, opts...),
ActivityRewardSettings: newActivityRewardSettings(db, opts...),
Admin: newAdmin(db, opts...),
@ -110,6 +115,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
GuildBoxes: newGuildBoxes(db, opts...),
GuildContributeLogs: newGuildContributeLogs(db, opts...),
GuildMembers: newGuildMembers(db, opts...),
IssueRandomCommitments: newIssueRandomCommitments(db, opts...),
LogOperation: newLogOperation(db, opts...),
LogRequest: newLogRequest(db, opts...),
MenuActions: newMenuActions(db, opts...),
@ -144,6 +150,7 @@ type Query struct {
ActivityCategories activityCategories
ActivityDrawEffects activityDrawEffects
ActivityDrawLogs activityDrawLogs
ActivityDrawReceipts activityDrawReceipts
ActivityIssues activityIssues
ActivityRewardSettings activityRewardSettings
Admin admin
@ -152,6 +159,7 @@ type Query struct {
GuildBoxes guildBoxes
GuildContributeLogs guildContributeLogs
GuildMembers guildMembers
IssueRandomCommitments issueRandomCommitments
LogOperation logOperation
LogRequest logRequest
MenuActions menuActions
@ -187,6 +195,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
ActivityCategories: q.ActivityCategories.clone(db),
ActivityDrawEffects: q.ActivityDrawEffects.clone(db),
ActivityDrawLogs: q.ActivityDrawLogs.clone(db),
ActivityDrawReceipts: q.ActivityDrawReceipts.clone(db),
ActivityIssues: q.ActivityIssues.clone(db),
ActivityRewardSettings: q.ActivityRewardSettings.clone(db),
Admin: q.Admin.clone(db),
@ -195,6 +204,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
GuildBoxes: q.GuildBoxes.clone(db),
GuildContributeLogs: q.GuildContributeLogs.clone(db),
GuildMembers: q.GuildMembers.clone(db),
IssueRandomCommitments: q.IssueRandomCommitments.clone(db),
LogOperation: q.LogOperation.clone(db),
LogRequest: q.LogRequest.clone(db),
MenuActions: q.MenuActions.clone(db),
@ -237,6 +247,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
ActivityCategories: q.ActivityCategories.replaceDB(db),
ActivityDrawEffects: q.ActivityDrawEffects.replaceDB(db),
ActivityDrawLogs: q.ActivityDrawLogs.replaceDB(db),
ActivityDrawReceipts: q.ActivityDrawReceipts.replaceDB(db),
ActivityIssues: q.ActivityIssues.replaceDB(db),
ActivityRewardSettings: q.ActivityRewardSettings.replaceDB(db),
Admin: q.Admin.replaceDB(db),
@ -245,6 +256,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
GuildBoxes: q.GuildBoxes.replaceDB(db),
GuildContributeLogs: q.GuildContributeLogs.replaceDB(db),
GuildMembers: q.GuildMembers.replaceDB(db),
IssueRandomCommitments: q.IssueRandomCommitments.replaceDB(db),
LogOperation: q.LogOperation.replaceDB(db),
LogRequest: q.LogRequest.replaceDB(db),
MenuActions: q.MenuActions.replaceDB(db),
@ -277,6 +289,7 @@ type queryCtx struct {
ActivityCategories *activityCategoriesDo
ActivityDrawEffects *activityDrawEffectsDo
ActivityDrawLogs *activityDrawLogsDo
ActivityDrawReceipts *activityDrawReceiptsDo
ActivityIssues *activityIssuesDo
ActivityRewardSettings *activityRewardSettingsDo
Admin *adminDo
@ -285,6 +298,7 @@ type queryCtx struct {
GuildBoxes *guildBoxesDo
GuildContributeLogs *guildContributeLogsDo
GuildMembers *guildMembersDo
IssueRandomCommitments *issueRandomCommitmentsDo
LogOperation *logOperationDo
LogRequest *logRequestDo
MenuActions *menuActionsDo
@ -317,6 +331,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
ActivityCategories: q.ActivityCategories.WithContext(ctx),
ActivityDrawEffects: q.ActivityDrawEffects.WithContext(ctx),
ActivityDrawLogs: q.ActivityDrawLogs.WithContext(ctx),
ActivityDrawReceipts: q.ActivityDrawReceipts.WithContext(ctx),
ActivityIssues: q.ActivityIssues.WithContext(ctx),
ActivityRewardSettings: q.ActivityRewardSettings.WithContext(ctx),
Admin: q.Admin.WithContext(ctx),
@ -325,6 +340,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
GuildBoxes: q.GuildBoxes.WithContext(ctx),
GuildContributeLogs: q.GuildContributeLogs.WithContext(ctx),
GuildMembers: q.GuildMembers.WithContext(ctx),
IssueRandomCommitments: q.IssueRandomCommitments.WithContext(ctx),
LogOperation: q.LogOperation.WithContext(ctx),
LogRequest: q.LogRequest.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.ValidEnd = field.NewTime(tableName, "valid_end")
_systemCoupons.Status = field.NewInt32(tableName, "status")
_systemCoupons.TotalQuantity = field.NewInt64(tableName, "total_quantity")
_systemCoupons.fillFieldMap()
@ -64,6 +65,7 @@ type systemCoupons struct {
ValidStart field.Time // 有效期开始
ValidEnd field.Time // 有效期结束
Status field.Int32 // 状态1启用 2停用
TotalQuantity field.Int64
fieldMap map[string]field.Expr
}
@ -93,6 +95,7 @@ func (s *systemCoupons) updateTableName(table string) *systemCoupons {
s.ValidStart = field.NewTime(table, "valid_start")
s.ValidEnd = field.NewTime(table, "valid_end")
s.Status = field.NewInt32(table, "status")
s.TotalQuantity = field.NewInt64(table, "total_quantity")
s.fillFieldMap()
@ -109,7 +112,7 @@ func (s *systemCoupons) GetFieldByName(fieldName string) (field.OrderExpr, bool)
}
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["created_at"] = s.CreatedAt
s.fieldMap["updated_at"] = s.UpdatedAt
@ -123,6 +126,7 @@ func (s *systemCoupons) fillFieldMap() {
s.fieldMap["valid_start"] = s.ValidStart
s.fieldMap["valid_end"] = s.ValidEnd
s.fieldMap["status"] = s.Status
s.fieldMap["total_quantity"] = s.TotalQuantity
}
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"` // 有效期开始
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停用
TotalQuantity int64 `gorm:"column:total_quantity" json:"total_quantity"`
}
// 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.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.PUT("/guilds/:guild_id", adminHandler.ModifyGuild())
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.PUT("/users/:user_id/addresses/:address_id/default", userHandler.SetDefaultUserAddress())
appAuthApiRouter.DELETE("/users/:user_id/addresses/:address_id", userHandler.DeleteUserAddress())
appAuthApiRouter.POST("/activities/:activity_id/issues/:issue_id/draw", activityHandler.ExecuteDraw())
}
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)
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)
}
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 {
logger logger.CustomLogger
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)
SetDefaultAddress(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 {

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 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-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();