feat: minesweeper dynamic config and granular rewards
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 48s

This commit is contained in:
邹方成 2025-12-24 17:33:13 +08:00
parent 425e64daa5
commit c9a83a232a
21 changed files with 1601 additions and 116 deletions

View File

@ -18,7 +18,7 @@ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -tags timetzda
export DOCKER_DEFAULT_PLATFORM=linux/amd64 export DOCKER_DEFAULT_PLATFORM=linux/amd64
docker build -t zfc931912343/bindbox-game:v1.9 . docker build -t zfc931912343/bindbox-game:v1.10 .
docker push zfc931912343/bindbox-game:v1.9 docker push zfc931912343/bindbox-game:v1.10
docker pull zfc931912343/bindbox-game:v1.9 &&docker rm -f bindbox-game && docker run -d --name bindbox-game -p 9991:9991 zfc931912343/bindbox-game:v1.9 docker pull zfc931912343/bindbox-game:v1.10 &&docker rm -f bindbox-game && docker run -d --name bindbox-game -p 9991:9991 zfc931912343/bindbox-game:v1.10

BIN
build.zip

Binary file not shown.

View File

@ -38,7 +38,7 @@
} }
})() })()
</script> </script>
<script type="module" crossorigin src="/assets/index-D9UEFhei.js"></script> <script type="module" crossorigin src="/assets/index-BtMJajWI.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BZQg_MtJ.css"> <link rel="stylesheet" crossorigin href="/assets/index-BZQg_MtJ.css">
</head> </head>

View File

@ -34,6 +34,6 @@ eg :
```shell ```shell
# 根目录下执行 # 根目录下执行
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,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,activity_draw_receipts,system_titles,system_title_effects,user_titles,user_title_effect_claims,payment_preorders,payment_transactions,payment_refunds,payment_notify_events,payment_bills,payment_bill_diff,ops_shipping_stats,system_configs,issue_position_claims,task_center_tasks,task_center_task_tiers,task_center_task_rewards,order_coupons,matching_card_types,channels" 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,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,activity_draw_receipts,system_titles,system_title_effects,user_titles,user_title_effect_claims,payment_preorders,payment_transactions,payment_refunds,payment_notify_events,payment_bills,payment_bill_diff,ops_shipping_stats,system_configs,issue_position_claims,task_center_tasks,task_center_task_tiers,task_center_task_rewards,order_coupons,matching_card_types,channels,user_game_tickets,game_ticket_logs"
``` ```

View File

@ -0,0 +1,380 @@
package game
import (
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/pkg/validation"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/dao"
"bindbox-game/internal/service/game"
usersvc "bindbox-game/internal/service/user"
"encoding/json"
"net/http"
"strconv"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
type handler struct {
logger logger.CustomLogger
db mysql.Repo
redis *redis.Client
ticketSvc game.TicketService
userSvc usersvc.Service
readDB *dao.Query
}
func New(l logger.CustomLogger, db mysql.Repo, rdb *redis.Client, userSvc usersvc.Service) *handler {
return &handler{
logger: l,
db: db,
redis: rdb,
ticketSvc: game.NewTicketService(l, db),
userSvc: userSvc,
readDB: dao.Use(db.GetDbR()),
}
}
// ========== Admin API ==========
type grantTicketRequest struct {
GameCode string `json:"game_code" binding:"required"`
Amount int `json:"amount" binding:"required,min=1"`
Remark string `json:"remark"`
}
// GrantUserTicket Admin为用户发放游戏资格
// @Summary 发放游戏资格
// @Tags 管理端.游戏
// @Param user_id path int true "用户ID"
// @Param RequestBody body grantTicketRequest true "请求参数"
// @Success 200 {object} map[string]any
// @Router /api/admin/users/{user_id}/game_tickets [post]
func (h *handler) GrantUserTicket() core.HandlerFunc {
return func(ctx core.Context) {
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil || userID <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "invalid user_id"))
return
}
req := new(grantTicketRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
err = h.ticketSvc.GrantTicket(ctx.RequestContext(), userID, req.GameCode, req.Amount, "admin", 0, req.Remark)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(map[string]any{"success": true})
}
}
// ListUserTickets Admin查询用户游戏资格日志
// @Summary 查询用户游戏资格日志
// @Tags 管理端.游戏
// @Param user_id path int true "用户ID"
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} map[string]any
// @Router /api/admin/users/{user_id}/game_tickets [get]
func (h *handler) ListUserTickets() core.HandlerFunc {
return func(ctx core.Context) {
userID, err := strconv.ParseInt(ctx.Param("user_id"), 10, 64)
if err != nil || userID <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "invalid user_id"))
return
}
var req struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
}
_ = ctx.ShouldBindQuery(&req)
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
logs, total, err := h.ticketSvc.GetTicketLogs(ctx.RequestContext(), userID, req.Page, req.PageSize)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(map[string]any{
"list": logs,
"total": total,
"page": req.Page,
"page_size": req.PageSize,
})
}
}
// ========== App API ==========
// GetMyTickets App获取我的游戏资格
// @Summary 获取我的游戏资格
// @Tags APP端.游戏
// @Param user_id path int true "用户ID"
// @Success 200 {object} map[string]int
// @Router /api/app/users/{user_id}/game_tickets [get]
func (h *handler) GetMyTickets() core.HandlerFunc {
return func(ctx core.Context) {
userID := int64(ctx.SessionUserInfo().Id)
tickets, err := h.ticketSvc.GetUserTickets(ctx.RequestContext(), userID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ServerError, err.Error()))
return
}
ctx.Payload(tickets)
}
}
type enterGameRequest struct {
GameCode string `json:"game_code" binding:"required"`
}
type enterGameResponse struct {
TicketToken string `json:"ticket_token"`
NakamaServer string `json:"nakama_server"`
NakamaKey string `json:"nakama_key"`
RemainingTimes int `json:"remaining_times"`
}
// EnterGame App进入游戏(消耗资格)
// @Summary 进入游戏
// @Tags APP端.游戏
// @Param RequestBody body enterGameRequest true "请求参数"
// @Success 200 {object} enterGameResponse
// @Router /api/app/games/enter [post]
func (h *handler) EnterGame() core.HandlerFunc {
return func(ctx core.Context) {
userID := int64(ctx.SessionUserInfo().Id)
req := new(enterGameRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
// 扣减资格
if err := h.ticketSvc.UseTicket(ctx.RequestContext(), userID, req.GameCode); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 180001, "游戏次数不足"))
return
}
// 生成临时token并存入Redis
ticketToken := generateTicketToken(userID)
h.redis.Set(ctx.RequestContext(), "game:ticket:"+ticketToken, userID, 30*60*1000000000) // 30分钟
// 查询剩余次数
ticket, _ := h.ticketSvc.GetUserTicketByGame(ctx.RequestContext(), userID, req.GameCode)
remaining := 0
if ticket != nil {
remaining = int(ticket.Available)
}
// TODO: 从配置读取Nakama服务器信息
ctx.Payload(&enterGameResponse{
TicketToken: ticketToken,
NakamaServer: "ws://localhost:7350",
NakamaKey: "defaultkey",
RemainingTimes: remaining,
})
}
}
// ========== Internal API (Nakama调用) ==========
type verifyRequest struct {
UserID string `json:"user_id"`
Ticket string `json:"ticket"`
}
type verifyResponse struct {
Valid bool `json:"valid"`
UserID string `json:"user_id"`
GameConfig map[string]any `json:"game_config,omitempty"`
}
// VerifyTicket Internal验证游戏票据
// @Summary 验证票据
// @Tags Internal.游戏
// @Param RequestBody body verifyRequest true "请求参数"
// @Success 200 {object} verifyResponse
// @Router /internal/game/verify [post]
func (h *handler) VerifyTicket() core.HandlerFunc {
return func(ctx core.Context) {
req := new(verifyRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
// 从Redis验证token
storedUserID, err := h.redis.Get(ctx.RequestContext(), "game:ticket:"+req.Ticket).Result()
if err != nil || storedUserID != req.UserID {
ctx.Payload(&verifyResponse{Valid: false})
return
}
// 获取游戏配置
gameConfig := make(map[string]any)
configKey := "game_minesweeper_config"
conf, err := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).Where(h.readDB.SystemConfigs.ConfigKey.Eq(configKey)).First()
if err == nil && conf != nil {
json.Unmarshal([]byte(conf.ConfigValue), &gameConfig)
}
ctx.Payload(&verifyResponse{Valid: true, UserID: req.UserID, GameConfig: gameConfig})
}
}
type settleRequest struct {
UserID string `json:"user_id"`
Ticket string `json:"ticket"`
MatchID string `json:"match_id"`
Win bool `json:"win"`
Score int `json:"score"`
}
type settleResponse struct {
Success bool `json:"success"`
Reward string `json:"reward,omitempty"`
}
// SettleGame Internal游戏结算
// @Summary 游戏结算
// @Tags Internal.游戏
// @Param RequestBody body settleRequest true "请求参数"
// @Success 200 {object} settleResponse
// @Router /internal/game/settle [post]
func (h *handler) SettleGame() core.HandlerFunc {
return func(ctx core.Context) {
req := new(settleRequest)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
// 验证token
storedUserID, err := h.redis.Get(ctx.RequestContext(), "game:ticket:"+req.Ticket).Result()
if err != nil || storedUserID != req.UserID {
ctx.Payload(&settleResponse{Success: false})
return
}
// 删除token防止重复使用
h.redis.Del(ctx.RequestContext(), "game:ticket:"+req.Ticket)
// 奖品发放逻辑
var rewardMsg string
var msConfig struct {
WinnerRewardPoints int64 `json:"winner_reward_points"`
ParticipationRewardPoints int64 `json:"participation_reward_points"`
WinnerRewardProductID int64 `json:"winner_reward_product_id"`
ParticipationRewardProductID int64 `json:"participation_reward_product_id"`
}
// 1. 读取配置
configKey := "game_minesweeper_config"
conf, err := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).Where(h.readDB.SystemConfigs.ConfigKey.Eq(configKey)).First()
if err == nil && conf != nil {
json.Unmarshal([]byte(conf.ConfigValue), &msConfig)
}
uid, _ := strconv.ParseInt(req.UserID, 10, 64)
// 2. 确定奖励内容
var targetProductID int64
var targetPoints int64
if req.Win {
targetProductID = msConfig.WinnerRewardProductID
targetPoints = msConfig.WinnerRewardPoints
if targetPoints == 0 && targetProductID == 0 {
targetPoints = 100 // 兜底
}
} else {
targetProductID = msConfig.ParticipationRewardProductID
targetPoints = msConfig.ParticipationRewardPoints
if targetPoints == 0 && targetProductID == 0 {
targetPoints = 10 // 兜底
}
}
// 3. 发放奖励
if targetProductID > 0 {
res, err := h.userSvc.GrantReward(ctx.RequestContext(), uid, usersvc.GrantRewardRequest{
ProductID: targetProductID,
Quantity: 1,
Remark: "扫雷游戏奖励",
})
if err != nil || !res.Success {
h.logger.Error("Failed to grant game product reward", zap.Error(err), zap.String("msg", res.Message))
rewardMsg = "奖励发放失败"
} else {
rewardMsg = "获得奖品"
}
} else if targetPoints > 0 {
err := h.userSvc.AddPointsWithAction(ctx.RequestContext(), uid, targetPoints, "game_reward", "扫雷游戏奖励", "minesweeper_settle", nil, nil)
if err != nil {
h.logger.Error("Failed to grant game points", zap.Error(err))
}
rewardMsg = strconv.FormatInt(targetPoints, 10) + "积分"
}
ctx.Payload(&settleResponse{Success: true, Reward: rewardMsg})
}
}
// GetMinesweeperConfig Internal获取扫雷配置
// @Summary 获取扫雷配置
// @Tags Internal.游戏
// @Success 200 {object} map[string]interface{}
// @Router /internal/game/minesweeper/config [get]
func (h *handler) GetMinesweeperConfig() core.HandlerFunc {
return func(ctx core.Context) {
configKey := "game_minesweeper_config"
conf, err := h.readDB.SystemConfigs.WithContext(ctx.RequestContext()).Where(h.readDB.SystemConfigs.ConfigKey.Eq(configKey)).First()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, "获取配置失败"))
return
}
var gameConfig map[string]interface{}
if err := json.Unmarshal([]byte(conf.ConfigValue), &gameConfig); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, "解析配置失败"))
return
}
ctx.Payload(gameConfig)
}
}
// ========== Helpers ==========
func generateTicketToken(userID int64) string {
return "GT" + randomString(16)
}
func randomString(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, n)
for i := range b {
b[i] = letters[i%len(letters)]
}
return string(b)
}

