- 支持创建/编辑/删除优惠码,设置赠送金额和使用限制 - 注册页面实时验证优惠码并显示赠送金额 - 支持 URL 参数自动填充 (?promo=CODE) - 添加优惠码验证接口速率限制 - 使用数据库行锁防止并发超限 - 新增后台优惠码管理页面,支持复制注册链接
210 lines
5.6 KiB
Go
210 lines
5.6 KiB
Go
package admin
|
||
|
||
import (
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// PromoHandler handles admin promo code management
|
||
type PromoHandler struct {
|
||
promoService *service.PromoService
|
||
}
|
||
|
||
// NewPromoHandler creates a new admin promo handler
|
||
func NewPromoHandler(promoService *service.PromoService) *PromoHandler {
|
||
return &PromoHandler{
|
||
promoService: promoService,
|
||
}
|
||
}
|
||
|
||
// CreatePromoCodeRequest represents create promo code request
|
||
type CreatePromoCodeRequest struct {
|
||
Code string `json:"code"` // 可选,为空则自动生成
|
||
BonusAmount float64 `json:"bonus_amount" binding:"required,min=0"` // 赠送余额
|
||
MaxUses int `json:"max_uses" binding:"min=0"` // 最大使用次数,0=无限
|
||
ExpiresAt *int64 `json:"expires_at"` // 过期时间戳(秒)
|
||
Notes string `json:"notes"` // 备注
|
||
}
|
||
|
||
// UpdatePromoCodeRequest represents update promo code request
|
||
type UpdatePromoCodeRequest struct {
|
||
Code *string `json:"code"`
|
||
BonusAmount *float64 `json:"bonus_amount" binding:"omitempty,min=0"`
|
||
MaxUses *int `json:"max_uses" binding:"omitempty,min=0"`
|
||
Status *string `json:"status" binding:"omitempty,oneof=active disabled"`
|
||
ExpiresAt *int64 `json:"expires_at"`
|
||
Notes *string `json:"notes"`
|
||
}
|
||
|
||
// List handles listing all promo codes with pagination
|
||
// GET /api/v1/admin/promo-codes
|
||
func (h *PromoHandler) List(c *gin.Context) {
|
||
page, pageSize := response.ParsePagination(c)
|
||
status := c.Query("status")
|
||
search := strings.TrimSpace(c.Query("search"))
|
||
if len(search) > 100 {
|
||
search = search[:100]
|
||
}
|
||
|
||
params := pagination.PaginationParams{
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
}
|
||
|
||
codes, paginationResult, err := h.promoService.List(c.Request.Context(), params, status, search)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
|
||
out := make([]dto.PromoCode, 0, len(codes))
|
||
for i := range codes {
|
||
out = append(out, *dto.PromoCodeFromService(&codes[i]))
|
||
}
|
||
response.Paginated(c, out, paginationResult.Total, page, pageSize)
|
||
}
|
||
|
||
// GetByID handles getting a promo code by ID
|
||
// GET /api/v1/admin/promo-codes/:id
|
||
func (h *PromoHandler) GetByID(c *gin.Context) {
|
||
codeID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "Invalid promo code ID")
|
||
return
|
||
}
|
||
|
||
code, err := h.promoService.GetByID(c.Request.Context(), codeID)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
|
||
response.Success(c, dto.PromoCodeFromService(code))
|
||
}
|
||
|
||
// Create handles creating a new promo code
|
||
// POST /api/v1/admin/promo-codes
|
||
func (h *PromoHandler) Create(c *gin.Context) {
|
||
var req CreatePromoCodeRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||
return
|
||
}
|
||
|
||
input := &service.CreatePromoCodeInput{
|
||
Code: req.Code,
|
||
BonusAmount: req.BonusAmount,
|
||
MaxUses: req.MaxUses,
|
||
Notes: req.Notes,
|
||
}
|
||
|
||
if req.ExpiresAt != nil {
|
||
t := time.Unix(*req.ExpiresAt, 0)
|
||
input.ExpiresAt = &t
|
||
}
|
||
|
||
code, err := h.promoService.Create(c.Request.Context(), input)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
|
||
response.Success(c, dto.PromoCodeFromService(code))
|
||
}
|
||
|
||
// Update handles updating a promo code
|
||
// PUT /api/v1/admin/promo-codes/:id
|
||
func (h *PromoHandler) Update(c *gin.Context) {
|
||
codeID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "Invalid promo code ID")
|
||
return
|
||
}
|
||
|
||
var req UpdatePromoCodeRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||
return
|
||
}
|
||
|
||
input := &service.UpdatePromoCodeInput{
|
||
Code: req.Code,
|
||
BonusAmount: req.BonusAmount,
|
||
MaxUses: req.MaxUses,
|
||
Status: req.Status,
|
||
Notes: req.Notes,
|
||
}
|
||
|
||
if req.ExpiresAt != nil {
|
||
if *req.ExpiresAt == 0 {
|
||
// 0 表示清除过期时间
|
||
input.ExpiresAt = nil
|
||
} else {
|
||
t := time.Unix(*req.ExpiresAt, 0)
|
||
input.ExpiresAt = &t
|
||
}
|
||
}
|
||
|
||
code, err := h.promoService.Update(c.Request.Context(), codeID, input)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
|
||
response.Success(c, dto.PromoCodeFromService(code))
|
||
}
|
||
|
||
// Delete handles deleting a promo code
|
||
// DELETE /api/v1/admin/promo-codes/:id
|
||
func (h *PromoHandler) Delete(c *gin.Context) {
|
||
codeID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "Invalid promo code ID")
|
||
return
|
||
}
|
||
|
||
err = h.promoService.Delete(c.Request.Context(), codeID)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
|
||
response.Success(c, gin.H{"message": "Promo code deleted successfully"})
|
||
}
|
||
|
||
// GetUsages handles getting usage records for a promo code
|
||
// GET /api/v1/admin/promo-codes/:id/usages
|
||
func (h *PromoHandler) GetUsages(c *gin.Context) {
|
||
codeID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
response.BadRequest(c, "Invalid promo code ID")
|
||
return
|
||
}
|
||
|
||
page, pageSize := response.ParsePagination(c)
|
||
params := pagination.PaginationParams{
|
||
Page: page,
|
||
PageSize: pageSize,
|
||
}
|
||
|
||
usages, paginationResult, err := h.promoService.ListUsages(c.Request.Context(), codeID, params)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
|
||
out := make([]dto.PromoCodeUsage, 0, len(usages))
|
||
for i := range usages {
|
||
out = append(out, *dto.PromoCodeUsageFromService(&usages[i]))
|
||
}
|
||
response.Paginated(c, out, paginationResult.Total, page, pageSize)
|
||
}
|