new-api/controller/promo_code.go
zizi 1de1cdb14f feat: add promo code bonus flow
Add promo code models and APIs, persist selected promo codes on top-up orders, and apply bonus quota idempotently when payments complete.
2026-05-20 12:57:54 +08:00

238 lines
5.4 KiB
Go

package controller
import (
"errors"
"strconv"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type createPromoCodeRequest struct {
Code string `json:"code"`
BonusAmount int `json:"bonus_amount"`
MaxUses int `json:"max_uses"`
MinRechargeAmount int `json:"min_recharge_amount"`
ExpiresAt string `json:"expires_at"`
Notes string `json:"notes"`
}
type updatePromoCodeRequest struct {
Status *int `json:"status"`
MaxUses *int `json:"max_uses"`
MinRechargeAmount *int `json:"min_recharge_amount"`
ExpiresAt *string `json:"expires_at"`
Notes *string `json:"notes"`
}
func GetPromoCodes(c *gin.Context) {
pageInfo := common.GetPageQuery(c)
keyword := strings.TrimSpace(c.Query("keyword"))
var status *int
if statusText := strings.TrimSpace(c.Query("status")); statusText != "" {
statusValue, err := strconv.Atoi(statusText)
if err != nil {
common.ApiErrorMsg(c, "status 参数格式不正确")
return
}
status = &statusValue
}
promoCodes, total, err := model.GetPromoCodes(pageInfo.GetPage(), pageInfo.GetPageSize(), status, keyword)
if err != nil {
common.ApiError(c, err)
return
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(promoCodes)
common.ApiSuccess(c, pageInfo)
}
func CreatePromoCode(c *gin.Context) {
var req createPromoCodeRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiError(c, err)
return
}
code := normalizePromoCode(req.Code)
if code == "" {
common.ApiErrorMsg(c, "优惠码不能为空")
return
}
if len(code) > 32 {
common.ApiErrorMsg(c, "优惠码长度不能超过 32 个字符")
return
}
if req.BonusAmount <= 0 {
common.ApiErrorMsg(c, "赠送额度必须大于 0")
return
}
if req.MaxUses < 0 {
common.ApiErrorMsg(c, "最大使用次数不能小于 0")
return
}
if req.MinRechargeAmount < 0 {
common.ApiErrorMsg(c, "最低充值金额不能小于 0")
return
}
if _, err := model.GetPromoCodeByCode(code); err == nil {
common.ApiErrorMsg(c, "优惠码已存在")
return
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
common.ApiError(c, err)
return
}
promoCode := &model.PromoCode{
Code: code,
BonusAmount: req.BonusAmount,
MaxUses: req.MaxUses,
MinRechargeAmount: req.MinRechargeAmount,
Notes: req.Notes,
Status: 1,
}
if strings.TrimSpace(req.ExpiresAt) != "" {
expiresAt, err := parsePromoCodeExpiresAt(req.ExpiresAt)
if err != nil {
common.ApiErrorMsg(c, "expires_at 必须是 RFC3339 格式")
return
}
promoCode.ExpiresAt = expiresAt
}
if err := promoCode.Insert(); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, promoCode)
}
func GetPromoCode(c *gin.Context) {
promoCode, ok := getPromoCodeFromParam(c)
if !ok {
return
}
common.ApiSuccess(c, promoCode)
}
func UpdatePromoCode(c *gin.Context) {
promoCode, ok := getPromoCodeFromParam(c)
if !ok {
return
}
var req updatePromoCodeRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiError(c, err)
return
}
if req.Status != nil {
promoCode.Status = *req.Status
}
if req.MaxUses != nil {
if *req.MaxUses < 0 {
common.ApiErrorMsg(c, "最大使用次数不能小于 0")
return
}
promoCode.MaxUses = *req.MaxUses
}
if req.MinRechargeAmount != nil {
if *req.MinRechargeAmount < 0 {
common.ApiErrorMsg(c, "最低充值金额不能小于 0")
return
}
promoCode.MinRechargeAmount = *req.MinRechargeAmount
}
if req.ExpiresAt != nil {
expiresAtText := strings.TrimSpace(*req.ExpiresAt)
if expiresAtText == "" {
promoCode.ExpiresAt = nil
} else {
expiresAt, err := parsePromoCodeExpiresAt(expiresAtText)
if err != nil {
common.ApiErrorMsg(c, "expires_at 必须是 RFC3339 格式")
return
}
promoCode.ExpiresAt = expiresAt
}
}
if req.Notes != nil {
promoCode.Notes = *req.Notes
}
if err := promoCode.Update(); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, promoCode)
}
func DeletePromoCode(c *gin.Context) {
promoCode, ok := getPromoCodeFromParam(c)
if !ok {
return
}
promoCode.Status = 0
if err := promoCode.Update(); err != nil {
common.ApiError(c, err)
return
}
common.ApiSuccess(c, nil)
}
func GetPromoCodeUsages(c *gin.Context) {
promoCode, ok := getPromoCodeFromParam(c)
if !ok {
return
}
pageInfo := common.GetPageQuery(c)
usages, total, err := model.GetPromoCodeUsages(pageInfo.GetPage(), pageInfo.GetPageSize(), promoCode.Id)
if err != nil {
common.ApiError(c, err)
return
}
pageInfo.SetTotal(int(total))
pageInfo.SetItems(usages)
common.ApiSuccess(c, pageInfo)
}
func getPromoCodeFromParam(c *gin.Context) (*model.PromoCode, bool) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
common.ApiError(c, err)
return nil, false
}
promoCode, err := model.GetPromoCodeByID(id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
common.ApiErrorMsg(c, "优惠码不存在")
return nil, false
}
common.ApiError(c, err)
return nil, false
}
return promoCode, true
}
func normalizePromoCode(code string) string {
return strings.ToUpper(strings.TrimSpace(code))
}
func parsePromoCodeExpiresAt(value string) (*time.Time, error) {
expiresAt, err := time.Parse(time.RFC3339, strings.TrimSpace(value))
if err != nil {
return nil, err
}
return &expiresAt, nil
}