Files
quyun-v2/backend/app/services/coupon.go

870 lines
24 KiB
Go

package services
import (
"context"
"errors"
"strings"
"time"
"quyun/v2/app/errorx"
coupon_dto "quyun/v2/app/http/v1/dto"
"quyun/v2/app/requests"
"quyun/v2/database/models"
"quyun/v2/pkg/consts"
"go.ipao.vip/gen/field"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// @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
}
statusFilter := strings.TrimSpace(status)
if statusFilter != "" && statusFilter != "all" {
if _, err := consts.ParseUserCouponStatus(statusFilter); err != nil {
return nil, errorx.ErrInvalidParameter.WithMsg("状态参数无效")
}
}
tbl, q := models.UserCouponQuery.QueryContext(ctx)
q = q.Where(tbl.UserID.Eq(userID))
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
}
couponMap, err := s.fetchCouponMap(ctx, tenantID, list)
if err != nil {
return nil, err
}
now := time.Now()
res := make([]coupon_dto.UserCouponItem, 0, len(list))
for _, v := range list {
if v == nil {
continue
}
c, ok := couponMap[v.CouponID]
if !ok {
continue
}
finalStatus := v.Status
if s.isCouponExpired(c, now) && v.Status == consts.UserCouponStatusUnused {
if err := s.markUserCouponExpired(ctx, v.ID); err != nil {
return nil, err
}
finalStatus = consts.UserCouponStatusExpired
}
if statusFilter != "" && statusFilter != "all" && string(finalStatus) != statusFilter {
continue
}
res = append(res, s.composeUserCouponItem(v, c, finalStatus))
}
return res, nil
}
func (s *coupon) ListAvailable(
ctx context.Context,
tenantID int64,
userID int64,
amount int64,
) ([]coupon_dto.UserCouponItem, error) {
if userID == 0 {
return nil, errorx.ErrUnauthorized
}
if amount <= 0 {
return nil, errorx.ErrInvalidParameter.WithMsg("订单金额无效")
}
tbl, q := models.UserCouponQuery.QueryContext(ctx)
q = q.Where(tbl.UserID.Eq(userID), tbl.Status.Eq(consts.UserCouponStatusUnused))
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
}
couponMap, err := s.fetchCouponMap(ctx, tenantID, list)
if err != nil {
return nil, err
}
now := time.Now()
res := make([]coupon_dto.UserCouponItem, 0, len(list))
for _, v := range list {
if v == nil {
continue
}
c, ok := couponMap[v.CouponID]
if !ok {
continue
}
if !s.isCouponActive(c, now) {
if s.isCouponExpired(c, now) && v.Status == consts.UserCouponStatusUnused {
if err := s.markUserCouponExpired(ctx, v.ID); err != nil {
return nil, err
}
}
continue
}
if amount < c.MinOrderAmount {
continue
}
res = append(res, s.composeUserCouponItem(v, c, consts.UserCouponStatusUnused))
}
return res, nil
}
func (s *coupon) Receive(
ctx context.Context,
tenantID int64,
userID int64,
couponID int64,
) (*coupon_dto.UserCouponItem, error) {
if userID == 0 {
return nil, errorx.ErrUnauthorized
}
if couponID == 0 {
return nil, errorx.ErrInvalidParameter.WithMsg("优惠券不存在")
}
var result *coupon_dto.UserCouponItem
err := models.Q.Transaction(func(tx *models.Query) error {
// 锁定优惠券行,防止并发超发。
tbl, q := tx.Coupon.QueryContext(ctx)
q = q.Where(tbl.ID.Eq(couponID))
if tenantID > 0 {
q = q.Where(tbl.TenantID.Eq(tenantID))
}
coupon, err := q.Clauses(clause.Locking{Strength: "UPDATE"}).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound.WithMsg("优惠券不存在")
}
return errorx.ErrDatabaseError.WithCause(err)
}
if tenantID > 0 && coupon.TenantID != tenantID {
return errorx.ErrForbidden.WithMsg("优惠券租户不匹配")
}
now := time.Now()
if !coupon.StartAt.IsZero() && now.Before(coupon.StartAt) {
return errorx.ErrBusinessLogic.WithMsg("优惠券尚未生效")
}
if !coupon.EndAt.IsZero() && now.After(coupon.EndAt) {
return errorx.ErrBusinessLogic.WithMsg("优惠券已过期")
}
existing, err := tx.UserCoupon.WithContext(ctx).
Where(tx.UserCoupon.UserID.Eq(userID), tx.UserCoupon.CouponID.Eq(coupon.ID)).
First()
if err == nil {
item := s.composeUserCouponItem(existing, coupon, existing.Status)
result = &item
return nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrDatabaseError.WithCause(err)
}
if coupon.TotalQuantity > 0 {
count, err := tx.UserCoupon.WithContext(ctx).
Where(tx.UserCoupon.CouponID.Eq(coupon.ID)).
Count()
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
if int32(count) >= coupon.TotalQuantity {
return errorx.ErrBusinessLogic.WithMsg("优惠券已领完")
}
}
uc := &models.UserCoupon{
UserID: userID,
CouponID: coupon.ID,
Status: consts.UserCouponStatusUnused,
}
if err := tx.UserCoupon.WithContext(ctx).Create(uc); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
item := s.composeUserCouponItem(uc, coupon, consts.UserCouponStatusUnused)
result = &item
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
func (s *coupon) Create(
ctx context.Context,
tenantID int64,
userID int64,
form *coupon_dto.CouponCreateForm,
) (*coupon_dto.CouponItem, error) {
if tenantID == 0 {
return nil, errorx.ErrForbidden.WithMsg("租户未绑定")
}
if form == nil {
return nil, errorx.ErrInvalidParameter.WithMsg("参数无效")
}
_ = userID
title := strings.TrimSpace(form.Title)
if title == "" {
return nil, errorx.ErrInvalidParameter.WithMsg("优惠券标题不能为空")
}
couponType, err := s.parseCouponType(form.Type)
if err != nil {
return nil, err
}
if err := s.validateCouponValue(couponType, form.Value, form.MaxDiscount); err != nil {
return nil, err
}
if form.MinOrderAmount < 0 {
return nil, errorx.ErrInvalidParameter.WithMsg("使用门槛无效")
}
if form.TotalQuantity < 0 {
return nil, errorx.ErrInvalidParameter.WithMsg("发行数量无效")
}
startAt, err := s.parseTime(form.StartAt)
if err != nil {
return nil, err
}
endAt, err := s.parseTime(form.EndAt)
if err != nil {
return nil, err
}
if !startAt.IsZero() && !endAt.IsZero() && startAt.After(endAt) {
return nil, errorx.ErrInvalidParameter.WithMsg("生效时间不能晚于过期时间")
}
coupon := &models.Coupon{
TenantID: tenantID,
Title: title,
Description: strings.TrimSpace(form.Description),
Type: couponType,
Value: form.Value,
MinOrderAmount: form.MinOrderAmount,
MaxDiscount: form.MaxDiscount,
TotalQuantity: form.TotalQuantity,
UsedQuantity: 0,
StartAt: startAt,
EndAt: endAt,
}
if err := models.CouponQuery.WithContext(ctx).Create(coupon); err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
item := s.composeCouponItem(coupon)
return &item, nil
}
func (s *coupon) Update(
ctx context.Context,
tenantID int64,
userID int64,
id int64,
form *coupon_dto.CouponUpdateForm,
) (*coupon_dto.CouponItem, error) {
if tenantID == 0 {
return nil, errorx.ErrForbidden.WithMsg("租户未绑定")
}
if id == 0 || form == nil {
return nil, errorx.ErrInvalidParameter.WithMsg("参数无效")
}
_ = userID
var result *coupon_dto.CouponItem
err := models.Q.Transaction(func(tx *models.Query) error {
tbl, q := tx.Coupon.QueryContext(ctx)
q = q.Where(tbl.ID.Eq(id), tbl.TenantID.Eq(tenantID))
coupon, err := q.Clauses(clause.Locking{Strength: "UPDATE"}).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound.WithMsg("优惠券不存在")
}
return errorx.ErrDatabaseError.WithCause(err)
}
now := time.Now()
claimedCount, err := tx.UserCoupon.WithContext(ctx).
Where(tx.UserCoupon.CouponID.Eq(coupon.ID)).
Count()
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
if claimedCount > 0 || (!coupon.StartAt.IsZero() && now.After(coupon.StartAt)) {
return errorx.ErrBusinessLogic.WithMsg("优惠券已领取或已生效,禁止修改")
}
updates := make([]field.AssignExpr, 0)
nextType := coupon.Type
nextValue := coupon.Value
nextMaxDiscount := coupon.MaxDiscount
startAt := coupon.StartAt
endAt := coupon.EndAt
if form.Title != nil {
value := strings.TrimSpace(*form.Title)
if value == "" {
return errorx.ErrInvalidParameter.WithMsg("优惠券标题不能为空")
}
updates = append(updates, tbl.Title.Value(value))
}
if form.Description != nil {
updates = append(updates, tbl.Description.Value(strings.TrimSpace(*form.Description)))
}
if form.Type != nil {
parsed, err := s.parseCouponType(*form.Type)
if err != nil {
return err
}
nextType = parsed
updates = append(updates, tbl.Type.Value(parsed))
}
if form.Value != nil {
nextValue = *form.Value
updates = append(updates, tbl.Value.Value(*form.Value))
}
if form.MinOrderAmount != nil {
if *form.MinOrderAmount < 0 {
return errorx.ErrInvalidParameter.WithMsg("使用门槛无效")
}
updates = append(updates, tbl.MinOrderAmount.Value(*form.MinOrderAmount))
}
if form.MaxDiscount != nil {
nextMaxDiscount = *form.MaxDiscount
updates = append(updates, tbl.MaxDiscount.Value(*form.MaxDiscount))
}
if form.TotalQuantity != nil {
if *form.TotalQuantity < 0 {
return errorx.ErrInvalidParameter.WithMsg("发行数量无效")
}
if *form.TotalQuantity > 0 && int32(claimedCount) > *form.TotalQuantity {
return errorx.ErrBusinessLogic.WithMsg("发行数量不足以覆盖已领取数量")
}
updates = append(updates, tbl.TotalQuantity.Value(*form.TotalQuantity))
}
if form.StartAt != nil {
parsed, err := s.parseTime(*form.StartAt)
if err != nil {
return err
}
startAt = parsed
updates = append(updates, tbl.StartAt.Value(parsed))
}
if form.EndAt != nil {
parsed, err := s.parseTime(*form.EndAt)
if err != nil {
return err
}
endAt = parsed
updates = append(updates, tbl.EndAt.Value(parsed))
}
if err := s.validateCouponValue(nextType, nextValue, nextMaxDiscount); err != nil {
return err
}
if !startAt.IsZero() && !endAt.IsZero() && startAt.After(endAt) {
return errorx.ErrInvalidParameter.WithMsg("生效时间不能晚于过期时间")
}
if len(updates) == 0 {
resultItem := s.composeCouponItem(coupon)
result = &resultItem
return nil
}
updates = append(updates, tbl.UpdatedAt.Value(time.Now()))
if _, err := tx.Coupon.WithContext(ctx).Where(tbl.ID.Eq(coupon.ID)).UpdateSimple(updates...); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
updated, err := tx.Coupon.WithContext(ctx).Where(tbl.ID.Eq(coupon.ID)).First()
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
resultItem := s.composeCouponItem(updated)
result = &resultItem
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
func (s *coupon) Get(
ctx context.Context,
tenantID int64,
id int64,
) (*coupon_dto.CouponItem, error) {
if id == 0 {
return nil, errorx.ErrInvalidParameter.WithMsg("优惠券不存在")
}
query := models.CouponQuery.WithContext(ctx).Where(models.CouponQuery.ID.Eq(id))
if tenantID > 0 {
query = query.Where(models.CouponQuery.TenantID.Eq(tenantID))
}
coupon, err := query.First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errorx.ErrRecordNotFound.WithMsg("优惠券不存在")
}
return nil, errorx.ErrDatabaseError.WithCause(err)
}
item := s.composeCouponItem(coupon)
return &item, nil
}
func (s *coupon) List(
ctx context.Context,
tenantID int64,
filter *coupon_dto.CouponListFilter,
) (*requests.Pager, error) {
if tenantID == 0 {
return nil, errorx.ErrForbidden.WithMsg("租户未绑定")
}
if filter == nil {
filter = &coupon_dto.CouponListFilter{}
}
tbl, q := models.CouponQuery.QueryContext(ctx)
q = q.Where(tbl.TenantID.Eq(tenantID))
if filter.Type != nil && strings.TrimSpace(*filter.Type) != "" {
parsed, err := s.parseCouponType(*filter.Type)
if err != nil {
return nil, err
}
q = q.Where(tbl.Type.Eq(parsed))
}
if filter.Keyword != nil && strings.TrimSpace(*filter.Keyword) != "" {
keyword := strings.TrimSpace(*filter.Keyword)
q = q.Where(field.Or(
tbl.Title.Like("%"+keyword+"%"),
tbl.Description.Like("%"+keyword+"%"),
))
}
if filter.Status != nil && strings.TrimSpace(*filter.Status) != "" {
status := strings.TrimSpace(*filter.Status)
now := time.Now()
switch status {
case "active":
q = q.Where(field.Or(tbl.StartAt.Lte(now), tbl.StartAt.IsNull()))
q = q.Where(field.Or(tbl.EndAt.Gte(now), tbl.EndAt.IsNull()))
case "expired":
q = q.Where(tbl.EndAt.IsNotNull(), tbl.EndAt.Lt(now))
default:
return nil, errorx.ErrInvalidParameter.WithMsg("状态参数无效")
}
}
p := requests.Pagination{Page: filter.Page, Limit: filter.Limit}
total, err := q.Count()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
list, err := q.Order(tbl.CreatedAt.Desc()).Offset(int(p.Offset())).Limit(int(p.Limit)).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
items := make([]coupon_dto.CouponItem, 0, len(list))
for _, c := range list {
if c == nil {
continue
}
items = append(items, s.composeCouponItem(c))
}
return &requests.Pager{
Pagination: requests.Pagination{Page: p.Page, Limit: p.Limit},
Total: total,
Items: items,
}, nil
}
func (s *coupon) Grant(
ctx context.Context,
tenantID int64,
couponID int64,
userIDs []int64,
) (int, error) {
if tenantID == 0 {
return 0, errorx.ErrForbidden.WithMsg("租户未绑定")
}
if couponID == 0 {
return 0, errorx.ErrInvalidParameter.WithMsg("优惠券不存在")
}
if len(userIDs) == 0 {
return 0, errorx.ErrInvalidParameter.WithMsg("用户列表为空")
}
uniq := make(map[int64]struct{}, len(userIDs))
cleanIDs := make([]int64, 0, len(userIDs))
for _, id := range userIDs {
if id <= 0 {
continue
}
if _, ok := uniq[id]; ok {
continue
}
uniq[id] = struct{}{}
cleanIDs = append(cleanIDs, id)
}
if len(cleanIDs) == 0 {
return 0, errorx.ErrInvalidParameter.WithMsg("用户列表为空")
}
var granted int
err := models.Q.Transaction(func(tx *models.Query) error {
// 锁定优惠券行,防止并发超发。
tbl, q := tx.Coupon.QueryContext(ctx)
q = q.Where(tbl.ID.Eq(couponID), tbl.TenantID.Eq(tenantID))
coupon, err := q.Clauses(clause.Locking{Strength: "UPDATE"}).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound.WithMsg("优惠券不存在")
}
return errorx.ErrDatabaseError.WithCause(err)
}
now := time.Now()
if !coupon.StartAt.IsZero() && now.Before(coupon.StartAt) {
return errorx.ErrBusinessLogic.WithMsg("优惠券尚未生效")
}
if !coupon.EndAt.IsZero() && now.After(coupon.EndAt) {
return errorx.ErrBusinessLogic.WithMsg("优惠券已过期")
}
existingList, err := tx.UserCoupon.WithContext(ctx).
Where(tx.UserCoupon.CouponID.Eq(coupon.ID), tx.UserCoupon.UserID.In(cleanIDs...)).
Find()
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
existMap := make(map[int64]struct{}, len(existingList))
for _, item := range existingList {
existMap[item.UserID] = struct{}{}
}
newIDs := make([]int64, 0, len(cleanIDs))
for _, id := range cleanIDs {
if _, ok := existMap[id]; ok {
continue
}
newIDs = append(newIDs, id)
}
if len(newIDs) == 0 {
return nil
}
if coupon.TotalQuantity > 0 {
count, err := tx.UserCoupon.WithContext(ctx).
Where(tx.UserCoupon.CouponID.Eq(coupon.ID)).
Count()
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
if int32(count)+int32(len(newIDs)) > coupon.TotalQuantity {
return errorx.ErrBusinessLogic.WithMsg("优惠券库存不足")
}
}
for _, id := range newIDs {
uc := &models.UserCoupon{
UserID: id,
CouponID: coupon.ID,
Status: consts.UserCouponStatusUnused,
}
if err := tx.UserCoupon.WithContext(ctx).Create(uc); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
granted++
}
return nil
})
if err != nil {
return 0, err
}
return granted, 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 != consts.UserCouponStatusUnused {
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) {
if err := s.markUserCouponExpired(ctx, uc.ID); err != nil {
return 0, err
}
return 0, errorx.ErrBusinessLogic.WithMsg("优惠券已过期")
}
if amount < c.MinOrderAmount {
return 0, errorx.ErrBusinessLogic.WithMsg("未达到优惠券使用门槛")
}
var discount int64
switch c.Type {
case consts.CouponTypeFixAmount:
discount = c.Value
case consts.CouponTypeDiscount:
discount = (amount * c.Value) / 100
if c.MaxDiscount > 0 && discount > c.MaxDiscount {
discount = c.MaxDiscount
}
default:
return 0, errorx.ErrDataCorrupted.WithMsg("优惠券类型异常")
}
// 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 != consts.UserCouponStatusUnused {
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(consts.UserCouponStatusUnused)).
Updates(&models.UserCoupon{
Status: consts.UserCouponStatusUsed,
OrderID: orderID,
UsedAt: now,
})
if err != nil {
return errorx.ErrDatabaseError.WithCause(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
}
func (s *coupon) fetchCouponMap(ctx context.Context, tenantID int64, list []*models.UserCoupon) (map[int64]*models.Coupon, error) {
couponIDSet := make(map[int64]struct{}, len(list))
couponIDs := make([]int64, 0, len(list))
for _, v := range list {
if v == nil {
continue
}
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
}
return couponMap, nil
}
func (s *coupon) composeUserCouponItem(uc *models.UserCoupon, c *models.Coupon, status consts.UserCouponStatus) coupon_dto.UserCouponItem {
item := coupon_dto.UserCouponItem{
ID: uc.ID,
CouponID: uc.CouponID,
Status: string(status),
}
if c != nil {
item.Title = c.Title
item.Description = c.Description
item.Type = string(c.Type)
item.Value = c.Value
item.MinOrderAmount = c.MinOrderAmount
item.StartAt = s.formatTime(c.StartAt)
item.EndAt = s.formatTime(c.EndAt)
}
return item
}
func (s *coupon) composeCouponItem(c *models.Coupon) coupon_dto.CouponItem {
return coupon_dto.CouponItem{
ID: c.ID,
Title: c.Title,
Description: c.Description,
Type: string(c.Type),
Value: c.Value,
MinOrderAmount: c.MinOrderAmount,
MaxDiscount: c.MaxDiscount,
TotalQuantity: c.TotalQuantity,
UsedQuantity: c.UsedQuantity,
StartAt: s.formatTime(c.StartAt),
EndAt: s.formatTime(c.EndAt),
CreatedAt: s.formatTime(c.CreatedAt),
UpdatedAt: s.formatTime(c.UpdatedAt),
}
}
func (s *coupon) markUserCouponExpired(ctx context.Context, userCouponID int64) error {
if userCouponID == 0 {
return nil
}
_, err := models.UserCouponQuery.WithContext(ctx).
Where(models.UserCouponQuery.ID.Eq(userCouponID), models.UserCouponQuery.Status.Eq(consts.UserCouponStatusUnused)).
UpdateSimple(models.UserCouponQuery.Status.Value(consts.UserCouponStatusExpired))
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
return nil
}
func (s *coupon) isCouponExpired(c *models.Coupon, now time.Time) bool {
return !c.EndAt.IsZero() && now.After(c.EndAt)
}
func (s *coupon) isCouponActive(c *models.Coupon, now time.Time) bool {
if !c.StartAt.IsZero() && now.Before(c.StartAt) {
return false
}
if !c.EndAt.IsZero() && now.After(c.EndAt) {
return false
}
return true
}
func (s *coupon) parseTime(val string) (time.Time, error) {
value := strings.TrimSpace(val)
if value == "" {
return time.Time{}, nil
}
tm, err := time.Parse(time.RFC3339, value)
if err != nil {
return time.Time{}, errorx.ErrInvalidFormat.WithMsg("时间格式无效")
}
return tm, nil
}
func (s *coupon) parseCouponType(val string) (consts.CouponType, error) {
value := strings.TrimSpace(val)
if value == "" {
return "", errorx.ErrInvalidParameter.WithMsg("优惠券类型不能为空")
}
couponType, err := consts.ParseCouponType(value)
if err != nil {
return "", errorx.ErrInvalidParameter.WithMsg("优惠券类型无效")
}
return couponType, nil
}
func (s *coupon) validateCouponValue(typ consts.CouponType, value, maxDiscount int64) error {
switch typ {
case consts.CouponTypeFixAmount:
if value <= 0 {
return errorx.ErrInvalidParameter.WithMsg("优惠券面值无效")
}
case consts.CouponTypeDiscount:
if value <= 0 || value > 100 {
return errorx.ErrInvalidParameter.WithMsg("折扣比例无效")
}
if maxDiscount < 0 {
return errorx.ErrInvalidParameter.WithMsg("最高抵扣金额无效")
}
default:
return errorx.ErrInvalidParameter.WithMsg("优惠券类型无效")
}
return nil
}
func (s *coupon) formatTime(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format(time.RFC3339)
}