bindbox-game/internal/api/admin/game_passes_admin.go

382 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}