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
docker build -t zfc931912343/bindbox-game:v1.9 .
docker push zfc931912343/bindbox-game:v1.9
docker build -t zfc931912343/bindbox-game:v1.10 .
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 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">
</head>

View File

@ -34,6 +34,6 @@ eg :
```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 {
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 取消发货申请
// @Summary 取消发货申请
// @Description 取消已提交但未发货的申请;恢复库存状态
// @Description 取消已提交但未发货的申请;恢复库存状态。支持按单个资产ID取消或按批次号批量取消
// @Tags APP端.用户
// @Accept json
// @Produce json
// @Security LoginVerifyToken
// @Param user_id path integer true "用户ID"
// @Param RequestBody body cancelShippingRequest true "请求参数资产ID"
// @Param RequestBody body cancelShippingRequest true "请求参数资产ID或批次号(二选一)"
// @Success 200 {object} cancelShippingResponse "成功"
// @Failure 400 {object} code.Failure "参数错误/记录不存在/已处理"
// @Router /api/app/users/{user_id}/inventory/cancel-shipping [post]
func (h *handler) CancelShipping() core.HandlerFunc {
return func(ctx core.Context) {
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, "参数错误"))
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)
err := h.user.CancelShipping(ctx.RequestContext(), userID, req.InventoryID)
count, err := h.user.CancelShipping(ctx.RequestContext(), userID, req.InventoryID, req.BatchNo)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10024, err.Error()))
return
}
ctx.Payload(nil)
ctx.Payload(&cancelShippingResponse{CancelledCount: count})
}
}

View File

@ -1,22 +1,23 @@
package app
import (
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"net/http"
"bindbox-game/internal/code"
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/validation"
"net/http"
)
type requestShippingBatchRequest struct {
InventoryIDs []int64 `json:"inventory_ids"`
AddressID *int64 `json:"address_id"`
InventoryIDs []int64 `json:"inventory_ids"`
AddressID *int64 `json:"address_id"`
}
type requestShippingBatchResponse struct {
AddressID int64 `json:"address_id"`
SuccessIDs []int64 `json:"success_ids"`
Skipped []map[string]any `json:"skipped"`
Failed []map[string]any `json:"failed"`
AddressID int64 `json:"address_id"`
BatchNo string `json:"batch_no"`
SuccessIDs []int64 `json:"success_ids"`
Skipped []map[string]any `json:"skipped"`
Failed []map[string]any `json:"failed"`
}
// RequestShippingBatch 批量申请发货(使用默认地址或指定地址)
@ -32,32 +33,32 @@ type requestShippingBatchResponse struct {
// @Failure 400 {object} code.Failure
// @Router /api/app/users/{user_id}/inventory/request-shipping-batch [post]
func (h *handler) RequestShippingBatch() core.HandlerFunc {
return func(ctx core.Context) {
req := new(requestShippingBatchRequest)
rsp := new(requestShippingBatchResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if len(req.InventoryIDs) == 0 || len(req.InventoryIDs) > 100 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "invalid inventory_ids"))
return
}
userID := int64(ctx.SessionUserInfo().Id)
addrID, success, skipped, failed, err := h.user.RequestShippings(ctx.RequestContext(), userID, req.InventoryIDs, req.AddressID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10023, err.Error()))
return
}
rsp.AddressID = addrID
rsp.SuccessIDs = success
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})
}
ctx.Payload(rsp)
}
return func(ctx core.Context) {
req := new(requestShippingBatchRequest)
rsp := new(requestShippingBatchResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if len(req.InventoryIDs) == 0 || len(req.InventoryIDs) > 100 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "invalid inventory_ids"))
return
}
userID := int64(ctx.SessionUserInfo().Id)
addrID, batchNo, success, skipped, failed, err := h.user.RequestShippings(ctx.RequestContext(), userID, req.InventoryIDs, req.AddressID)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, 10023, err.Error()))
return
}
rsp.AddressID = addrID
rsp.BatchNo = batchNo
rsp.SuccessIDs = success
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})
}
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
Banner *banner
Channels *channels
GameTicketLogs *gameTicketLogs
IssuePositionClaims *issuePositionClaims
LogOperation *logOperation
LogRequest *logRequest
@ -60,6 +61,7 @@ var (
TaskCenterTasks *taskCenterTasks
UserAddresses *userAddresses
UserCoupons *userCoupons
UserGameTickets *userGameTickets
UserInventory *userInventory
UserInventoryTransfers *userInventoryTransfers
UserInvites *userInvites
@ -83,6 +85,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
Admin = &Q.Admin
Banner = &Q.Banner
Channels = &Q.Channels
GameTicketLogs = &Q.GameTicketLogs
IssuePositionClaims = &Q.IssuePositionClaims
LogOperation = &Q.LogOperation
LogRequest = &Q.LogRequest
@ -116,6 +119,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
TaskCenterTasks = &Q.TaskCenterTasks
UserAddresses = &Q.UserAddresses
UserCoupons = &Q.UserCoupons
UserGameTickets = &Q.UserGameTickets
UserInventory = &Q.UserInventory
UserInventoryTransfers = &Q.UserInventoryTransfers
UserInvites = &Q.UserInvites
@ -140,6 +144,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
Admin: newAdmin(db, opts...),
Banner: newBanner(db, opts...),
Channels: newChannels(db, opts...),
GameTicketLogs: newGameTicketLogs(db, opts...),
IssuePositionClaims: newIssuePositionClaims(db, opts...),
LogOperation: newLogOperation(db, opts...),
LogRequest: newLogRequest(db, opts...),
@ -173,6 +178,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
TaskCenterTasks: newTaskCenterTasks(db, opts...),
UserAddresses: newUserAddresses(db, opts...),
UserCoupons: newUserCoupons(db, opts...),
UserGameTickets: newUserGameTickets(db, opts...),
UserInventory: newUserInventory(db, opts...),
UserInventoryTransfers: newUserInventoryTransfers(db, opts...),
UserInvites: newUserInvites(db, opts...),
@ -198,6 +204,7 @@ type Query struct {
Admin admin
Banner banner
Channels channels
GameTicketLogs gameTicketLogs
IssuePositionClaims issuePositionClaims
LogOperation logOperation
LogRequest logRequest
@ -231,6 +238,7 @@ type Query struct {
TaskCenterTasks taskCenterTasks
UserAddresses userAddresses
UserCoupons userCoupons
UserGameTickets userGameTickets
UserInventory userInventory
UserInventoryTransfers userInventoryTransfers
UserInvites userInvites
@ -257,6 +265,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
Admin: q.Admin.clone(db),
Banner: q.Banner.clone(db),
Channels: q.Channels.clone(db),
GameTicketLogs: q.GameTicketLogs.clone(db),
IssuePositionClaims: q.IssuePositionClaims.clone(db),
LogOperation: q.LogOperation.clone(db),
LogRequest: q.LogRequest.clone(db),
@ -290,6 +299,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
TaskCenterTasks: q.TaskCenterTasks.clone(db),
UserAddresses: q.UserAddresses.clone(db),
UserCoupons: q.UserCoupons.clone(db),
UserGameTickets: q.UserGameTickets.clone(db),
UserInventory: q.UserInventory.clone(db),
UserInventoryTransfers: q.UserInventoryTransfers.clone(db),
UserInvites: q.UserInvites.clone(db),
@ -323,6 +333,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
Admin: q.Admin.replaceDB(db),
Banner: q.Banner.replaceDB(db),
Channels: q.Channels.replaceDB(db),
GameTicketLogs: q.GameTicketLogs.replaceDB(db),
IssuePositionClaims: q.IssuePositionClaims.replaceDB(db),
LogOperation: q.LogOperation.replaceDB(db),
LogRequest: q.LogRequest.replaceDB(db),
@ -356,6 +367,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
TaskCenterTasks: q.TaskCenterTasks.replaceDB(db),
UserAddresses: q.UserAddresses.replaceDB(db),
UserCoupons: q.UserCoupons.replaceDB(db),
UserGameTickets: q.UserGameTickets.replaceDB(db),
UserInventory: q.UserInventory.replaceDB(db),
UserInventoryTransfers: q.UserInventoryTransfers.replaceDB(db),
UserInvites: q.UserInvites.replaceDB(db),
@ -379,6 +391,7 @@ type queryCtx struct {
Admin *adminDo
Banner *bannerDo
Channels *channelsDo
GameTicketLogs *gameTicketLogsDo
IssuePositionClaims *issuePositionClaimsDo
LogOperation *logOperationDo
LogRequest *logRequestDo
@ -412,6 +425,7 @@ type queryCtx struct {
TaskCenterTasks *taskCenterTasksDo
UserAddresses *userAddressesDo
UserCoupons *userCouponsDo
UserGameTickets *userGameTicketsDo
UserInventory *userInventoryDo
UserInventoryTransfers *userInventoryTransfersDo
UserInvites *userInvitesDo
@ -435,6 +449,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
Admin: q.Admin.WithContext(ctx),
Banner: q.Banner.WithContext(ctx),
Channels: q.Channels.WithContext(ctx),
GameTicketLogs: q.GameTicketLogs.WithContext(ctx),
IssuePositionClaims: q.IssuePositionClaims.WithContext(ctx),
LogOperation: q.LogOperation.WithContext(ctx),
LogRequest: q.LogRequest.WithContext(ctx),
@ -468,6 +483,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
TaskCenterTasks: q.TaskCenterTasks.WithContext(ctx),
UserAddresses: q.UserAddresses.WithContext(ctx),
UserCoupons: q.UserCoupons.WithContext(ctx),
UserGameTickets: q.UserGameTickets.WithContext(ctx),
UserInventory: q.UserInventory.WithContext(ctx),
UserInventoryTransfers: q.UserInventoryTransfers.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"
appapi "bindbox-game/internal/api/app"
commonapi "bindbox-game/internal/api/common"
gameapi "bindbox-game/internal/api/game"
payapi "bindbox-game/internal/api/pay"
taskcenterapi "bindbox-game/internal/api/task_center"
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)
commonHandler := commonapi.New(logger, db)
payHandler := payapi.New(logger, db, taskSvc)
// minesweeperHandler := minesweeperapi.New(logger, db)
gameHandler := gameapi.New(logger, db, rdb, userSvc)
intc := interceptor.New(logger, db)
// 内部服务接口路由组 (供 Nakama 调用)
// internalRouter := mux.Group("/internal")
// {
// TODO: 添加IP白名单或Internal-Key验证中间件
// internalRouter.POST("/game/verify", minesweeperHandler.VerifyTicket())
// internalRouter.POST("/game/settle", minesweeperHandler.SettleGame())
// }
internalRouter := mux.Group("/internal")
{
// TODO: 添加IP白名单或Internal-Key验证中间件
internalRouter.POST("/game/verify", gameHandler.VerifyTicket())
internalRouter.POST("/game/settle", gameHandler.SettleGame())
internalRouter.GET("/game/minesweeper/config", gameHandler.GetMinesweeperConfig())
}
// 管理端非认证接口路由组
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.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/: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.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/revoke", userHandler.RevokeAddressShare())
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"
"time"
gamesvc "bindbox-game/internal/service/game"
titlesvc "bindbox-game/internal/service/title"
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 {
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 {
tx.Rollback()

View File

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

View File

@ -7,40 +7,84 @@ import (
)
// CancelShipping 取消发货申请
func (s *service) CancelShipping(ctx context.Context, userID int64, inventoryID int64) error {
// 1. 开启事务
return s.writeDB.Transaction(func(tx *dao.Query) error {
// 2. 查询发货记录(必须是待发货状态 status=1
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()
// 支持按单个资产ID取消或按批次号批量取消
// 返回成功取消的记录数
func (s *service) CancelShipping(ctx context.Context, userID int64, inventoryID int64, batchNo string) (int64, error) {
var cancelledCount int64
if err != nil {
return fmt.Errorf("shipping record not found or already processed")
err := s.writeDB.Transaction(func(tx *dao.Query) error {
var records []*struct {
ID int64
InventoryID int64
}
// 3. 更新发货记录状态为已取消 (status=5)
if _, err := tx.ShippingRecords.WithContext(ctx).
Where(tx.ShippingRecords.ID.Eq(sr.ID)).
Update(tx.ShippingRecords.Status, 5); err != nil {
return err
// 根据参数查询待取消的发货记录
if batchNo != "" {
// 按批次号查询
rows, err := tx.ShippingRecords.WithContext(ctx).
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)
// 并追加备注
// 使用原生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=?",
remark,
inventoryID,
userID,
).Error; err != nil {
return err
if len(records) == 0 {
return fmt.Errorf("no pending shipping records found")
}
// 批量处理每条记录
for _, rec := range records {
// 更新发货记录状态为已取消 (status=5)
if _, err := tx.ShippingRecords.WithContext(ctx).
Where(tx.ShippingRecords.ID.Eq(rec.ID)).
Update(tx.ShippingRecords.Status, 5); err != nil {
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
})
if err != nil {
return 0, err
}
return cancelledCount, nil
}

View File

@ -1,16 +1,15 @@
package user
import (
"context"
"testing"
"context"
"testing"
)
func TestRequestShippings_DedupAndSkip(t *testing.T) {
s := New(nil, nil)
// 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)
if err != nil {
// no real DB; just ensure function can be called
}
s := New(nil, nil)
// 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)
if err != nil {
// 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
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)
CancelShipping(ctx context.Context, userID int64, inventoryID int64) error
RequestShippings(ctx context.Context, userID int64, inventoryIDs []int64, addressID *int64) (int64, []int64, []struct {
CancelShipping(ctx context.Context, userID int64, inventoryID int64, batchNo string) (int64, error)
RequestShippings(ctx context.Context, userID int64, inventoryIDs []int64, addressID *int64) (addrID int64, batchNo string, success []int64, skipped []struct {
ID int64
Reason string
}, []struct {
}, failed []struct {
ID int64
Reason string
}, error)
}, err error)
VoidUserInventory(ctx context.Context, adminID int64, userID int64, inventoryID int64) error
RedeemInventoryToPoints(ctx context.Context, userID int64, inventoryID 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":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: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='游戏资格变动日志';