View File

@ -8,36 +8,44 @@ import (
) )
type cancelShippingRequest struct { type cancelShippingRequest struct {
InventoryID int64 `json:"inventory_id"` InventoryID int64 `json:"inventory_id"` // 单个资产ID与batch_no二选一
BatchNo string `json:"batch_no"` // 批次号与inventory_id二选一取消整批
} }
type cancelShippingResponse struct{} type cancelShippingResponse struct {
CancelledCount int64 `json:"cancelled_count"` // 成功取消的数量
}
// CancelShipping 取消发货申请 // CancelShipping 取消发货申请
// @Summary 取消发货申请 // @Summary 取消发货申请
// @Description 取消已提交但未发货的申请;恢复库存状态 // @Description 取消已提交但未发货的申请;恢复库存状态。支持按单个资产ID取消或按批次号批量取消
// @Tags APP端.用户 // @Tags APP端.用户
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Security LoginVerifyToken // @Security LoginVerifyToken
// @Param user_id path integer true "用户ID" // @Param user_id path integer true "用户ID"
// @Param RequestBody body cancelShippingRequest true "请求参数资产ID" // @Param RequestBody body cancelShippingRequest true "请求参数资产ID或批次号(二选一)"
// @Success 200 {object} cancelShippingResponse "成功" // @Success 200 {object} cancelShippingResponse "成功"
// @Failure 400 {object} code.Failure "参数错误/记录不存在/已处理" // @Failure 400 {object} code.Failure "参数错误/记录不存在/已处理"
// @Router /api/app/users/{user_id}/inventory/cancel-shipping [post] // @Router /api/app/users/{user_id}/inventory/cancel-shipping [post]
func (h *handler) CancelShipping() core.HandlerFunc { func (h *handler) CancelShipping() core.HandlerFunc {
return func(ctx core.Context) { return func(ctx core.Context) {
req := new(cancelShippingRequest) req := new(cancelShippingRequest)
if err := ctx.ShouldBindJSON(req); err != nil || req.InventoryID == 0 { if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "参数错误")) ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "参数错误"))
return return
} }
// 必须提供 inventory_id 或 batch_no 其中之一
if req.InventoryID == 0 && req.BatchNo == "" {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "请提供inventory_id或batch_no"))
return
}
userID := int64(ctx.SessionUserInfo().Id) userID := int64(ctx.SessionUserInfo().Id)
err := h.user.CancelShipping(ctx.RequestContext(), userID, req.InventoryID) count, err := h.user.CancelShipping(ctx.RequestContext(), userID, req.InventoryID, req.BatchNo)
if err != nil { if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10024, err.Error())) ctx.AbortWithError(core.Error(http.StatusBadRequest, 10024, err.Error()))
return return
} }
ctx.Payload(nil) ctx.Payload(&cancelShippingResponse{CancelledCount: count})
} }
} }

View File

@ -1,22 +1,23 @@
package app package app
import ( import (
"bindbox-game/internal/code" "bindbox-game/internal/code"
"bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation" "bindbox-game/internal/pkg/validation"
"net/http" "net/http"
) )
type requestShippingBatchRequest struct { type requestShippingBatchRequest struct {
InventoryIDs []int64 `json:"inventory_ids"` InventoryIDs []int64 `json:"inventory_ids"`
AddressID *int64 `json:"address_id"` AddressID *int64 `json:"address_id"`
} }
type requestShippingBatchResponse struct { type requestShippingBatchResponse struct {
AddressID int64 `json:"address_id"` AddressID int64 `json:"address_id"`
SuccessIDs []int64 `json:"success_ids"` BatchNo string `json:"batch_no"`
Skipped []map[string]any `json:"skipped"` SuccessIDs []int64 `json:"success_ids"`
Failed []map[string]any `json:"failed"` Skipped []map[string]any `json:"skipped"`
Failed []map[string]any `json:"failed"`
} }
// RequestShippingBatch 批量申请发货(使用默认地址或指定地址) // RequestShippingBatch 批量申请发货(使用默认地址或指定地址)
@ -32,32 +33,32 @@ type requestShippingBatchResponse struct {
// @Failure 400 {object} code.Failure // @Failure 400 {object} code.Failure
// @Router /api/app/users/{user_id}/inventory/request-shipping-batch [post] // @Router /api/app/users/{user_id}/inventory/request-shipping-batch [post]
func (h *handler) RequestShippingBatch() core.HandlerFunc { func (h *handler) RequestShippingBatch() core.HandlerFunc {
return func(ctx core.Context) { return func(ctx core.Context) {
req := new(requestShippingBatchRequest) req := new(requestShippingBatchRequest)
rsp := new(requestShippingBatchResponse) rsp := new(requestShippingBatchResponse)
if err := ctx.ShouldBindJSON(req); err != nil { if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return return
} }
if len(req.InventoryIDs) == 0 || len(req.InventoryIDs) > 100 { if len(req.InventoryIDs) == 0 || len(req.InventoryIDs) > 100 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "invalid inventory_ids")) ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "invalid inventory_ids"))
return return
} }
userID := int64(ctx.SessionUserInfo().Id) userID := int64(ctx.SessionUserInfo().Id)
addrID, success, skipped, failed, err := h.user.RequestShippings(ctx.RequestContext(), userID, req.InventoryIDs, req.AddressID) addrID, batchNo, success, skipped, failed, err := h.user.RequestShippings(ctx.RequestContext(), userID, req.InventoryIDs, req.AddressID)
if err != nil { if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10023, err.Error())) ctx.AbortWithError(core.Error(http.StatusBadRequest, 10023, err.Error()))
return return
} }
rsp.AddressID = addrID rsp.AddressID = addrID
rsp.SuccessIDs = success rsp.BatchNo = batchNo
for _, s := range skipped { rsp.SuccessIDs = success
rsp.Skipped = append(rsp.Skipped, map[string]any{"id": s.ID, "reason": s.Reason}) for _, s := range skipped {
} rsp.Skipped = append(rsp.Skipped, map[string]any{"id": s.ID, "reason": s.Reason})
for _, f := range failed { }
rsp.Failed = append(rsp.Failed, map[string]any{"id": f.ID, "reason": f.Reason}) for _, f := range failed {
} rsp.Failed = append(rsp.Failed, map[string]any{"id": f.ID, "reason": f.Reason})
ctx.Payload(rsp) }
} ctx.Payload(rsp)
}
} }

View File

