diff --git a/README.md b/README.md index 66fc8e2..ac5b6e0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/build.zip b/build.zip index 9a8b4f4..32f6cd4 100644 Binary files a/build.zip and b/build.zip differ diff --git a/build/resources/admin/index.html b/build/resources/admin/index.html index 6cc47bf..b1f1ac1 100644 --- a/build/resources/admin/index.html +++ b/build/resources/admin/index.html @@ -38,7 +38,7 @@ } })() - + diff --git a/cmd/gormgen/README.md b/cmd/gormgen/README.md index ba71f20..9c7a2e0 100644 --- a/cmd/gormgen/README.md +++ b/cmd/gormgen/README.md @@ -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" ``` diff --git a/internal/api/game/handler.go b/internal/api/game/handler.go new file mode 100644 index 0000000..4b8916c --- /dev/null +++ b/internal/api/game/handler.go @@ -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) +} diff --git a/internal/api/user/cancel_shipping_app.go b/internal/api/user/cancel_shipping_app.go index f925699..6552b71 100644 --- a/internal/api/user/cancel_shipping_app.go +++ b/internal/api/user/cancel_shipping_app.go @@ -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}) } } diff --git a/internal/api/user/request_shipping_batch_app.go b/internal/api/user/request_shipping_batch_app.go index 0f065b6..9267d12 100644 --- a/internal/api/user/request_shipping_batch_app.go +++ b/internal/api/user/request_shipping_batch_app.go @@ -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) + } } - diff --git a/internal/repository/mysql/dao/game_ticket_logs.gen.go b/internal/repository/mysql/dao/game_ticket_logs.gen.go new file mode 100644 index 0000000..d889d68 --- /dev/null +++ b/internal/repository/mysql/dao/game_ticket_logs.gen.go @@ -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 +} diff --git a/internal/repository/mysql/dao/gen.go b/internal/repository/mysql/dao/gen.go index 574d7ec..7a6c2ed 100644 --- a/internal/repository/mysql/dao/gen.go +++ b/internal/repository/mysql/dao/gen.go @@ -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), diff --git a/internal/repository/mysql/dao/user_game_tickets.gen.go b/internal/repository/mysql/dao/user_game_tickets.gen.go new file mode 100644 index 0000000..95fc909 --- /dev/null +++ b/internal/repository/mysql/dao/user_game_tickets.gen.go @@ -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 +} diff --git a/internal/repository/mysql/model/game_ticket_logs.gen.go b/internal/repository/mysql/model/game_ticket_logs.gen.go new file mode 100644 index 0000000..11f8cea --- /dev/null +++ b/internal/repository/mysql/model/game_ticket_logs.gen.go @@ -0,0 +1,30 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package model + +import ( + "time" +) + +const 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 +} diff --git a/internal/repository/mysql/model/user_game_tickets.gen.go b/internal/repository/mysql/model/user_game_tickets.gen.go new file mode 100644 index 0000000..dd98f33 --- /dev/null +++ b/internal/repository/mysql/model/user_game_tickets.gen.go @@ -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 +} diff --git a/internal/router/router.go b/internal/router/router.go index e71967b..ed9a087 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -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()) diff --git a/internal/service/game/ticket_service.go b/internal/service/game/ticket_service.go new file mode 100644 index 0000000..bf03398 --- /dev/null +++ b/internal/service/game/ticket_service.go @@ -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 +} diff --git a/internal/service/task_center/service.go b/internal/service/task_center/service.go index 19c30f2..cdacd7c 100644 --- a/internal/service/task_center/service.go +++ b/internal/service/task_center/service.go @@ -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() diff --git a/internal/service/user/address_share.go b/internal/service/user/address_share.go index 667e838..3309a57 100644 --- a/internal/service/user/address_share.go +++ b/internal/service/user/address_share.go @@ -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) { diff --git a/internal/service/user/cancel_shipping.go b/internal/service/user/cancel_shipping.go index d54c693..3389b29 100644 --- a/internal/service/user/cancel_shipping.go +++ b/internal/service/user/cancel_shipping.go @@ -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 } diff --git a/internal/service/user/request_shipping_batch_test.go b/internal/service/user/request_shipping_batch_test.go index 18bd27a..9985bb7 100644 --- a/internal/service/user/request_shipping_batch_test.go +++ b/internal/service/user/request_shipping_batch_test.go @@ -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 + } } - diff --git a/internal/service/user/user.go b/internal/service/user/user.go index 19b6355..ca6cad8 100644 --- a/internal/service/user/user.go +++ b/internal/service/user/user.go @@ -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) diff --git a/logs/mini-chat-access.log b/logs/mini-chat-access.log index 748bdf8..b532934 100644 --- a/logs/mini-chat-access.log +++ b/logs/mini-chat-access.log @@ -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} diff --git a/migrations/006_game_tickets.sql b/migrations/006_game_tickets.sql new file mode 100644 index 0000000..17f2c88 --- /dev/null +++ b/migrations/006_game_tickets.sql @@ -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='游戏资格变动日志';