refactor(utils): 修复密码哈希比较逻辑错误 feat(user): 新增按状态筛选优惠券接口 docs: 添加虚拟发货与任务中心相关文档 fix(wechat): 修正Code2Session上下文传递问题 test: 补充订单折扣与积分转换测试用例 build: 更新配置文件与构建脚本 style: 清理多余的空行与注释
525 lines
18 KiB
Go
525 lines
18 KiB
Go
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)
|
||
}
|
||
}
|