@ -0,0 +1,356 @@
// 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 newGameTicketLogs(db *gorm.DB, opts ...gen.DOOption) gameTicketLogs {
_gameTicketLogs := gameTicketLogs{}
_gameTicketLogs.gameTicketLogsDo.UseDB(db, opts...)
_gameTicketLogs.gameTicketLogsDo.UseModel(&model.GameTicketLogs{})
tableName := _gameTicketLogs.gameTicketLogsDo.TableName()
_gameTicketLogs.ALL = field.NewAsterisk(tableName)
_gameTicketLogs.ID = field.NewInt64(tableName, "id")
_gameTicketLogs.CreatedAt = field.NewTime(tableName, "created_at")
_gameTicketLogs.UserID = field.NewInt64(tableName, "user_id")
_gameTicketLogs.GameCode = field.NewString(tableName, "game_code")
_gameTicketLogs.ChangeType = field.NewInt32(tableName, "change_type")
_gameTicketLogs.Amount = field.NewInt32(tableName, "amount")
_gameTicketLogs.Balance = field.NewInt32(tableName, "balance")
_gameTicketLogs.Source = field.NewString(tableName, "source")
_gameTicketLogs.SourceID = field.NewInt64(tableName, "source_id")
_gameTicketLogs.Remark = field.NewString(tableName, "remark")
_gameTicketLogs.fillFieldMap()
return _gameTicketLogs
}
// gameTicketLogs 游戏资格变动日志
type gameTicketLogs struct {
gameTicketLogsDo
ALL field.Asterisk
ID field.Int64
CreatedAt field.Time
UserID field.Int64 // 用户ID
GameCode field.String // 游戏代码
ChangeType field.Int32 // 1=获得 2=使用
Amount field.Int32 // 变动数量
Balance field.Int32 // 变动后余额
Source field.String // 来源: order/task/admin
SourceID field.Int64 // 来源ID
Remark field.String // 备注
fieldMap map[string]field.Expr
}
func (g gameTicketLogs) Table(newTableName string) *gameTicketLogs {
g.gameTicketLogsDo.UseTable(newTableName)
return g.updateTableName(newTableName)
}
func (g gameTicketLogs) As(alias string) *gameTicketLogs {
g.gameTicketLogsDo.DO = *(g.gameTicketLogsDo.As(alias).(*gen.DO))
return g.updateTableName(alias)
}
func (g *gameTicketLogs) updateTableName(table string) *gameTicketLogs {
g.ALL = field.NewAsterisk(table)
g.ID = field.NewInt64(table, "id")
g.CreatedAt = field.NewTime(table, "created_at")
g.UserID = field.NewInt64(table, "user_id")
g.GameCode = field.NewString(table, "game_code")
g.ChangeType = field.NewInt32(table, "change_type")
g.Amount = field.NewInt32(table, "amount")
g.Balance = field.NewInt32(table, "balance")
g.Source = field.NewString(table, "source")
g.SourceID = field.NewInt64(table, "source_id")
g.Remark = field.NewString(table, "remark")
g.fillFieldMap()
return g
}
func (g *gameTicketLogs) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := g.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (g *gameTicketLogs) fillFieldMap() {
g.fieldMap = make(map[string]field.Expr, 10)
g.fieldMap["id"] = g.ID
g.fieldMap["created_at"] = g.CreatedAt
g.fieldMap["user_id"] = g.UserID
g.fieldMap["game_code"] = g.GameCode
g.fieldMap["change_type"] = g.ChangeType
g.fieldMap["amount"] = g.Amount
g.fieldMap["balance"] = g.Balance
g.fieldMap["source"] = g.Source
g.fieldMap["source_id"] = g.SourceID
g.fieldMap["remark"] = g.Remark
}
func (g gameTicketLogs) clone(db *gorm.DB) gameTicketLogs {
g.gameTicketLogsDo.ReplaceConnPool(db.Statement.ConnPool)
return g
}
func (g gameTicketLogs) replaceDB(db *gorm.DB) gameTicketLogs {
g.gameTicketLogsDo.ReplaceDB(db)
return g
}
type gameTicketLogsDo struct{ gen.DO }
func (g gameTicketLogsDo) Debug() *gameTicketLogsDo {
return g.withDO(g.DO.Debug())
}
func (g gameTicketLogsDo) WithContext(ctx context.Context) *gameTicketLogsDo {
return g.withDO(g.DO.WithContext(ctx))
}
func (g gameTicketLogsDo) ReadDB() *gameTicketLogsDo {
return g.Clauses(dbresolver.Read)
}
func (g gameTicketLogsDo) WriteDB() *gameTicketLogsDo {
return g.Clauses(dbresolver.Write)
}
func (g gameTicketLogsDo) Session(config *gorm.Session) *gameTicketLogsDo {
return g.withDO(g.DO.Session(config))
}
func (g gameTicketLogsDo) Clauses(conds ...clause.Expression) *gameTicketLogsDo {
return g.withDO(g.DO.Clauses(conds...))
}
func (g gameTicketLogsDo) Returning(value interface{}, columns ...string) *gameTicketLogsDo {
return g.withDO(g.DO.Returning(value, columns...))
}
func (g gameTicketLogsDo) Not(conds ...gen.Condition) *gameTicketLogsDo {
return g.withDO(g.DO.Not(conds...))
}
func (g gameTicketLogsDo) Or(conds ...gen.Condition) *gameTicketLogsDo {
return g.withDO(g.DO.Or(conds...))
}
func (g gameTicketLogsDo) Select(conds ...field.Expr) *gameTicketLogsDo {
return g.withDO(g.DO.Select(conds...))
}
func (g gameTicketLogsDo) Where(conds ...gen.Condition) *gameTicketLogsDo {
return g.withDO(g.DO.Where(conds...))
}
func (g gameTicketLogsDo) Order(conds ...field.Expr) *gameTicketLogsDo {
return g.withDO(g.DO.Order(conds...))
}
func (g gameTicketLogsDo) Distinct(cols ...field.Expr) *gameTicketLogsDo {
return g.withDO(g.DO.Distinct(cols...))
}
func (g gameTicketLogsDo) Omit(cols ...field.Expr) *gameTicketLogsDo {
return g.withDO(g.DO.Omit(cols...))
}
func (g gameTicketLogsDo) Join(table schema.Tabler, on ...field.Expr) *gameTicketLogsDo {
return g.withDO(g.DO.Join(table, on...))
}
func (g gameTicketLogsDo) LeftJoin(table schema.Tabler, on ...field.Expr) *gameTicketLogsDo {
return g.withDO(g.DO.LeftJoin(table, on...))
}
func (g gameTicketLogsDo) RightJoin(table schema.Tabler, on ...field.Expr) *gameTicketLogsDo {
return g.withDO(g.DO.RightJoin(table, on...))
}
func (g gameTicketLogsDo) Group(cols ...field.Expr) *gameTicketLogsDo {
return g.withDO(g.DO.Group(cols...))
}
func (g gameTicketLogsDo) Having(conds ...gen.Condition) *gameTicketLogsDo {
return g.withDO(g.DO.Having(conds...))
}
func (g gameTicketLogsDo) Limit(limit int) *gameTicketLogsDo {
return g.withDO(g.DO.Limit(limit))
}
func (g gameTicketLogsDo) Offset(offset int) *gameTicketLogsDo {
return g.withDO(g.DO.Offset(offset))
}
func (g gameTicketLogsDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *gameTicketLogsDo {
return g.withDO(g.DO.Scopes(funcs...))
}
func (g gameTicketLogsDo) Unscoped() *gameTicketLogsDo {
return g.withDO(g.DO.Unscoped())
}
func (g gameTicketLogsDo) Create(values ...*model.GameTicketLogs) error {
if len(values) == 0 {
return nil
}
return g.DO.Create(values)
}
func (g gameTicketLogsDo) CreateInBatches(values []*model.GameTicketLogs, batchSize int) error {
return g.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 (g gameTicketLogsDo) Save(values ...*model.GameTicketLogs) error {
if len(values) == 0 {
return nil
}
return g.DO.Save(values)
}
func (g gameTicketLogsDo) First() (*model.GameTicketLogs, error) {
if result, err := g.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.GameTicketLogs), nil
}
}
func (g gameTicketLogsDo) Take() (*model.GameTicketLogs, error) {
if result, err := g.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.GameTicketLogs), nil
}
}
func (g gameTicketLogsDo) Last() (*model.GameTicketLogs, error) {
if result, err := g.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.GameTicketLogs), nil
}
}
func (g gameTicketLogsDo) Find() ([]*model.GameTicketLogs, error) {
result, err := g.DO.Find()
return result.([]*model.GameTicketLogs), err
}
func (g gameTicketLogsDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.GameTicketLogs, err error) {
buf := make([]*model.GameTicketLogs, 0, batchSize)
err = g.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 (g gameTicketLogsDo) FindInBatches(result *[]*model.GameTicketLogs, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return g.DO.FindInBatches(result, batchSize, fc)
}
func (g gameTicketLogsDo) Attrs(attrs ...field.AssignExpr) *gameTicketLogsDo {
return g.withDO(g.DO.Attrs(attrs...))
}
func (g gameTicketLogsDo) Assign(attrs ...field.AssignExpr) *gameTicketLogsDo {
return g.withDO(g.DO.Assign(attrs...))
}
func (g gameTicketLogsDo) Joins(fields ...field.RelationField) *gameTicketLogsDo {
for _, _f := range fields {
g = *g.withDO(g.DO.Joins(_f))
}
return &g
}
func (g gameTicketLogsDo) Preload(fields ...field.RelationField) *gameTicketLogsDo {
for _, _f := range fields {
g = *g.withDO(g.DO.Preload(_f))
}
return &g
}
func (g gameTicketLogsDo) FirstOrInit() (*model.GameTicketLogs, error) {
if result, err := g.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.GameTicketLogs), nil
}
}
func (g gameTicketLogsDo) FirstOrCreate() (*model.GameTicketLogs, error) {
if result, err := g.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.GameTicketLogs), nil
}
}
func (g gameTicketLogsDo) FindByPage(offset int, limit int) (result []*model.GameTicketLogs, count int64, err error) {
result, err = g.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 = g.Offset(-1).Limit(-1).Count()
return
}
func (g gameTicketLogsDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = g.Count()
if err != nil {
return
}
err = g.Offset(offset).Limit(limit).Scan(result)
return
}
func (g gameTicketLogsDo) Scan(result interface{}) (err error) {
return g.DO.Scan(result)
}
func (g gameTicketLogsDo) Delete(models ...*model.GameTicketLogs) (result gen.ResultInfo, err error) {
return g.DO.Delete(models)
}
func (g *gameTicketLogsDo) withDO(do gen.Dao) *gameTicketLogsDo {
g.DO = *do.(*gen.DO)
return g
}

View File

