diff --git a/.DS_Store b/.DS_Store index e88bbac..f4fe911 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/bin/server b/bin/server index 93762fa..078b122 100755 Binary files a/bin/server and b/bin/server differ diff --git a/bindbox-game b/bindbox-game index 0120e07..672cb39 100755 Binary files a/bindbox-game and b/bindbox-game differ diff --git a/bindbox-server b/bindbox-server new file mode 100755 index 0000000..67fda43 Binary files /dev/null and b/bindbox-server differ diff --git a/cmd/gormgen/README.md b/cmd/gormgen/README.md index a2fd36f..028a4e2 100644 --- a/cmd/gormgen/README.md +++ b/cmd/gormgen/README.md @@ -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" ``` \ No newline at end of file diff --git a/configs/configs.go b/configs/configs.go index 21a9e4c..a43bc18 100644 --- a/configs/configs.go +++ b/configs/configs.go @@ -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 ( diff --git a/configs/dev_configs.toml b/configs/dev_configs.toml index e69de29..747f1e2 100644 --- a/configs/dev_configs.toml +++ b/configs/dev_configs.toml @@ -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" \ No newline at end of file diff --git a/configs/fat_configs.toml b/configs/fat_configs.toml index 3ccc1fb..be15faf 100644 --- a/configs/fat_configs.toml +++ b/configs/fat_configs.toml @@ -27,3 +27,6 @@ secret_id = "AKIDtjPtAFPNDuR1UnxvoUCoRAnJgw164Zv6" secret_key = "B0vvjMoMsKcipnJlLnFyWt6A2JRSJ0Wr" # 可选:如有 CDN/自定义域名则填写,否则留空 base_url = "" + +[random] +commit_master_key = "4d7a3b8f9c2e1a5d6b4f8c0e3a7d2b1c6f9e4a5d8c1b3f7a2e5d6c4b8f0e3a7d2b1c" \ No newline at end of file diff --git a/configs/pro_configs.toml b/configs/pro_configs.toml index 139597f..747f1e2 100644 --- a/configs/pro_configs.toml +++ b/configs/pro_configs.toml @@ -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" \ No newline at end of file diff --git a/configs/uat_configs.toml b/configs/uat_configs.toml index 139597f..747f1e2 100644 --- a/configs/uat_configs.toml +++ b/configs/uat_configs.toml @@ -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" \ No newline at end of file diff --git a/data.db b/data.db new file mode 100644 index 0000000..e69de29 diff --git a/internal/api/activity/draw_app.go b/internal/api/activity/draw_app.go new file mode 100644 index 0000000..4ecc956 --- /dev/null +++ b/internal/api/activity/draw_app.go @@ -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}) + } +} \ No newline at end of file diff --git a/internal/api/admin/batch_draw.go b/internal/api/admin/batch_draw.go new file mode 100644 index 0000000..ebdd277 --- /dev/null +++ b/internal/api/admin/batch_draw.go @@ -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}) + } +} diff --git a/internal/api/admin/batch_users.go b/internal/api/admin/batch_users.go new file mode 100644 index 0000000..a80fc7c --- /dev/null +++ b/internal/api/admin/batch_users.go @@ -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: "操作成功"}) + } +} \ No newline at end of file diff --git a/internal/api/admin/draw_receipt.go b/internal/api/admin/draw_receipt.go new file mode 100644 index 0000000..f6707ca --- /dev/null +++ b/internal/api/admin/draw_receipt.go @@ -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, + }) + } +} diff --git a/internal/api/admin/draw_simulate.go b/internal/api/admin/draw_simulate.go new file mode 100644 index 0000000..d5eac60 --- /dev/null +++ b/internal/api/admin/draw_simulate.go @@ -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) +} \ No newline at end of file diff --git a/internal/api/admin/draw_verify_helper.go b/internal/api/admin/draw_verify_helper.go new file mode 100644 index 0000000..b21bbe7 --- /dev/null +++ b/internal/api/admin/draw_verify_helper.go @@ -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) +} \ No newline at end of file diff --git a/internal/api/admin/issue_random_commit.go b/internal/api/admin/issue_random_commit.go new file mode 100644 index 0000000..442cc21 --- /dev/null +++ b/internal/api/admin/issue_random_commit.go @@ -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) +} \ No newline at end of file diff --git a/internal/api/admin/verify_draw.go b/internal/api/admin/verify_draw.go new file mode 100644 index 0000000..96baa26 --- /dev/null +++ b/internal/api/admin/verify_draw.go @@ -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: "验证通过"}) + } +} \ No newline at end of file diff --git a/internal/code/code.go b/internal/code/code.go index 92ee512..a541353 100644 --- a/internal/code/code.go +++ b/internal/code/code.go @@ -15,11 +15,11 @@ type Failure struct { Message string `json:"message"` // 描述信息 } -const ( - ServerError = 10101 - ParamBindError = 10102 - JWTAuthVerifyError = 10103 - UploadError = 10104 + const ( + ServerError = 10101 + ParamBindError = 10102 + JWTAuthVerifyError = 10103 + UploadError = 10104 AdminLoginError = 20101 CreateAppError = 20201 @@ -69,8 +69,14 @@ const ( DeleteActivityIssueError = 20509 CreateIssueRewardsError = 20510 ListIssueRewardsError = 20511 - ListDrawLogsError = 20512 -) + ListDrawLogsError = 20512 + ExecuteDrawError = 20513 + CommitIssueRandomError = 20514 + GetIssueRandomCommitError= 20515 + CreateUserError = 20516 + DeleteUserError = 20517 + GetDrawReceiptError = 20518 + ) const ( CreateGuildError = 20601 diff --git a/internal/code/zh-cn.go b/internal/code/zh-cn.go index ffccaf2..2da41db 100644 --- a/internal/code/zh-cn.go +++ b/internal/code/zh-cn.go @@ -41,8 +41,12 @@ var zhCNText = map[int]string{ ModifyActivityIssueError: "修改活动期数失败", DeleteActivityIssueError: "删除活动期数失败", CreateIssueRewardsError: "创建期数奖品失败", - ListIssueRewardsError: "获取期数奖品失败", - ListDrawLogsError: "获取抽奖记录失败", + ListIssueRewardsError: "获取期数奖品失败", + ListDrawLogsError: "获取抽奖记录失败", + ExecuteDrawError: "执行抽奖失败", + CommitIssueRandomError: "生成期随机承诺失败", + GetIssueRandomCommitError:"获取期随机承诺失败", + GetDrawReceiptError: "获取抽奖收据失败", CreateGuildError: "创建工会失败", ModifyGuildError: "修改工会失败", diff --git a/internal/repository/mysql/dao/activity_draw_receipts.gen.go b/internal/repository/mysql/dao/activity_draw_receipts.gen.go new file mode 100644 index 0000000..23cef1e --- /dev/null +++ b/internal/repository/mysql/dao/activity_draw_receipts.gen.go @@ -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 // 抽奖日志ID(activity_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 +} diff --git a/internal/repository/mysql/dao/gen.go b/internal/repository/mysql/dao/gen.go index f8e007c..1e21108 100644 --- a/internal/repository/mysql/dao/gen.go +++ b/internal/repository/mysql/dao/gen.go @@ -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), diff --git a/internal/repository/mysql/dao/issue_random_commitments.gen.go b/internal/repository/mysql/dao/issue_random_commitments.gen.go new file mode 100644 index 0000000..35e07e7 --- /dev/null +++ b/internal/repository/mysql/dao/issue_random_commitments.gen.go @@ -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 +} diff --git a/internal/repository/mysql/dao/system_coupons.gen.go b/internal/repository/mysql/dao/system_coupons.gen.go index f0c4cf6..215ba51 100644 --- a/internal/repository/mysql/dao/system_coupons.gen.go +++ b/internal/repository/mysql/dao/system_coupons.gen.go @@ -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 { diff --git a/internal/repository/mysql/model/activity_draw_receipts.gen.go b/internal/repository/mysql/model/activity_draw_receipts.gen.go new file mode 100644 index 0000000..d4fd834 --- /dev/null +++ b/internal/repository/mysql/model/activity_draw_receipts.gen.go @@ -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:抽奖日志ID(activity_draw_logs.id)" json:"draw_log_id"` // 抽奖日志ID(activity_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 +} diff --git a/internal/repository/mysql/model/issue_random_commitments.gen.go b/internal/repository/mysql/model/issue_random_commitments.gen.go new file mode 100644 index 0000000..5a9e61b --- /dev/null +++ b/internal/repository/mysql/model/issue_random_commitments.gen.go @@ -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 +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 +} diff --git a/internal/repository/mysql/model/system_coupons.gen.go b/internal/repository/mysql/model/system_coupons.gen.go index db9ca27..0e65402 100644 --- a/internal/repository/mysql/model/system_coupons.gen.go +++ b/internal/repository/mysql/model/system_coupons.gen.go @@ -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 diff --git a/internal/router/router.go b/internal/router/router.go index b1d9f61..c2a86f5 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -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 diff --git a/internal/service/activity/activity.go b/internal/service/activity/activity.go index dcfced4..c667b18 100644 --- a/internal/service/activity/activity.go +++ b/internal/service/activity/activity.go @@ -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 diff --git a/internal/service/activity/draw_execute.go b/internal/service/activity/draw_execute.go new file mode 100644 index 0000000..e724628 --- /dev/null +++ b/internal/service/activity/draw_execute.go @@ -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) + } +} \ No newline at end of file diff --git a/internal/service/activity/random_commit.go b/internal/service/activity/random_commit.go new file mode 100644 index 0000000..107176c --- /dev/null +++ b/internal/service/activity/random_commit.go @@ -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 +} diff --git a/internal/service/activity/seed_crypto.go b/internal/service/activity/seed_crypto.go new file mode 100644 index 0000000..3cdadea --- /dev/null +++ b/internal/service/activity/seed_crypto.go @@ -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 +} \ No newline at end of file diff --git a/internal/service/user/batch_user.go b/internal/service/user/batch_user.go new file mode 100644 index 0000000..d73fb63 --- /dev/null +++ b/internal/service/user/batch_user.go @@ -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 +} \ No newline at end of file diff --git a/internal/service/user/user.go b/internal/service/user/user.go index 4251c15..a52457e 100644 --- a/internal/service/user/user.go +++ b/internal/service/user/user.go @@ -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 { diff --git a/logs/mini-chat-access.log b/logs/mini-chat-access.log index 021d497..8e643f8 100644 --- a/logs/mini-chat-access.log +++ b/logs/mini-chat-access.log @@ -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"} diff --git a/test_lottery_profit.js b/test_lottery_profit.js new file mode 100644 index 0000000..798fb1f --- /dev/null +++ b/test_lottery_profit.js @@ -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(); \ No newline at end of file