Files
quyun-v2/backend/app/services/coupon.go
2026-01-08 09:57:04 +08:00

136 lines
3.6 KiB
Go

package services
import (
"context"
"time"
"quyun/v2/app/errorx"
coupon_dto "quyun/v2/app/http/v1/dto"
"quyun/v2/database/models"
)
// @provider
type coupon struct{}
func (s *coupon) ListUserCoupons(
ctx context.Context,
userID int64,
status string,
) ([]coupon_dto.UserCouponItem, error) {
if userID == 0 {
return nil, errorx.ErrUnauthorized
}
uid := userID
tbl, q := models.UserCouponQuery.QueryContext(ctx)
q = q.Where(tbl.UserID.Eq(uid))
if status != "" {
q = q.Where(tbl.Status.Eq(status))
}
list, err := q.Order(tbl.CreatedAt.Desc()).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
var res []coupon_dto.UserCouponItem
for _, v := range list {
c, _ := models.CouponQuery.WithContext(ctx).Where(models.CouponQuery.ID.Eq(v.CouponID)).First()
item := coupon_dto.UserCouponItem{
ID: v.ID,
CouponID: v.CouponID,
Status: v.Status,
}
if c != nil {
item.Title = c.Title
item.Description = c.Description
item.Type = c.Type
item.Value = c.Value
item.MinOrderAmount = c.MinOrderAmount
if !c.StartAt.IsZero() {
item.StartAt = c.StartAt.Format(time.RFC3339)
}
if !c.EndAt.IsZero() {
item.EndAt = c.EndAt.Format(time.RFC3339)
}
}
res = append(res, item)
}
return res, nil
}
// Validate checks if a coupon can be used for an order and returns the discount amount
func (s *coupon) Validate(ctx context.Context, userID, userCouponID, amount int64) (int64, error) {
uc, err := models.UserCouponQuery.WithContext(ctx).Where(models.UserCouponQuery.ID.Eq(userCouponID)).First()
if err != nil {
return 0, errorx.ErrRecordNotFound.WithMsg("优惠券不存在")
}
if uc.UserID != userID {
return 0, errorx.ErrUnauthorized.WithMsg("无权使用该优惠券")
}
if uc.Status != "unused" {
return 0, errorx.ErrBusinessLogic.WithMsg("优惠券已使用或失效")
}
c, err := models.CouponQuery.WithContext(ctx).Where(models.CouponQuery.ID.Eq(uc.CouponID)).First()
if err != nil {
return 0, errorx.ErrRecordNotFound.WithMsg("优惠券信息缺失")
}
now := time.Now()
if !c.StartAt.IsZero() && now.Before(c.StartAt) {
return 0, errorx.ErrBusinessLogic.WithMsg("优惠券尚未生效")
}
if !c.EndAt.IsZero() && now.After(c.EndAt) {
return 0, errorx.ErrBusinessLogic.WithMsg("优惠券已过期")
}
if amount < c.MinOrderAmount {
return 0, errorx.ErrBusinessLogic.WithMsg("未达到优惠券使用门槛")
}
var discount int64
if c.Type == "fix_amount" {
discount = c.Value
} else if c.Type == "discount" {
discount = (amount * c.Value) / 100
if c.MaxDiscount > 0 && discount > c.MaxDiscount {
discount = c.MaxDiscount
}
}
// Discount cannot exceed order amount
if discount > amount {
discount = amount
}
return discount, nil
}
// MarkUsed marks a user coupon as used (intended to be called inside a transaction)
func (s *coupon) MarkUsed(ctx context.Context, tx *models.Query, userCouponID, orderID int64) error {
now := time.Now()
// Update User Coupon
info, err := tx.UserCoupon.WithContext(ctx).
Where(tx.UserCoupon.ID.Eq(userCouponID), tx.UserCoupon.Status.Eq("unused")).
Updates(&models.UserCoupon{
Status: "used",
OrderID: orderID,
UsedAt: now,
})
if err != nil {
return err
}
if info.RowsAffected == 0 {
return errorx.ErrBusinessLogic.WithMsg("优惠券核销失败")
}
// Update Coupon used quantity (Optional, but good for stats)
// We need CouponID from uc
uc, _ := tx.UserCoupon.WithContext(ctx).Where(tx.UserCoupon.ID.Eq(userCouponID)).First()
_, _ = tx.Coupon.WithContext(ctx).Where(tx.Coupon.ID.Eq(uc.CouponID)).UpdateSimple(tx.Coupon.UsedQuantity.Add(1))
return nil
}