@ -27,6 +27,7 @@ var (
Admin *admin Admin *admin
Banner *banner Banner *banner
Channels *channels Channels *channels
GameTicketLogs *gameTicketLogs
IssuePositionClaims *issuePositionClaims IssuePositionClaims *issuePositionClaims
LogOperation *logOperation LogOperation *logOperation
LogRequest *logRequest LogRequest *logRequest
@ -60,6 +61,7 @@ var (
TaskCenterTasks *taskCenterTasks TaskCenterTasks *taskCenterTasks
UserAddresses *userAddresses UserAddresses *userAddresses
UserCoupons *userCoupons UserCoupons *userCoupons
UserGameTickets *userGameTickets
UserInventory *userInventory UserInventory *userInventory
UserInventoryTransfers *userInventoryTransfers UserInventoryTransfers *userInventoryTransfers
UserInvites *userInvites UserInvites *userInvites
@ -83,6 +85,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
Admin = &Q.Admin Admin = &Q.Admin
Banner = &Q.Banner Banner = &Q.Banner
Channels = &Q.Channels Channels = &Q.Channels
GameTicketLogs = &Q.GameTicketLogs
IssuePositionClaims = &Q.IssuePositionClaims IssuePositionClaims = &Q.IssuePositionClaims
LogOperation = &Q.LogOperation LogOperation = &Q.LogOperation
LogRequest = &Q.LogRequest LogRequest = &Q.LogRequest
@ -116,6 +119,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
TaskCenterTasks = &Q.TaskCenterTasks TaskCenterTasks = &Q.TaskCenterTasks
UserAddresses = &Q.UserAddresses UserAddresses = &Q.UserAddresses
UserCoupons = &Q.UserCoupons UserCoupons = &Q.UserCoupons
UserGameTickets = &Q.UserGameTickets
UserInventory = &Q.UserInventory UserInventory = &Q.UserInventory
UserInventoryTransfers = &Q.UserInventoryTransfers UserInventoryTransfers = &Q.UserInventoryTransfers
UserInvites = &Q.UserInvites UserInvites = &Q.UserInvites
@ -140,6 +144,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
Admin: newAdmin(db, opts...), Admin: newAdmin(db, opts...),
Banner: newBanner(db, opts...), Banner: newBanner(db, opts...),
Channels: newChannels(db, opts...), Channels: newChannels(db, opts...),
GameTicketLogs: newGameTicketLogs(db, opts...),
IssuePositionClaims: newIssuePositionClaims(db, opts...), IssuePositionClaims: newIssuePositionClaims(db, opts...),
LogOperation: newLogOperation(db, opts...), LogOperation: newLogOperation(db, opts...),
LogRequest: newLogRequest(db, opts...), LogRequest: newLogRequest(db, opts...),
@ -173,6 +178,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
TaskCenterTasks: newTaskCenterTasks(db, opts...), TaskCenterTasks: newTaskCenterTasks(db, opts...),
UserAddresses: newUserAddresses(db, opts...), UserAddresses: newUserAddresses(db, opts...),
UserCoupons: newUserCoupons(db, opts...), UserCoupons: newUserCoupons(db, opts...),
UserGameTickets: newUserGameTickets(db, opts...),
UserInventory: newUserInventory(db, opts...), UserInventory: newUserInventory(db, opts...),
UserInventoryTransfers: newUserInventoryTransfers(db, opts...), UserInventoryTransfers: newUserInventoryTransfers(db, opts...),
UserInvites: newUserInvites(db, opts...), UserInvites: newUserInvites(db, opts...),
@ -198,6 +204,7 @@ type Query struct {
Admin admin Admin admin
Banner banner Banner banner
Channels channels Channels channels
GameTicketLogs gameTicketLogs
IssuePositionClaims issuePositionClaims IssuePositionClaims issuePositionClaims
LogOperation logOperation LogOperation logOperation
LogRequest logRequest LogRequest logRequest
@ -231,6 +238,7 @@ type Query struct {
TaskCenterTasks taskCenterTasks TaskCenterTasks taskCenterTasks
UserAddresses userAddresses UserAddresses userAddresses
UserCoupons userCoupons UserCoupons userCoupons
UserGameTickets userGameTickets
UserInventory userInventory UserInventory userInventory
UserInventoryTransfers userInventoryTransfers UserInventoryTransfers userInventoryTransfers
UserInvites userInvites UserInvites userInvites
@ -257,6 +265,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
Admin: q.Admin.clone(db), Admin: q.Admin.clone(db),
Banner: q.Banner.clone(db), Banner: q.Banner.clone(db),
Channels: q.Channels.clone(db), Channels: q.Channels.clone(db),
GameTicketLogs: q.GameTicketLogs.clone(db),
IssuePositionClaims: q.IssuePositionClaims.clone(db), IssuePositionClaims: q.IssuePositionClaims.clone(db),
LogOperation: q.LogOperation.clone(db), LogOperation: q.LogOperation.clone(db),
LogRequest: q.LogRequest.clone(db), LogRequest: q.LogRequest.clone(db),
@ -290,6 +299,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
TaskCenterTasks: q.TaskCenterTasks.clone(db), TaskCenterTasks: q.TaskCenterTasks.clone(db),
UserAddresses: q.UserAddresses.clone(db), UserAddresses: q.UserAddresses.clone(db),
UserCoupons: q.UserCoupons.clone(db), UserCoupons: q.UserCoupons.clone(db),
UserGameTickets: q.UserGameTickets.clone(db),
UserInventory: q.UserInventory.clone(db), UserInventory: q.UserInventory.clone(db),
UserInventoryTransfers: q.UserInventoryTransfers.clone(db), UserInventoryTransfers: q.UserInventoryTransfers.clone(db),
UserInvites: q.UserInvites.clone(db), UserInvites: q.UserInvites.clone(db),
@ -323,6 +333,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
Admin: q.Admin.replaceDB(db), Admin: q.Admin.replaceDB(db),
Banner: q.Banner.replaceDB(db), Banner: q.Banner.replaceDB(db),
Channels: q.Channels.replaceDB(db), Channels: q.Channels.replaceDB(db),
GameTicketLogs: q.GameTicketLogs.replaceDB(db),
IssuePositionClaims: q.IssuePositionClaims.replaceDB(db), IssuePositionClaims: q.IssuePositionClaims.replaceDB(db),
LogOperation: q.LogOperation.replaceDB(db), LogOperation: q.LogOperation.replaceDB(db),
LogRequest: q.LogRequest.replaceDB(db), LogRequest: q.LogRequest.replaceDB(db),
@ -356,6 +367,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
TaskCenterTasks: q.TaskCenterTasks.replaceDB(db), TaskCenterTasks: q.TaskCenterTasks.replaceDB(db),
UserAddresses: q.UserAddresses.replaceDB(db), UserAddresses: q.UserAddresses.replaceDB(db),
UserCoupons: q.UserCoupons.replaceDB(db), UserCoupons: q.UserCoupons.replaceDB(db),
UserGameTickets: q.UserGameTickets.replaceDB(db),
UserInventory: q.UserInventory.replaceDB(db), UserInventory: q.UserInventory.replaceDB(db),
UserInventoryTransfers: q.UserInventoryTransfers.replaceDB(db), UserInventoryTransfers: q.UserInventoryTransfers.replaceDB(db),
UserInvites: q.UserInvites.replaceDB(db), UserInvites: q.UserInvites.replaceDB(db),
@ -379,6 +391,7 @@ type queryCtx struct {
Admin *adminDo Admin *adminDo
Banner *bannerDo Banner *bannerDo
Channels *channelsDo Channels *channelsDo
GameTicketLogs *gameTicketLogsDo
IssuePositionClaims *issuePositionClaimsDo IssuePositionClaims *issuePositionClaimsDo
LogOperation *logOperationDo LogOperation *logOperationDo
LogRequest *logRequestDo LogRequest *logRequestDo
@ -412,6 +425,7 @@ type queryCtx struct {
TaskCenterTasks *taskCenterTasksDo TaskCenterTasks *taskCenterTasksDo
UserAddresses *userAddressesDo UserAddresses *userAddressesDo
UserCoupons *userCouponsDo UserCoupons *userCouponsDo
UserGameTickets *userGameTicketsDo
UserInventory *userInventoryDo UserInventory *userInventoryDo
UserInventoryTransfers *userInventoryTransfersDo UserInventoryTransfers *userInventoryTransfersDo
UserInvites *userInvitesDo UserInvites *userInvitesDo
@ -435,6 +449,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
Admin: q.Admin.WithContext(ctx), Admin: q.Admin.WithContext(ctx),
Banner: q.Banner.WithContext(ctx), Banner: q.Banner.WithContext(ctx),
Channels: q.Channels.WithContext(ctx), Channels: q.Channels.WithContext(ctx),
GameTicketLogs: q.GameTicketLogs.WithContext(ctx),
IssuePositionClaims: q.IssuePositionClaims.WithContext(ctx), IssuePositionClaims: q.IssuePositionClaims.WithContext(ctx),
LogOperation: q.LogOperation.WithContext(ctx), LogOperation: q.LogOperation.WithContext(ctx),
LogRequest: q.LogRequest.WithContext(ctx), LogRequest: q.LogRequest.WithContext(ctx),
@ -468,6 +483,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
TaskCenterTasks: q.TaskCenterTasks.WithContext(ctx), TaskCenterTasks: q.TaskCenterTasks.WithContext(ctx),
UserAddresses: q.UserAddresses.WithContext(ctx), UserAddresses: q.UserAddresses.WithContext(ctx),
UserCoupons: q.UserCoupons.WithContext(ctx), UserCoupons: q.UserCoupons.WithContext(ctx),
UserGameTickets: q.UserGameTickets.WithContext(ctx),
UserInventory: q.UserInventory.WithContext(ctx), UserInventory: q.UserInventory.WithContext(ctx),
UserInventoryTransfers: q.UserInventoryTransfers.WithContext(ctx), UserInventoryTransfers: q.UserInventoryTransfers.WithContext(ctx),
UserInvites: q.UserInvites.WithContext(ctx), UserInvites: q.UserInvites.WithContext(ctx),

View File

@ -0,0 +1,348 @@
// 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 newUserGameTickets(db *gorm.DB, opts ...gen.DOOption) userGameTickets {
_userGameTickets := userGameTickets{}
_userGameTickets.userGameTicketsDo.UseDB(db, opts...)
_userGameTickets.userGameTicketsDo.UseModel(&model.UserGameTickets{})
tableName := _userGameTickets.userGameTicketsDo.TableName()
_userGameTickets.ALL = field.NewAsterisk(tableName)
_userGameTickets.ID = field.NewInt64(tableName, "id")
_userGameTickets.CreatedAt = field.NewTime(tableName, "created_at")
_userGameTickets.UpdatedAt = field.NewTime(tableName, "updated_at")
_userGameTickets.UserID = field.NewInt64(tableName, "user_id")
_userGameTickets.GameCode = field.NewString(tableName, "game_code")
_userGameTickets.Available = field.NewInt32(tableName, "available")
_userGameTickets.TotalEarned = field.NewInt32(tableName, "total_earned")
_userGameTickets.TotalUsed = field.NewInt32(tableName, "total_used")
_userGameTickets.fillFieldMap()
return _userGameTickets
}
// userGameTickets 用户游戏资格
type userGameTickets struct {
userGameTicketsDo
ALL field.Asterisk
ID field.Int64
CreatedAt field.Time
UpdatedAt field.Time
UserID field.Int64 // 用户ID
GameCode field.String // 游戏代码
Available field.Int32 // 可用次数
TotalEarned field.Int32 // 累计获得
TotalUsed field.Int32 // 累计使用
fieldMap map[string]field.Expr
}
func (u userGameTickets) Table(newTableName string) *userGameTickets {
u.userGameTicketsDo.UseTable(newTableName)
return u.updateTableName(newTableName)
}
func (u userGameTickets) As(alias string) *userGameTickets {
u.userGameTicketsDo.DO = *(u.userGameTicketsDo.As(alias).(*gen.DO))
return u.updateTableName(alias)
}
func (u *userGameTickets) updateTableName(table string) *userGameTickets {
u.ALL = field.NewAsterisk(table)
u.ID = field.NewInt64(table, "id")
u.CreatedAt = field.NewTime(table, "created_at")
u.UpdatedAt = field.NewTime(table, "updated_at")
u.UserID = field.NewInt64(table, "user_id")
u.GameCode = field.NewString(table, "game_code")
u.Available = field.NewInt32(table, "available")
u.TotalEarned = field.NewInt32(table, "total_earned")
u.TotalUsed = field.NewInt32(table, "total_used")
u.fillFieldMap()
return u
}
func (u *userGameTickets) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := u.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (u *userGameTickets) fillFieldMap() {
u.fieldMap = make(map[string]field.Expr, 8)
u.fieldMap["id"] = u.ID
u.fieldMap["created_at"] = u.CreatedAt
u.fieldMap["updated_at"] = u.UpdatedAt
u.fieldMap["user_id"] = u.UserID
u.fieldMap["game_code"] = u.GameCode
u.fieldMap["available"] = u.Available
u.fieldMap["total_earned"] = u.TotalEarned
u.fieldMap["total_used"] = u.TotalUsed
}
func (u userGameTickets) clone(db *gorm.DB) userGameTickets {
u.userGameTicketsDo.ReplaceConnPool(db.Statement.ConnPool)
return u
}
func (u userGameTickets) replaceDB(db *gorm.DB) userGameTickets {
u.userGameTicketsDo.ReplaceDB(db)
return u
}
type userGameTicketsDo struct{ gen.DO }
func (u userGameTicketsDo) Debug() *userGameTicketsDo {
return u.withDO(u.DO.Debug())
}
func (u userGameTicketsDo) WithContext(ctx context.Context) *userGameTicketsDo {
return u.withDO(u.DO.WithContext(ctx))
}
func (u userGameTicketsDo) ReadDB() *userGameTicketsDo {
return u.Clauses(dbresolver.Read)
}
func (u userGameTicketsDo) WriteDB() *userGameTicketsDo {
return u.Clauses(dbresolver.Write)
}
func (u userGameTicketsDo) Session(config *gorm.Session) *userGameTicketsDo {
return u.withDO(u.DO.Session(config))
}
func (u userGameTicketsDo) Clauses(conds ...clause.Expression) *userGameTicketsDo {
return u.withDO(u.DO.Clauses(conds...))
}
func (u userGameTicketsDo) Returning(value interface{}, columns ...string) *userGameTicketsDo {
return u.withDO(u.DO.Returning(value, columns...))
}
func (u userGameTicketsDo) Not(conds ...gen.Condition) *userGameTicketsDo {
return u.withDO(u.DO.Not(conds...))
}
func (u userGameTicketsDo) Or(conds ...gen.Condition) *userGameTicketsDo {
return u.withDO(u.DO.Or(conds...))
}
func (u userGameTicketsDo) Select(conds ...field.Expr) *userGameTicketsDo {
return u.withDO(u.DO.Select(conds...))
}
func (u userGameTicketsDo) Where(conds ...gen.Condition) *userGameTicketsDo {
return u.withDO(u.DO.Where(conds...))
}
func (u userGameTicketsDo) Order(conds ...field.Expr) *userGameTicketsDo {
return u.withDO(u.DO.Order(conds...))
}
func (u userGameTicketsDo) Distinct(cols ...field.Expr) *userGameTicketsDo {
return u.withDO(u.DO.Distinct(cols...))
}
func (u userGameTicketsDo) Omit(cols ...field.Expr) *userGameTicketsDo {
return u.withDO(u.DO.Omit(cols...))
}
func (u userGameTicketsDo) Join(table schema.Tabler, on ...field.Expr) *userGameTicketsDo {
return u.withDO(u.DO.Join(table, on...))
}
func (u userGameTicketsDo) LeftJoin(table schema.Tabler, on ...field.Expr) *userGameTicketsDo {
return u.withDO(u.DO.LeftJoin(table, on...))
}
func (u userGameTicketsDo) RightJoin(table schema.Tabler, on ...field.Expr) *userGameTicketsDo {
return u.withDO(u.DO.RightJoin(table, on...))
}
func (u userGameTicketsDo) Group(cols ...field.Expr) *userGameTicketsDo {
return u.withDO(u.DO.Group(cols...))
}
func (u userGameTicketsDo) Having(conds ...gen.Condition) *userGameTicketsDo {
return u.withDO(u.DO.Having(conds...))
}
func (u userGameTicketsDo) Limit(limit int) *userGameTicketsDo {
return u.withDO(u.DO.Limit(limit))
}
func (u userGameTicketsDo) Offset(offset int) *userGameTicketsDo {
return u.withDO(u.DO.Offset(offset))
}
func (u userGameTicketsDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *userGameTicketsDo {
return u.withDO(u.DO.Scopes(funcs...))
}
func (u userGameTicketsDo) Unscoped() *userGameTicketsDo {
return u.withDO(u.DO.Unscoped())
}
func (u userGameTicketsDo) Create(values ...*model.UserGameTickets) error {
if len(values) == 0 {
return nil
}
return u.DO.Create(values)
}
func (u userGameTicketsDo) CreateInBatches(values []*model.UserGameTickets, batchSize int) error {
return u.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 (u userGameTicketsDo) Save(values ...*model.UserGameTickets) error {
if len(values) == 0 {
return nil
}
return u.DO.Save(values)
}
func (u userGameTicketsDo) First() (*model.UserGameTickets, error) {
if result, err := u.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.UserGameTickets), nil
}
}
func (u userGameTicketsDo) Take() (*model.UserGameTickets, error) {
if result, err := u.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.UserGameTickets), nil
}
}
func (u userGameTicketsDo) Last() (*model.UserGameTickets, error) {
if result, err := u.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.UserGameTickets), nil
}
}
func (u userGameTicketsDo) Find() ([]*model.UserGameTickets, error) {
result, err := u.DO.Find()
return result.([]*model.UserGameTickets), err
}
func (u userGameTicketsDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.UserGameTickets, err error) {
buf := make([]*model.UserGameTickets, 0, batchSize)
err = u.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 (u userGameTicketsDo) FindInBatches(result *[]*model.UserGameTickets, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return u.DO.FindInBatches(result, batchSize, fc)
}
func (u userGameTicketsDo) Attrs(attrs ...field.AssignExpr) *userGameTicketsDo {
return u.withDO(u.DO.Attrs(attrs...))
}
func (u userGameTicketsDo) Assign(attrs ...field.AssignExpr) *userGameTicketsDo {
return u.withDO(u.DO.Assign(attrs...))
}
func (u userGameTicketsDo) Joins(fields ...field.RelationField) *userGameTicketsDo {
for _, _f := range fields {
u = *u.withDO(u.DO.Joins(_f))
}
return &u
}
func (u userGameTicketsDo) Preload(fields ...field.RelationField) *userGameTicketsDo {
for _, _f := range fields {
u = *u.withDO(u.DO.Preload(_f))
}
return &u
}
func (u userGameTicketsDo) FirstOrInit() (*model.UserGameTickets, error) {
if result, err := u.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.UserGameTickets), nil
}
}
func (u userGameTicketsDo) FirstOrCreate() (*model.UserGameTickets, error) {
if result, err := u.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.UserGameTickets), nil
}
}
func (u userGameTicketsDo) FindByPage(offset int, limit int) (result []*model.UserGameTickets, count int64, err error) {
result, err = u.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 = u.Offset(-1).Limit(-1).Count()
return
}
func (u userGameTicketsDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = u.Count()
if err != nil {
return
}
err = u.Offset(offset).Limit(limit).Scan(result)
return
}
func (u userGameTicketsDo) Scan(result interface{}) (err error) {
return u.DO.Scan(result)
}
func (u userGameTicketsDo) Delete(models ...*model.UserGameTickets) (result gen.ResultInfo, err error) {
return u.DO.Delete(models)
}
func (u *userGameTicketsDo) withDO(do gen.Dao) *userGameTicketsDo {
u.DO = *do.(*gen.DO)
return u
}

