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"` } // 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 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 } 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) } }