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) }