package services import ( "context" "time" "quyun/v2/app/errorx" coupon_dto "quyun/v2/app/http/v1/dto" "quyun/v2/database/models" "quyun/v2/pkg/consts" "github.com/spf13/cast" ) // @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: cast.ToString(v.ID), CouponID: cast.ToString(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 }