Add promo code models and APIs, persist selected promo codes on top-up orders, and apply bonus quota idempotently when payments complete.
238 lines
5.4 KiB
Go
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
|
|
}
|