package admin import ( "net/http" "strconv" "time" "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/validation" "bindbox-game/internal/repository/mysql/model" ) // ==================== 次数卡发放 API ==================== type grantGamePassRequest struct { UserID int64 `json:"user_id" binding:"required"` ActivityID *int64 `json:"activity_id"` // 可选,NULL表示全局通用 Count int32 `json:"count" binding:"required,min=1"` ValidDays *int32 `json:"valid_days"` // 可选,NULL表示永久有效 Remark string `json:"remark"` } type grantGamePassResponse struct { ID int64 `json:"id"` Message string `json:"message"` } // GrantGamePass 为用户发放次数卡 // @Summary 发放次数卡 // @Description 管理员为用户发放游戏次数卡 // @Tags 管理端.次数卡 // @Accept json // @Produce json // @Param RequestBody body grantGamePassRequest true "请求参数" // @Success 200 {object} grantGamePassResponse // @Failure 400 {object} code.Failure // @Router /api/admin/game-passes/grant [post] // @Security LoginVerifyToken func (h *handler) GrantGamePass() core.HandlerFunc { return func(ctx core.Context) { req := new(grantGamePassRequest) res := new(grantGamePassResponse) if err := ctx.ShouldBindJSON(req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } // 检查用户是否存在 user, err := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.Eq(req.UserID)).First() if err != nil || user == nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "用户不存在")) return } now := time.Now() m := &model.UserGamePasses{ UserID: req.UserID, Remaining: req.Count, TotalGranted: req.Count, TotalUsed: 0, Source: "admin", Remark: req.Remark, CreatedAt: now, UpdatedAt: now, } if req.ActivityID != nil && *req.ActivityID > 0 { m.ActivityID = *req.ActivityID } if req.ValidDays != nil && *req.ValidDays > 0 { m.ExpiredAt = now.Add(time.Duration(*req.ValidDays) * 24 * time.Hour) } q := h.writeDB.UserGamePasses.WithContext(ctx.RequestContext()) if m.ExpiredAt.IsZero() { q = q.Omit(h.writeDB.UserGamePasses.ExpiredAt) } if err := q.Create(m); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error())) return } res.ID = m.ID res.Message = "发放成功" ctx.Payload(res) } } // ==================== 次数卡列表 API ==================== type listGamePassesRequest struct { UserID *int64 `form:"user_id"` ActivityID *int64 `form:"activity_id"` Page int `form:"page"` PageSize int `form:"page_size"` } type gamePassItem struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` UserName string `json:"user_name"` ActivityID int64 `json:"activity_id"` ActivityName string `json:"activity_name"` Remaining int32 `json:"remaining"` TotalGranted int32 `json:"total_granted"` TotalUsed int32 `json:"total_used"` ExpiredAt string `json:"expired_at"` Source string `json:"source"` Remark string `json:"remark"` CreatedAt string `json:"created_at"` } type listGamePassesResponse struct { Page int `json:"page"` PageSize int `json:"page_size"` Total int64 `json:"total"` List []gamePassItem `json:"list"` } // ListGamePasses 查询次数卡列表 // @Summary 次数卡列表 // @Description 查询用户次数卡列表,支持按用户、活动过滤 // @Tags 管理端.次数卡 // @Accept json // @Produce json // @Param user_id query integer false "用户ID" // @Param activity_id query integer false "活动ID" // @Param page query integer false "页码" // @Param page_size query integer false "每页条数" // @Success 200 {object} listGamePassesResponse // @Failure 400 {object} code.Failure // @Router /api/admin/game-passes/list [get] // @Security LoginVerifyToken func (h *handler) ListGamePasses() core.HandlerFunc { return func(ctx core.Context) { req := new(listGamePassesRequest) res := new(listGamePassesResponse) if err := ctx.ShouldBindForm(req); err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err))) return } if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 { req.PageSize = 20 } q := h.readDB.UserGamePasses.WithContext(ctx.RequestContext()).ReadDB() if req.UserID != nil && *req.UserID > 0 { q = q.Where(h.readDB.UserGamePasses.UserID.Eq(*req.UserID)) } if req.ActivityID != nil && *req.ActivityID > 0 { q = q.Where(h.readDB.UserGamePasses.ActivityID.Eq(*req.ActivityID)) } total, err := q.Count() if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error())) return } passes, err := q.Order(h.readDB.UserGamePasses.ID.Desc()). Limit(req.PageSize). Offset((req.Page - 1) * req.PageSize). Find() if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error())) return } // 批量获取用户名和活动名 userIDs := make([]int64, 0) activityIDs := make([]int64, 0) for _, p := range passes { userIDs = append(userIDs, p.UserID) if p.ActivityID > 0 { activityIDs = append(activityIDs, p.ActivityID) } } userMap := make(map[int64]string) if len(userIDs) > 0 { users, _ := h.readDB.Users.WithContext(ctx.RequestContext()).Where(h.readDB.Users.ID.In(userIDs...)).Find() for _, u := range users { userMap[u.ID] = u.Nickname } } activityMap := make(map[int64]string) if len(activityIDs) > 0 { activities, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.In(activityIDs...)).Find() for _, a := range activities { activityMap[a.ID] = a.Name } } res.Page = req.Page res.PageSize = req.PageSize res.Total = total res.List = make([]gamePassItem, len(passes)) for i, p := range passes { expiredAt := "" if !p.ExpiredAt.IsZero() { expiredAt = p.ExpiredAt.Format("2006-01-02 15:04:05") } res.List[i] = gamePassItem{ ID: p.ID, UserID: p.UserID, UserName: userMap[p.UserID], ActivityID: p.ActivityID, ActivityName: activityMap[p.ActivityID], Remaining: p.Remaining, TotalGranted: p.TotalGranted, TotalUsed: p.TotalUsed, ExpiredAt: expiredAt, Source: p.Source, Remark: p.Remark, CreatedAt: p.CreatedAt.Format("2006-01-02 15:04:05"), } } ctx.Payload(res) } } // ==================== 查询单个用户的次数卡 ==================== type getUserGamePassesResponse struct { UserID int64 `json:"user_id"` TotalRemaining int32 `json:"total_remaining"` GlobalRemaining int32 `json:"global_remaining"` ActivityPasses []gamePassItem `json:"activity_passes"` } // GetUserGamePasses 查询单个用户的次数卡 // @Summary 查询用户次数卡 // @Description 查询指定用户的所有次数卡 // @Tags 管理端.次数卡 // @Accept json // @Produce json // @Param user_id path integer true "用户ID" // @Success 200 {object} getUserGamePassesResponse // @Failure 400 {object} code.Failure // @Router /api/admin/users/{user_id}/game-passes [get] // @Security LoginVerifyToken func (h *handler) GetUserGamePasses() core.HandlerFunc { return func(ctx core.Context) { userIDStr := ctx.Param("user_id") userID, _ := strconv.ParseInt(userIDStr, 10, 64) if userID <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "用户ID无效")) return } res := new(getUserGamePassesResponse) res.UserID = userID now := time.Now() passes, err := h.readDB.UserGamePasses.WithContext(ctx.RequestContext()). Where(h.readDB.UserGamePasses.UserID.Eq(userID)). Where(h.readDB.UserGamePasses.Remaining.Gt(0)). Find() if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error())) return } // 获取活动名称 activityIDs := make([]int64, 0) for _, p := range passes { if p.ActivityID > 0 { activityIDs = append(activityIDs, p.ActivityID) } } activityMap := make(map[int64]string) if len(activityIDs) > 0 { activities, _ := h.readDB.Activities.WithContext(ctx.RequestContext()).Where(h.readDB.Activities.ID.In(activityIDs...)).Find() for _, a := range activities { activityMap[a.ID] = a.Name } } res.ActivityPasses = make([]gamePassItem, 0) for _, p := range passes { // 检查是否过期 if !p.ExpiredAt.IsZero() && p.ExpiredAt.Before(now) { continue } expiredAt := "" if !p.ExpiredAt.IsZero() { expiredAt = p.ExpiredAt.Format("2006-01-02 15:04:05") } item := gamePassItem{ ID: p.ID, UserID: p.UserID, ActivityID: p.ActivityID, ActivityName: activityMap[p.ActivityID], Remaining: p.Remaining, TotalGranted: p.TotalGranted, TotalUsed: p.TotalUsed, ExpiredAt: expiredAt, Source: p.Source, Remark: p.Remark, CreatedAt: p.CreatedAt.Format("2006-01-02 15:04:05"), } res.TotalRemaining += p.Remaining if p.ActivityID == 0 { res.GlobalRemaining += p.Remaining } res.ActivityPasses = append(res.ActivityPasses, item) } ctx.Payload(res) } } // ==================== 检查活动次数卡 ==================== type checkActivityGamePassesResponse struct { ActivityID int64 `json:"activity_id"` UserCount int64 `json:"user_count"` TotalRemaining int64 `json:"total_remaining"` CanDelete bool `json:"can_delete"` Message string `json:"message"` } // CheckActivityGamePasses 检查活动是否有未使用的次数卡 // @Summary 检查活动次数卡 // @Description 检查指定活动是否有用户持有未使用的次数卡,用于下架/删除前校验 // @Tags 管理端.次数卡 // @Accept json // @Produce json // @Param activity_id path integer true "活动ID" // @Success 200 {object} checkActivityGamePassesResponse // @Failure 400 {object} code.Failure // @Router /api/admin/activities/{activity_id}/game-passes/check [get] // @Security LoginVerifyToken func (h *handler) CheckActivityGamePasses() core.HandlerFunc { return func(ctx core.Context) { activityIDStr := ctx.Param("activity_id") activityID, _ := strconv.ParseInt(activityIDStr, 10, 64) if activityID <= 0 { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "活动ID无效")) return } res := new(checkActivityGamePassesResponse) res.ActivityID = activityID // 统计用户数和剩余次数 type stats struct { UserCount int64 TotalRemaining int64 } var s stats h.repo.GetDbR().Raw(` SELECT COUNT(DISTINCT user_id) as user_count, COALESCE(SUM(remaining), 0) as total_remaining FROM user_game_passes WHERE activity_id = ? AND remaining > 0 `, activityID).Scan(&s) res.UserCount = s.UserCount res.TotalRemaining = s.TotalRemaining res.CanDelete = s.UserCount == 0 if s.UserCount > 0 { res.Message = "有用户持有该活动的未使用次数卡,请先处理后再下架/删除" } else { res.Message = "无用户持有该活动的次数卡,可以安全下架/删除" } ctx.Payload(res) } }