382 lines
11 KiB
Go
382 lines
11 KiB
Go
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)
|
||
}
|
||
}
|