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

552 lines
19 KiB
Go
Raw Permalink 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"
activitysvc "bindbox-game/internal/service/activity"
)
type createActivityRequest struct {
Name string `json:"name" binding:"required"`
Banner string `json:"banner"`
Image string `json:"image"`
GameplayIntro string `json:"gameplay_intro"`
ActivityCategoryID int64 `json:"activity_category_id" binding:"required"`
Status int32 `json:"status"`
PriceDraw int64 `json:"price_draw"`
IsBoss int32 `json:"is_boss"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
DrawMode string `json:"draw_mode"`
MinParticipants int64 `json:"min_participants"`
ScheduledTime string `json:"scheduled_time"`
ScheduledDelayMinutes int64 `json:"scheduled_delay_minutes"`
IntervalMinutes int64 `json:"interval_minutes"`
RefundCouponType string `json:"refund_coupon_type"`
RefundCouponAmount float64 `json:"refund_coupon_amount"`
RefundCouponID int64 `json:"refund_coupon_id"`
PlayType string `json:"play_type"`
AllowItemCards int32 `json:"allow_item_cards"`
AllowCoupons int32 `json:"allow_coupons"`
}
type createActivityResponse struct {
ID int64 `json:"id"`
Message string `json:"message"`
}
type activityDetailResponse struct {
ID int64 `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Name string `json:"name"`
Banner string `json:"banner"`
ActivityCategoryID int64 `json:"activity_category_id"`
Status int32 `json:"status"`
PriceDraw int64 `json:"price_draw"`
IsBoss int32 `json:"is_boss"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
DrawMode string `json:"draw_mode"`
PlayType string `json:"play_type"`
MinParticipants int64 `json:"min_participants"`
IntervalMinutes int64 `json:"interval_minutes"`
ScheduledTime time.Time `json:"scheduled_time"`
LastSettledAt time.Time `json:"last_settled_at"`
RefundCouponID int64 `json:"refund_coupon_id"`
Image string `json:"image"`
GameplayIntro string `json:"gameplay_intro"`
AllowItemCards bool `json:"allow_item_cards"`
AllowCoupons bool `json:"allow_coupons"`
}
// CreateActivity 创建活动
// @Summary 创建活动
// @Description 创建活动配置基本信息与分类、Boss标签
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param RequestBody body createActivityRequest true "请求参数"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities [post]
// @Security LoginVerifyToken
func (h *handler) CreateActivity() core.HandlerFunc {
return func(ctx core.Context) {
req := new(createActivityRequest)
res := new(createActivityResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if req.ActivityCategoryID == 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "活动分类ID不能为空"))
return
}
if req.PriceDraw <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "抽奖价格必须大于0单位积分"))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateActivityError, "禁止操作"))
return
}
var st, et *time.Time
if req.StartTime != "" {
if t, err := time.Parse(time.RFC3339, req.StartTime); err == nil {
st = &t
} else if t2, err2 := time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local); err2 == nil {
st = &t2
}
}
if req.EndTime != "" {
if t, err := time.Parse(time.RFC3339, req.EndTime); err == nil {
et = &t
} else if t2, err2 := time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local); err2 == nil {
et = &t2
}
}
aic := req.AllowItemCards
if aic == 0 {
aic = 1
}
acp := req.AllowCoupons
if acp == 0 {
acp = 1
}
item, err := h.activity.CreateActivity(ctx.RequestContext(), activitysvc.CreateActivityInput{
Name: req.Name,
Banner: req.Banner,
Image: req.Image,
GameplayIntro: req.GameplayIntro,
ActivityCategoryID: req.ActivityCategoryID,
Status: req.Status,
PriceDraw: req.PriceDraw,
IsBoss: req.IsBoss,
StartTime: st,
EndTime: et,
AllowItemCards: aic,
AllowCoupons: acp,
})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateActivityError, err.Error()))
return
}
if req.DrawMode != "" {
var stPtr *time.Time
if req.ScheduledDelayMinutes > 0 {
t := time.Now().Add(time.Duration(req.ScheduledDelayMinutes) * time.Minute)
stPtr = &t
} else if req.IntervalMinutes > 0 && req.ScheduledTime == "" {
t := time.Now().Add(time.Duration(req.IntervalMinutes) * time.Minute)
stPtr = &t
} else if req.ScheduledTime != "" {
if t, err := time.Parse(time.RFC3339, req.ScheduledTime); err == nil {
stPtr = &t
} else if t2, err2 := time.ParseInLocation("2006-01-02 15:04:05", req.ScheduledTime, time.Local); err2 == nil {
stPtr = &t2
}
}
if err := h.activity.SaveActivityDrawConfig(ctx.RequestContext(), item.ID, activitysvc.DrawConfig{PlayType: req.PlayType, DrawMode: req.DrawMode, MinParticipants: req.MinParticipants, IntervalMinutes: req.IntervalMinutes, ScheduledTime: stPtr, RefundCouponID: req.RefundCouponID}); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.CreateActivityError, err.Error()))
return
}
}
res.ID = item.ID
res.Message = "操作成功"
ctx.Payload(res)
}
}
type modifyActivityRequest struct {
Name string `json:"name"`
Banner string `json:"banner"`
Image string `json:"image"`
GameplayIntro string `json:"gameplay_intro"`
ActivityCategoryID int64 `json:"activity_category_id"`
Status int32 `json:"status"`
PriceDraw int64 `json:"price_draw"`
IsBoss int32 `json:"is_boss"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
DrawMode string `json:"draw_mode"`
MinParticipants int64 `json:"min_participants"`
ScheduledTime string `json:"scheduled_time"`
ScheduledDelayMinutes int64 `json:"scheduled_delay_minutes"`
IntervalMinutes int64 `json:"interval_minutes"`
RefundCouponType string `json:"refund_coupon_type"`
RefundCouponAmount float64 `json:"refund_coupon_amount"`
RefundCouponID int64 `json:"refund_coupon_id"`
PlayType string `json:"play_type"`
AllowItemCards *int32 `json:"allow_item_cards"`
AllowCoupons *int32 `json:"allow_coupons"`
Force bool `json:"force"`
}
// ModifyActivity 修改活动
// @Summary 修改活动
// @Description 修改活动基本信息、分类、Boss标签等
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Param RequestBody body modifyActivityRequest true "请求参数"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id} [put]
// @Security LoginVerifyToken
func (h *handler) ModifyActivity() core.HandlerFunc {
return func(ctx core.Context) {
req := new(modifyActivityRequest)
res := new(simpleMessageResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ModifyActivityError, "禁止操作"))
return
}
id, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
return
}
var st, et *time.Time
if req.StartTime != "" {
if t, err := time.Parse(time.RFC3339, req.StartTime); err == nil {
st = &t
} else if t2, err2 := time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local); err2 == nil {
st = &t2
}
}
if req.EndTime != "" {
if t, err := time.Parse(time.RFC3339, req.EndTime); err == nil {
et = &t
} else if t2, err2 := time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local); err2 == nil {
et = &t2
}
}
// 检查下线时的未消费次数卡
if req.Status == 2 && !req.Force {
ugp := h.readDB.UserGamePasses
count, err := ugp.WithContext(ctx.RequestContext()).Where(ugp.ActivityID.Eq(id), ugp.Remaining.Gt(0)).Count()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ModifyActivityError, err.Error()))
return
}
if count > 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ModifyActivityError, "该活动还有 "+strconv.FormatInt(count, 10)+" 张未使用的次数卡,确认强制下架吗?请勾选强制执行。"))
return
}
}
if err := h.activity.ModifyActivity(ctx.RequestContext(), id, activitysvc.ModifyActivityInput{
Name: req.Name,
Banner: req.Banner,
Image: req.Image,
GameplayIntro: req.GameplayIntro,
ActivityCategoryID: req.ActivityCategoryID,
Status: req.Status,
PriceDraw: req.PriceDraw,
IsBoss: req.IsBoss,
StartTime: st,
EndTime: et,
AllowItemCards: req.AllowItemCards,
AllowCoupons: req.AllowCoupons,
}); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ModifyActivityError, err.Error()))
return
}
if req.DrawMode != "" {
var stPtr *time.Time
if req.ScheduledDelayMinutes > 0 {
t := time.Now().Add(time.Duration(req.ScheduledDelayMinutes) * time.Minute)
stPtr = &t
} else if req.IntervalMinutes > 0 && req.ScheduledTime == "" {
t := time.Now().Add(time.Duration(req.IntervalMinutes) * time.Minute)
stPtr = &t
} else if req.ScheduledTime != "" {
if t, err := time.Parse(time.RFC3339, req.ScheduledTime); err == nil {
stPtr = &t
} else if t2, err2 := time.ParseInLocation("2006-01-02 15:04:05", req.ScheduledTime, time.Local); err2 == nil {
stPtr = &t2
}
}
if err := h.activity.SaveActivityDrawConfig(ctx.RequestContext(), id, activitysvc.DrawConfig{PlayType: req.PlayType, DrawMode: req.DrawMode, MinParticipants: req.MinParticipants, IntervalMinutes: req.IntervalMinutes, ScheduledTime: stPtr, RefundCouponID: req.RefundCouponID}); err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.ModifyActivityError, err.Error()))
return
}
}
res.Message = "操作成功"
ctx.Payload(res)
}
}
// DeleteActivity 删除活动
// @Summary 删除活动
// @Description 删除指定活动
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Success 200 {object} simpleMessageResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id} [delete]
// @Security LoginVerifyToken
func (h *handler) DeleteActivity() core.HandlerFunc {
return func(ctx core.Context) {
res := new(simpleMessageResponse)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.DeleteActivityError, "禁止操作"))
return
}
id, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
return
}
// 检查是否有未使用的次数卡
ugp := h.readDB.UserGamePasses
count, err := ugp.WithContext(ctx.RequestContext()).Where(ugp.ActivityID.Eq(id), ugp.Remaining.Gt(0)).Count()
if err != nil {
ctx.AbortWithError(core.Error(http.StatusInternalServerError, code.DeleteActivityError, err.Error()))
return
}
if count > 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.DeleteActivityError, "该活动还有 "+strconv.FormatInt(count, 10)+" 张未使用的次数卡,禁止删除。请先处理或等待过期。"))
return
}
if err := h.activity.DeleteActivity(ctx.RequestContext(), id); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.DeleteActivityError, err.Error()))
return
}
res.Message = "操作成功"
ctx.Payload(res)
}
}
// GetActivityDetail 查看活动详情
// @Summary 查看活动详情
// @Description 查看指定活动的详细信息
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Success 200 {object} activityDetailResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id} [get]
// @Security LoginVerifyToken
func (h *handler) GetActivityDetail() core.HandlerFunc {
return func(ctx core.Context) {
id, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
return
}
item, err := h.activity.GetActivity(ctx.RequestContext(), id)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.GetActivityError, err.Error()))
return
}
rsp := &activityDetailResponse{
ID: item.ID,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
Name: item.Name,
Banner: item.Banner,
ActivityCategoryID: item.ActivityCategoryID,
Status: item.Status,
PriceDraw: item.PriceDraw,
IsBoss: item.IsBoss,
StartTime: item.StartTime,
EndTime: item.EndTime,
DrawMode: item.DrawMode,
PlayType: item.PlayType,
MinParticipants: item.MinParticipants,
IntervalMinutes: item.IntervalMinutes,
ScheduledTime: item.ScheduledTime,
LastSettledAt: item.LastSettledAt,
RefundCouponID: item.RefundCouponID,
Image: item.Image,
GameplayIntro: item.GameplayIntro,
AllowItemCards: item.AllowItemCards,
AllowCoupons: item.AllowCoupons,
}
ctx.Payload(rsp)
}
}
type copyActivityResponse struct {
NewActivityID int64 `json:"new_activity_id"`
Status string `json:"status"`
}
// CopyActivity 复制活动
// @Summary 复制活动
// @Description 根据活动ID深度复制期次与奖品生成新的活动
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param activity_id path integer true "活动ID"
// @Success 200 {object} copyActivityResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities/{activity_id}/copy [post]
// @Security LoginVerifyToken
func (h *handler) CopyActivity() core.HandlerFunc {
return func(ctx core.Context) {
res := new(copyActivityResponse)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateActivityError, "禁止操作"))
return
}
id, err := strconv.ParseInt(ctx.Param("activity_id"), 10, 64)
if err != nil || id <= 0 {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "未传递活动ID"))
return
}
newID, err := h.activity.CopyActivity(ctx.RequestContext(), id)
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.CreateActivityError, err.Error()))
return
}
res.NewActivityID = newID
res.Status = "success"
ctx.Payload(res)
}
}
type listActivitiesRequest struct {
Name string `form:"name"`
CategoryID int64 `form:"category_id"`
IsBoss int32 `form:"is_boss"`
Status int32 `form:"status"`
Page int `form:"page"`
PageSize int `form:"page_size"`
}
type activityItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
Banner string `json:"banner"`
Image string `json:"image"`
ActivityCategoryID int64 `json:"activity_category_id"`
CategoryName string `json:"category_name"`
Status int32 `json:"status"`
PriceDraw int64 `json:"price_draw"`
IsBoss int32 `json:"is_boss"`
}
type listActivitiesResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []activityItem `json:"list"`
}
// ListActivities 活动列表
// @Summary 活动列表
// @Description 获取活动列表支持分类、Boss、状态过滤与分页
// @Tags 管理端.活动
// @Accept json
// @Produce json
// @Param name query string false "活动名称(模糊)"
// @Param category_id query int false "活动分类ID"
// @Param is_boss query int false "是否Boss(0/1)"
// @Param status query int false "状态(1进行中 2下线)"
// @Param page query int true "页码" default(1)
// @Param page_size query int true "每页数量最多100" default(20)
// @Success 200 {object} listActivitiesResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/activities [get]
// @Security LoginVerifyToken
func (h *handler) ListActivities() core.HandlerFunc {
return func(ctx core.Context) {
req := new(listActivitiesRequest)
res := new(listActivitiesResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, validation.Error(err)))
return
}
var isBossPtr *int32
if req.IsBoss == 0 || req.IsBoss == 1 {
isBossPtr = &req.IsBoss
}
var statusPtr *int32
if req.Status == 1 || req.Status == 2 {
statusPtr = &req.Status
}
items, total, err := h.activity.ListActivities(ctx.RequestContext(), struct {
Name string
CategoryID int64
IsBoss *int32
Status *int32
Page int
PageSize int
}{Name: req.Name, CategoryID: req.CategoryID, IsBoss: isBossPtr, Status: statusPtr, Page: req.Page, PageSize: req.PageSize})
if err != nil {
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ListActivitiesError, err.Error()))
return
}
res.Page = req.Page
res.PageSize = req.PageSize
res.Total = total
res.List = make([]activityItem, len(items))
// collect category ids
var catIDs []int64
catSet := make(map[int64]struct{})
for _, v := range items {
if v.ActivityCategoryID != 0 {
if _, ok := catSet[v.ActivityCategoryID]; !ok {
catSet[v.ActivityCategoryID] = struct{}{}
catIDs = append(catIDs, v.ActivityCategoryID)
}
}
}
nameMap, _ := h.activity.GetCategoryNames(ctx.RequestContext(), catIDs)
for i, v := range items {
res.List[i] = activityItem{
ID: v.ID,
Name: v.Name,
Banner: v.Banner,
Image: v.Image,
ActivityCategoryID: v.ActivityCategoryID,
CategoryName: nameMap[v.ActivityCategoryID],
Status: v.Status,
PriceDraw: v.PriceDraw,
IsBoss: v.IsBoss,
}
}
ctx.Payload(res)
}
}