193 lines
5.1 KiB
Go
193 lines
5.1 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"quyun/v2/app/errorx"
|
|
coupon_dto "quyun/v2/app/http/v1/dto"
|
|
"quyun/v2/database/models"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// @provider
|
|
type coupon struct{}
|
|
|
|
func (s *coupon) ListUserCoupons(
|
|
ctx context.Context,
|
|
tenantID int64,
|
|
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)
|
|
}
|
|
if len(list) == 0 {
|
|
return []coupon_dto.UserCouponItem{}, nil
|
|
}
|
|
|
|
couponIDSet := make(map[int64]struct{}, len(list))
|
|
couponIDs := make([]int64, 0, len(list))
|
|
for _, v := range list {
|
|
if _, ok := couponIDSet[v.CouponID]; ok {
|
|
continue
|
|
}
|
|
couponIDs = append(couponIDs, v.CouponID)
|
|
couponIDSet[v.CouponID] = struct{}{}
|
|
}
|
|
|
|
cTbl, cQ := models.CouponQuery.QueryContext(ctx)
|
|
cQ = cQ.Where(cTbl.ID.In(couponIDs...))
|
|
if tenantID > 0 {
|
|
cQ = cQ.Where(cTbl.TenantID.Eq(tenantID))
|
|
}
|
|
coupons, err := cQ.Find()
|
|
if err != nil {
|
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
|
}
|
|
|
|
couponMap := make(map[int64]*models.Coupon, len(coupons))
|
|
for _, c := range coupons {
|
|
couponMap[c.ID] = c
|
|
}
|
|
|
|
res := make([]coupon_dto.UserCouponItem, 0, len(list))
|
|
for _, v := range list {
|
|
c, ok := couponMap[v.CouponID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
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, tenantID, 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("优惠券信息缺失")
|
|
}
|
|
if tenantID > 0 && c.TenantID != tenantID {
|
|
return 0, errorx.ErrForbidden.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, tenantID, userCouponID, orderID int64) error {
|
|
uc, err := tx.UserCoupon.WithContext(ctx).Where(tx.UserCoupon.ID.Eq(userCouponID)).First()
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return errorx.ErrRecordNotFound.WithMsg("优惠券不存在")
|
|
}
|
|
return errorx.ErrDatabaseError.WithCause(err)
|
|
}
|
|
if uc.Status != "unused" {
|
|
return errorx.ErrBusinessLogic.WithMsg("优惠券核销失败")
|
|
}
|
|
|
|
c, err := tx.Coupon.WithContext(ctx).Where(tx.Coupon.ID.Eq(uc.CouponID)).First()
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return errorx.ErrRecordNotFound.WithMsg("优惠券信息缺失")
|
|
}
|
|
return errorx.ErrDatabaseError.WithCause(err)
|
|
}
|
|
if tenantID > 0 && c.TenantID != tenantID {
|
|
return errorx.ErrForbidden.WithMsg("优惠券租户不匹配")
|
|
}
|
|
|
|
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)
|
|
_, _ = tx.Coupon.WithContext(ctx).Where(tx.Coupon.ID.Eq(uc.CouponID)).UpdateSimple(tx.Coupon.UsedQuantity.Add(1))
|
|
|
|
return nil
|
|
}
|