View File

@ -0,0 +1,30 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameGameTicketLogs = "game_ticket_logs"
// GameTicketLogs 游戏资格变动日志
type GameTicketLogs struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP(3)" json:"created_at"`
UserID int64 `gorm:"column:user_id;not null;comment:用户ID" json:"user_id"` // 用户ID
GameCode string `gorm:"column:game_code;not null;comment:游戏代码" json:"game_code"` // 游戏代码
ChangeType int32 `gorm:"column:change_type;not null;comment:1=获得 2=使用" json:"change_type"` // 1=获得 2=使用
Amount int32 `gorm:"column:amount;not null;comment:变动数量" json:"amount"` // 变动数量
Balance int32 `gorm:"column:balance;not null;comment:变动后余额" json:"balance"` // 变动后余额
Source string `gorm:"column:source;comment:来源: order/task/admin" json:"source"` // 来源: order/task/admin
SourceID int64 `gorm:"column:source_id;comment:来源ID" json:"source_id"` // 来源ID
Remark string `gorm:"column:remark;comment:备注" json:"remark"` // 备注
}
// TableName GameTicketLogs's table name
func (*GameTicketLogs) TableName() string {
return TableNameGameTicketLogs
}

View File

@ -0,0 +1,28 @@
// 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 TableNameUserGameTickets = "user_game_tickets"
// UserGameTickets 用户游戏资格
type UserGameTickets struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP(3)" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP(3)" json:"updated_at"`
UserID int64 `gorm:"column:user_id;not null;comment:用户ID" json:"user_id"` // 用户ID
GameCode string `gorm:"column:game_code;not null;default:minesweeper;comment:游戏代码" json:"game_code"` // 游戏代码
Available int32 `gorm:"column:available;not null;comment:可用次数" json:"available"` // 可用次数
TotalEarned int32 `gorm:"column:total_earned;not null;comment:累计获得" json:"total_earned"` // 累计获得
TotalUsed int32 `gorm:"column:total_used;not null;comment:累计使用" json:"total_used"` // 累计使用
}
// TableName UserGameTickets's table name
func (*UserGameTickets) TableName() string {
return TableNameUserGameTickets
}

View File

@ -6,6 +6,7 @@ import (
"bindbox-game/internal/api/admin" "bindbox-game/internal/api/admin"
appapi "bindbox-game/internal/api/app" appapi "bindbox-game/internal/api/app"
commonapi "bindbox-game/internal/api/common" commonapi "bindbox-game/internal/api/common"
gameapi "bindbox-game/internal/api/game"
payapi "bindbox-game/internal/api/pay" payapi "bindbox-game/internal/api/pay"
taskcenterapi "bindbox-game/internal/api/task_center" taskcenterapi "bindbox-game/internal/api/task_center"
userapi "bindbox-game/internal/api/user" userapi "bindbox-game/internal/api/user"
@ -65,16 +66,17 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
userHandler := userapi.New(logger, db) userHandler := userapi.New(logger, db)
commonHandler := commonapi.New(logger, db) commonHandler := commonapi.New(logger, db)
payHandler := payapi.New(logger, db, taskSvc) payHandler := payapi.New(logger, db, taskSvc)
// minesweeperHandler := minesweeperapi.New(logger, db) gameHandler := gameapi.New(logger, db, rdb, userSvc)
intc := interceptor.New(logger, db) intc := interceptor.New(logger, db)
// 内部服务接口路由组 (供 Nakama 调用) // 内部服务接口路由组 (供 Nakama 调用)
// internalRouter := mux.Group("/internal") internalRouter := mux.Group("/internal")
// { {
// TODO: 添加IP白名单或Internal-Key验证中间件 // TODO: 添加IP白名单或Internal-Key验证中间件
// internalRouter.POST("/game/verify", minesweeperHandler.VerifyTicket()) internalRouter.POST("/game/verify", gameHandler.VerifyTicket())
// internalRouter.POST("/game/settle", minesweeperHandler.SettleGame()) internalRouter.POST("/game/settle", gameHandler.SettleGame())
// } internalRouter.GET("/game/minesweeper/config", gameHandler.GetMinesweeperConfig())
}
// 管理端非认证接口路由组 // 管理端非认证接口路由组
adminNonAuthApiRouter := mux.Group("/api/admin") adminNonAuthApiRouter := mux.Group("/api/admin")
@ -224,6 +226,10 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
adminAuthApiRouter.PUT("/matching_card_types/:id", adminHandler.ModifyMatchingCardType()) adminAuthApiRouter.PUT("/matching_card_types/:id", adminHandler.ModifyMatchingCardType())
adminAuthApiRouter.DELETE("/matching_card_types/:id", adminHandler.DeleteMatchingCardType()) adminAuthApiRouter.DELETE("/matching_card_types/:id", adminHandler.DeleteMatchingCardType())
// 游戏资格管理
adminAuthApiRouter.GET("/users/:user_id/game_tickets", gameHandler.ListUserTickets())
adminAuthApiRouter.POST("/users/:user_id/game_tickets", gameHandler.GrantUserTicket())
// 发货统计 // 发货统计
adminAuthApiRouter.GET("/ops_shipping_stats", intc.RequireAdminAction("ops:shipping:view"), adminHandler.ListShippingStats()) adminAuthApiRouter.GET("/ops_shipping_stats", intc.RequireAdminAction("ops:shipping:view"), adminHandler.ListShippingStats())
adminAuthApiRouter.GET("/ops_shipping_stats/:id", intc.RequireAdminAction("ops:shipping:view"), adminHandler.GetShippingStat()) adminAuthApiRouter.GET("/ops_shipping_stats/:id", intc.RequireAdminAction("ops:shipping:view"), adminHandler.GetShippingStat())
@ -351,6 +357,10 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo) (core.Mux, func(), er
appAuthApiRouter.POST("/matching/check", activityHandler.CheckMatchingGame()) appAuthApiRouter.POST("/matching/check", activityHandler.CheckMatchingGame())
appAuthApiRouter.GET("/matching/state", activityHandler.GetMatchingGameState()) appAuthApiRouter.GET("/matching/state", activityHandler.GetMatchingGameState())
// 扫雷游戏
appAuthApiRouter.GET("/users/:user_id/game_tickets", gameHandler.GetMyTickets())
appAuthApiRouter.POST("/games/enter", gameHandler.EnterGame())
appAuthApiRouter.POST("/users/:user_id/inventory/address-share/create", userHandler.CreateAddressShare()) appAuthApiRouter.POST("/users/:user_id/inventory/address-share/create", userHandler.CreateAddressShare())
appAuthApiRouter.POST("/users/:user_id/inventory/address-share/revoke", userHandler.RevokeAddressShare()) appAuthApiRouter.POST("/users/:user_id/inventory/address-share/revoke", userHandler.RevokeAddressShare())
appAuthApiRouter.POST("/users/:user_id/inventory/request-shipping", userHandler.RequestShippingBatch()) appAuthApiRouter.POST("/users/:user_id/inventory/request-shipping", userHandler.RequestShippingBatch())

View File

@ -0,0 +1,190 @@
package game
import (
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/dao"
"bindbox-game/internal/repository/mysql/model"
"context"
"fmt"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// TicketService 游戏资格服务
type TicketService interface {
// GrantTicket 发放游戏资格
GrantTicket(ctx context.Context, userID int64, gameCode string, amount int, source string, sourceID int64, remark string) error
// UseTicket 使用游戏资格
UseTicket(ctx context.Context, userID int64, gameCode string) error
// GetUserTickets 获取用户资格
GetUserTickets(ctx context.Context, userID int64) (map[string]int, error)
// GetUserTicketByGame 获取用户指定游戏资格
GetUserTicketByGame(ctx context.Context, userID int64, gameCode string) (*model.UserGameTickets, error)
// GetTicketLogs 获取用户资格变动日志
GetTicketLogs(ctx context.Context, userID int64, page, pageSize int) ([]*model.GameTicketLogs, int64, error)
}
type ticketService struct {
logger logger.CustomLogger
readDB *dao.Query
writeDB *dao.Query
repo mysql.Repo
}
// NewTicketService 创建资格服务
func NewTicketService(l logger.CustomLogger, db mysql.Repo) TicketService {
return &ticketService{
logger: l,
readDB: dao.Use(db.GetDbR()),
writeDB: dao.Use(db.GetDbW()),
repo: db,
}
}
// GrantTicket 发放游戏资格
func (s *ticketService) GrantTicket(ctx context.Context, userID int64, gameCode string, amount int, source string, sourceID int64, remark string) error {
if amount <= 0 {
return fmt.Errorf("amount must be positive")
}
return s.repo.GetDbW().Transaction(func(tx *gorm.DB) error {
// Upsert user_game_tickets
ticket := &model.UserGameTickets{
UserID: userID,
GameCode: gameCode,
Available: int32(amount),
TotalEarned: int32(amount),
TotalUsed: 0,
}
err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "game_code"}},
DoUpdates: clause.Assignments(map[string]interface{}{
"available": gorm.Expr("available + ?", amount),
"total_earned": gorm.Expr("total_earned + ?", amount),
"updated_at": time.Now(),
}),
}).Create(ticket).Error
if err != nil {
return err
}
// 查询变动后余额
var balance int32
tx.Model(&model.UserGameTickets{}).
Where("user_id = ? AND game_code = ?", userID, gameCode).
Pluck("available", &balance)
// 记录日志
log := &model.GameTicketLogs{
UserID: userID,
GameCode: gameCode,
ChangeType: 1, // 获得
Amount: int32(amount),
Balance: balance,
Source: source,
SourceID: sourceID,
Remark: remark,
}
return tx.Create(log).Error
})
}
// UseTicket 使用游戏资格
func (s *ticketService) UseTicket(ctx context.Context, userID int64, gameCode string) error {
return s.repo.GetDbW().Transaction(func(tx *gorm.DB) error {
// 检查并扣减
result := tx.Model(&model.UserGameTickets{}).
Where("user_id = ? AND game_code = ? AND available > 0", userID, gameCode).
Updates(map[string]interface{}{
"available": gorm.Expr("available - 1"),
"total_used": gorm.Expr("total_used + 1"),
"updated_at": time.Now(),
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("insufficient game tickets")
}
// 查询变动后余额
var balance int32
tx.Model(&model.UserGameTickets{}).
Where("user_id = ? AND game_code = ?", userID, gameCode).
Pluck("available", &balance)
// 记录日志
log := &model.GameTicketLogs{
UserID: userID,
GameCode: gameCode,
ChangeType: 2, // 使用
Amount: 1,
Balance: balance,
Source: "game_enter",
Remark: "进入游戏",
}
return tx.Create(log).Error
})
}
// GetUserTickets 获取用户所有游戏资格
func (s *ticketService) GetUserTickets(ctx context.Context, userID int64) (map[string]int, error) {
var tickets []*model.UserGameTickets
err := s.repo.GetDbR().WithContext(ctx).
Where("user_id = ?", userID).
Find(&tickets).Error
if err != nil {
s.logger.Error("GetUserTickets failed", zap.Int64("user_id", userID), zap.Error(err))
return nil, err
}
result := make(map[string]int)
for _, t := range tickets {
result[t.GameCode] = int(t.Available)
}
return result, nil
}
// GetUserTicketByGame 获取用户指定游戏资格
func (s *ticketService) GetUserTicketByGame(ctx context.Context, userID int64, gameCode string) (*model.UserGameTickets, error) {
var ticket model.UserGameTickets
err := s.repo.GetDbR().WithContext(ctx).
Where("user_id = ? AND game_code = ?", userID, gameCode).
First(&ticket).Error
if err != nil {
return nil, err
}
return &ticket, nil
}
// GetTicketLogs 获取用户游戏资格变动日志
func (s *ticketService) GetTicketLogs(ctx context.Context, userID int64, page, pageSize int) ([]*model.GameTicketLogs, int64, error) {
var logs []*model.GameTicketLogs
var total int64
db := s.repo.GetDbR().WithContext(ctx).Model(&model.GameTicketLogs{}).Where("user_id = ?", userID)
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
offset := (page - 1) * pageSize
if err := db.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&logs).Error; err != nil {
return nil, 0, err
}
return logs, total, nil
}

View File

@ -12,6 +12,7 @@ import (
"fmt" "fmt"
"time" "time"
gamesvc "bindbox-game/internal/service/game"
titlesvc "bindbox-game/internal/service/title" titlesvc "bindbox-game/internal/service/title"
usersvc "bindbox-game/internal/service/user" usersvc "bindbox-game/internal/service/user"
@ -665,6 +666,16 @@ func (s *service) grantTierRewards(ctx context.Context, taskID int64, tierID int
if pl.TitleID > 0 { if pl.TitleID > 0 {
err = s.titleSvc.AssignUserTitle(ctx, userID, pl.TitleID, nil, "task_center") err = s.titleSvc.AssignUserTitle(ctx, userID, pl.TitleID, nil, "task_center")
} }
case "game_ticket":
var pl struct {
GameCode string `json:"game_code"`
Amount int `json:"amount"`
}
_ = json.Unmarshal([]byte(r.RewardPayload), &pl)
if pl.GameCode != "" && pl.Amount > 0 {
gameSvc := gamesvc.NewTicketService(s.logger, s.repo)
err = gameSvc.GrantTicket(ctx, userID, pl.GameCode, pl.Amount, "task_center", taskID, "任务奖励")
}
} }
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()

View File

@ -142,15 +142,15 @@ func generateBatchNo(userID int64) string {
return fmt.Sprintf("B%d%d", userID, time.Now().UnixNano()/1000000) return fmt.Sprintf("B%d%d", userID, time.Now().UnixNano()/1000000)
} }
func (s *service) RequestShippings(ctx context.Context, userID int64, inventoryIDs []int64, addressID *int64) (int64, []int64, []struct { func (s *service) RequestShippings(ctx context.Context, userID int64, inventoryIDs []int64, addressID *int64) (addrID int64, batchNo string, success []int64, skipped []struct {
ID int64 ID int64
Reason string Reason string
}, []struct { }, failed []struct {
ID int64 ID int64
Reason string Reason string
}, error) { }, err error) {
if len(inventoryIDs) == 0 { if len(inventoryIDs) == 0 {
return 0, nil, nil, []struct { return 0, "", nil, nil, []struct {
ID int64 ID int64
Reason string Reason string
}{{ID: 0, Reason: "invalid_params"}}, nil }{{ID: 0, Reason: "invalid_params"}}, nil
@ -166,25 +166,24 @@ func (s *service) RequestShippings(ctx context.Context, userID int64, inventoryI
} }
} }
if len(uniq) == 0 { if len(uniq) == 0 {
return 0, nil, nil, []struct { return 0, "", nil, nil, []struct {
ID int64 ID int64
Reason string Reason string
}{{ID: 0, Reason: "invalid_params"}}, nil }{{ID: 0, Reason: "invalid_params"}}, nil
} }
var addrID int64
if addressID != nil && *addressID > 0 { if addressID != nil && *addressID > 0 {
ua, _ := s.readDB.UserAddresses.WithContext(ctx).Where(s.readDB.UserAddresses.ID.Eq(*addressID), s.readDB.UserAddresses.UserID.Eq(userID)).First() ua, _ := s.readDB.UserAddresses.WithContext(ctx).Where(s.readDB.UserAddresses.ID.Eq(*addressID), s.readDB.UserAddresses.UserID.Eq(userID)).First()
if ua == nil { if ua == nil {
return 0, nil, nil, []struct { return 0, "", nil, nil, []struct {
ID int64 ID int64
Reason string Reason string
}{{ID: 0, Reason: "address_not_found"}}, nil }{{ID: 0, Reason: "address_not_found"}}, nil
} }
addrID = ua.ID addrID = ua.ID
} else { } else {
da, err := s.readDB.UserAddresses.WithContext(ctx).Where(s.readDB.UserAddresses.UserID.Eq(userID), s.readDB.UserAddresses.IsDefault.Eq(1)).First() da, e := s.readDB.UserAddresses.WithContext(ctx).Where(s.readDB.UserAddresses.UserID.Eq(userID), s.readDB.UserAddresses.IsDefault.Eq(1)).First()
if err != nil || da == nil { if e != nil || da == nil {
return 0, nil, nil, []struct { return 0, "", nil, nil, []struct {
ID int64 ID int64
Reason string Reason string
}{{ID: 0, Reason: "no_default_address"}}, nil }{{ID: 0, Reason: "no_default_address"}}, nil
@ -192,18 +191,15 @@ func (s *service) RequestShippings(ctx context.Context, userID int64, inventoryI
addrID = da.ID addrID = da.ID
} }
// 生成批次号(只有多个有效项时才生成) // 始终生成批次号,方便用户查询和管理
batchNo := "" batchNo = generateBatchNo(userID)
if len(uniq) > 1 {
batchNo = generateBatchNo(userID)
}
success := make([]int64, 0, len(uniq)) success = make([]int64, 0, len(uniq))
skipped := make([]struct { skipped = make([]struct {
ID int64 ID int64
Reason string Reason string
}, 0) }, 0)
failed := make([]struct { failed = make([]struct {
ID int64 ID int64
Reason string Reason string
}, 0) }, 0)
@ -246,7 +242,7 @@ func (s *service) RequestShippings(ctx context.Context, userID int64, inventoryI
} }
success = append(success, id) success = append(success, id)
} }
return addrID, success, skipped, failed, nil return addrID, batchNo, success, skipped, failed, nil
} }
func (s *service) RedeemInventoryToPoints(ctx context.Context, userID int64, inventoryID int64) (int64, error) { func (s *service) RedeemInventoryToPoints(ctx context.Context, userID int64, inventoryID int64) (int64, error) {

View File

@ -7,40 +7,84 @@ import (
) )
// CancelShipping 取消发货申请 // CancelShipping 取消发货申请
func (s *service) CancelShipping(ctx context.Context, userID int64, inventoryID int64) error { // 支持按单个资产ID取消或按批次号批量取消
// 1. 开启事务 // 返回成功取消的记录数
return s.writeDB.Transaction(func(tx *dao.Query) error { func (s *service) CancelShipping(ctx context.Context, userID int64, inventoryID int64, batchNo string) (int64, error) {
// 2. 查询发货记录(必须是待发货状态 status=1 var cancelledCount int64
sr, err := tx.ShippingRecords.WithContext(ctx).
Where(tx.ShippingRecords.InventoryID.Eq(inventoryID)).
Where(tx.ShippingRecords.UserID.Eq(userID)).
Where(tx.ShippingRecords.Status.Eq(1)).
First()
if err != nil { err := s.writeDB.Transaction(func(tx *dao.Query) error {
return fmt.Errorf("shipping record not found or already processed") var records []*struct {
ID int64
InventoryID int64
} }
// 3. 更新发货记录状态为已取消 (status=5) // 根据参数查询待取消的发货记录
if _, err := tx.ShippingRecords.WithContext(ctx). if batchNo != "" {
Where(tx.ShippingRecords.ID.Eq(sr.ID)). // 按批次号查询
Update(tx.ShippingRecords.Status, 5); err != nil { rows, err := tx.ShippingRecords.WithContext(ctx).
return err Select(tx.ShippingRecords.ID, tx.ShippingRecords.InventoryID).
Where(tx.ShippingRecords.BatchNo.Eq(batchNo)).
Where(tx.ShippingRecords.UserID.Eq(userID)).
Where(tx.ShippingRecords.Status.Eq(1)). // 待发货状态
Find()
if err != nil {
return fmt.Errorf("query shipping records failed: %w", err)
}
for _, r := range rows {
records = append(records, &struct {
ID int64
InventoryID int64
}{ID: r.ID, InventoryID: r.InventoryID})
}
} else if inventoryID > 0 {
// 按单个资产ID查询
sr, err := tx.ShippingRecords.WithContext(ctx).
Where(tx.ShippingRecords.InventoryID.Eq(inventoryID)).
Where(tx.ShippingRecords.UserID.Eq(userID)).
Where(tx.ShippingRecords.Status.Eq(1)).
First()
if err != nil {
return fmt.Errorf("shipping record not found or already processed")
}
records = append(records, &struct {
ID int64
InventoryID int64
}{ID: sr.ID, InventoryID: sr.InventoryID})
} }
// 4. 恢复库存状态为可用 (status=1) if len(records) == 0 {
// 并追加备注 return fmt.Errorf("no pending shipping records found")
// 使用原生SQL以确保CONCAT行为一致 }
remark := fmt.Sprintf("|shipping_cancelled_by_user:%d", userID)
if err := tx.UserInventory.WithContext(ctx).UnderlyingDB().Exec( // 批量处理每条记录
"UPDATE user_inventory SET status=1, remark=CONCAT(IFNULL(remark,''), ?) WHERE id=? AND user_id=?", for _, rec := range records {
remark, // 更新发货记录状态为已取消 (status=5)
inventoryID, if _, err := tx.ShippingRecords.WithContext(ctx).
userID, Where(tx.ShippingRecords.ID.Eq(rec.ID)).
).Error; err != nil { Update(tx.ShippingRecords.Status, 5); err != nil {
return err return err
}
// 恢复库存状态为可用 (status=1)
remark := fmt.Sprintf("|shipping_cancelled_by_user:%d", userID)
if err := tx.UserInventory.WithContext(ctx).UnderlyingDB().Exec(
"UPDATE user_inventory SET status=1, remark=CONCAT(IFNULL(remark,''), ?) WHERE id=? AND user_id=?",
remark,
rec.InventoryID,
userID,
).Error; err != nil {
return err
}
cancelledCount++
} }
return nil return nil
}) })
if err != nil {
return 0, err
}
return cancelledCount, nil
} }

View File

@ -1,16 +1,15 @@
package user package user
import ( import (
"context" "context"
"testing" "testing"
) )
func TestRequestShippings_DedupAndSkip(t *testing.T) { func TestRequestShippings_DedupAndSkip(t *testing.T) {
s := New(nil, nil) s := New(nil, nil)
// s.readDB/s.writeDB are nil in this placeholder; this test is a placeholder to ensure compilation. // s.readDB/s.writeDB are nil in this placeholder; this test is a placeholder to ensure compilation.
_, _, _, _, err := s.RequestShippings(context.Background(), 1, []int64{0, 1, 1}, nil) _, _, _, _, _, err := s.RequestShippings(context.Background(), 1, []int64{0, 1, 1}, nil)
if err != nil { if err != nil {
// no real DB; just ensure function can be called // no real DB; just ensure function can be called
} }
} }

View File

@ -50,14 +50,14 @@ type Service interface {
RevokeAddressShare(ctx context.Context, userID int64, inventoryID int64) error RevokeAddressShare(ctx context.Context, userID int64, inventoryID int64) error
SubmitAddressShare(ctx context.Context, shareToken string, name string, mobile string, province string, city string, district string, address string, submittedByUserID *int64, submittedIP *string) (int64, error) SubmitAddressShare(ctx context.Context, shareToken string, name string, mobile string, province string, city string, district string, address string, submittedByUserID *int64, submittedIP *string) (int64, error)
RequestShipping(ctx context.Context, userID int64, inventoryID int64) (int64, error) RequestShipping(ctx context.Context, userID int64, inventoryID int64) (int64, error)
CancelShipping(ctx context.Context, userID int64, inventoryID int64) error CancelShipping(ctx context.Context, userID int64, inventoryID int64, batchNo string) (int64, error)
RequestShippings(ctx context.Context, userID int64, inventoryIDs []int64, addressID *int64) (int64, []int64, []struct { RequestShippings(ctx context.Context, userID int64, inventoryIDs []int64, addressID *int64) (addrID int64, batchNo string, success []int64, skipped []struct {
ID int64 ID int64
Reason string Reason string
}, []struct { }, failed []struct {
ID int64 ID int64
Reason string Reason string
}, error) }, err error)
VoidUserInventory(ctx context.Context, adminID int64, userID int64, inventoryID int64) error VoidUserInventory(ctx context.Context, adminID int64, userID int64, inventoryID int64) error
RedeemInventoryToPoints(ctx context.Context, userID int64, inventoryID int64) (int64, error) RedeemInventoryToPoints(ctx context.Context, userID int64, inventoryID int64) (int64, error)
RedeemInventoriesToPoints(ctx context.Context, userID int64, inventoryIDs []int64) (int64, error) RedeemInventoriesToPoints(ctx context.Context, userID int64, inventoryIDs []int64) (int64, error)

View File

@ -331,3 +331,39 @@
{"level":"info","time":"2025-12-23 23:32:14","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":2} {"level":"info","time":"2025-12-23 23:32:14","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":2}
{"level":"info","time":"2025-12-23 23:32:14","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":3} {"level":"info","time":"2025-12-23 23:32:14","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":3}
{"level":"info","time":"2025-12-23 23:32:14","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":0} {"level":"info","time":"2025-12-23 23:32:14","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":0}
{"level":"info","time":"2025-12-23 23:38:04","caller":"logger/logger.go:309","msg":"Connected to Redis","domain":"mini-chat[fat]","addr":"118.25.13.43:8379"}
{"level":"info","time":"2025-12-23 23:38:04","caller":"logger/logger.go:309","msg":"Task center worker started","domain":"mini-chat[fat]"}
{"level":"info","time":"2025-12-23 23:38:04","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":1}
{"level":"info","time":"2025-12-23 23:38:04","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":2}
{"level":"info","time":"2025-12-23 23:38:04","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":0}
{"level":"info","time":"2025-12-23 23:38:04","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":3}
{"level":"info","time":"2025-12-23 23:38:04","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":4}
{"level":"info","time":"2025-12-24 13:04:26","caller":"logger/logger.go:309","msg":"Connected to Redis","domain":"mini-chat[fat]","addr":"118.25.13.43:8379"}
{"level":"info","time":"2025-12-24 13:04:26","caller":"logger/logger.go:309","msg":"Task center worker started","domain":"mini-chat[fat]"}
{"level":"info","time":"2025-12-24 13:04:26","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":4}
{"level":"info","time":"2025-12-24 13:04:26","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":0}
{"level":"info","time":"2025-12-24 13:04:26","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":3}
{"level":"info","time":"2025-12-24 13:04:26","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":1}
{"level":"info","time":"2025-12-24 13:04:26","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":2}
{"level":"info","time":"2025-12-24 13:26:35","caller":"logger/logger.go:309","msg":"Connected to Redis","domain":"mini-chat[fat]","addr":"118.25.13.43:8379"}
{"level":"info","time":"2025-12-24 13:26:35","caller":"logger/logger.go:309","msg":"Task center worker started","domain":"mini-chat[fat]"}
{"level":"info","time":"2025-12-24 13:26:35","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":2}
{"level":"info","time":"2025-12-24 13:26:35","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":0}
{"level":"info","time":"2025-12-24 13:26:35","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":3}
{"level":"info","time":"2025-12-24 13:26:35","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":1}
{"level":"info","time":"2025-12-24 13:26:35","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":4}
{"level":"info","time":"2025-12-24 13:29:35","caller":"logger/logger.go:309","msg":"Processing event","domain":"mini-chat[fat]","type":"order_paid","worker_id":1}
{"level":"info","time":"2025-12-24 14:48:36","caller":"logger/logger.go:309","msg":"Connected to Redis","domain":"mini-chat[fat]","addr":"118.25.13.43:8379"}
{"level":"info","time":"2025-12-24 14:48:36","caller":"logger/logger.go:309","msg":"Task center worker started","domain":"mini-chat[fat]"}
{"level":"info","time":"2025-12-24 14:48:36","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":4}
{"level":"info","time":"2025-12-24 14:48:36","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":1}
{"level":"info","time":"2025-12-24 14:48:36","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":2}
{"level":"info","time":"2025-12-24 14:48:36","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":0}
{"level":"info","time":"2025-12-24 14:48:36","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":3}
{"level":"info","time":"2025-12-24 17:05:25","caller":"logger/logger.go:309","msg":"Connected to Redis","domain":"mini-chat[fat]","addr":"118.25.13.43:8379"}
{"level":"info","time":"2025-12-24 17:05:26","caller":"logger/logger.go:309","msg":"Task center worker started","domain":"mini-chat[fat]"}
{"level":"info","time":"2025-12-24 17:05:26","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":2}
{"level":"info","time":"2025-12-24 17:05:26","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":0}
{"level":"info","time":"2025-12-24 17:05:26","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":4}
{"level":"info","time":"2025-12-24 17:05:26","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":1}
{"level":"info","time":"2025-12-24 17:05:26","caller":"logger/logger.go:309","msg":"Worker routine started","domain":"mini-chat[fat]","worker_id":3}

View File

@ -0,0 +1,32 @@
-- 游戏资格系统表
-- Migration: 006_game_tickets.sql
-- 用户游戏资格
CREATE TABLE IF NOT EXISTS user_game_tickets (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
user_id BIGINT NOT NULL COMMENT '用户ID',
game_code VARCHAR(32) NOT NULL DEFAULT 'minesweeper' COMMENT '游戏代码',
available INT NOT NULL DEFAULT 0 COMMENT '可用次数',
total_earned INT NOT NULL DEFAULT 0 COMMENT '累计获得',
total_used INT NOT NULL DEFAULT 0 COMMENT '累计使用',
UNIQUE KEY uk_user_game (user_id, game_code),
INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户游戏资格';
-- 资格变动日志
CREATE TABLE IF NOT EXISTS game_ticket_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3),
user_id BIGINT NOT NULL COMMENT '用户ID',
game_code VARCHAR(32) NOT NULL COMMENT '游戏代码',
change_type TINYINT NOT NULL COMMENT '1=获得 2=使用',
amount INT NOT NULL COMMENT '变动数量',
balance INT NOT NULL COMMENT '变动后余额',
source VARCHAR(32) COMMENT '来源: order/task/admin',
source_id BIGINT COMMENT '来源ID',
remark VARCHAR(255) COMMENT '备注',
INDEX idx_user_game (user_id, game_code),
INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='游戏资格变动日志';