| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- package service
- import (
- "context"
- "crypto/rand"
- "encoding/hex"
- "fmt"
- "strings"
- "time"
- dbent "github.com/Wei-Shaw/sub2api/ent"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
- )
- var (
- ErrPromoCodeNotFound = infraerrors.NotFound("PROMO_CODE_NOT_FOUND", "promo code not found")
- ErrPromoCodeExpired = infraerrors.BadRequest("PROMO_CODE_EXPIRED", "promo code has expired")
- ErrPromoCodeDisabled = infraerrors.BadRequest("PROMO_CODE_DISABLED", "promo code is disabled")
- ErrPromoCodeMaxUsed = infraerrors.BadRequest("PROMO_CODE_MAX_USED", "promo code has reached maximum uses")
- ErrPromoCodeAlreadyUsed = infraerrors.Conflict("PROMO_CODE_ALREADY_USED", "you have already used this promo code")
- ErrPromoCodeInvalid = infraerrors.BadRequest("PROMO_CODE_INVALID", "invalid promo code")
- )
- // PromoService 优惠码服务
- type PromoService struct {
- promoRepo PromoCodeRepository
- userRepo UserRepository
- billingCacheService *BillingCacheService
- entClient *dbent.Client
- authCacheInvalidator APIKeyAuthCacheInvalidator
- }
- // NewPromoService 创建优惠码服务实例
- func NewPromoService(
- promoRepo PromoCodeRepository,
- userRepo UserRepository,
- billingCacheService *BillingCacheService,
- entClient *dbent.Client,
- authCacheInvalidator APIKeyAuthCacheInvalidator,
- ) *PromoService {
- return &PromoService{
- promoRepo: promoRepo,
- userRepo: userRepo,
- billingCacheService: billingCacheService,
- entClient: entClient,
- authCacheInvalidator: authCacheInvalidator,
- }
- }
- // ValidatePromoCode 验证优惠码(注册前调用)
- // 返回 nil, nil 表示空码(不报错)
- func (s *PromoService) ValidatePromoCode(ctx context.Context, code string) (*PromoCode, error) {
- code = strings.TrimSpace(code)
- if code == "" {
- return nil, nil // 空码不报错,直接返回
- }
- promoCode, err := s.promoRepo.GetByCode(ctx, code)
- if err != nil {
- // 保留原始错误类型,不要统一映射为 NotFound
- return nil, err
- }
- if err := s.validatePromoCodeStatus(promoCode); err != nil {
- return nil, err
- }
- return promoCode, nil
- }
- // validatePromoCodeStatus 验证优惠码状态
- func (s *PromoService) validatePromoCodeStatus(promoCode *PromoCode) error {
- if !promoCode.CanUse() {
- if promoCode.IsExpired() {
- return ErrPromoCodeExpired
- }
- if promoCode.Status == PromoCodeStatusDisabled {
- return ErrPromoCodeDisabled
- }
- if promoCode.MaxUses > 0 && promoCode.UsedCount >= promoCode.MaxUses {
- return ErrPromoCodeMaxUsed
- }
- return ErrPromoCodeInvalid
- }
- return nil
- }
- // ApplyPromoCode 应用优惠码(注册成功后调用)
- // 使用事务和行锁确保并发安全
- func (s *PromoService) ApplyPromoCode(ctx context.Context, userID int64, code string) error {
- code = strings.TrimSpace(code)
- if code == "" {
- return nil
- }
- // 开启事务
- tx, err := s.entClient.Tx(ctx)
- if err != nil {
- return fmt.Errorf("begin transaction: %w", err)
- }
- defer func() { _ = tx.Rollback() }()
- txCtx := dbent.NewTxContext(ctx, tx)
- // 在事务中获取并锁定优惠码记录(FOR UPDATE)
- promoCode, err := s.promoRepo.GetByCodeForUpdate(txCtx, code)
- if err != nil {
- return err
- }
- // 在事务中验证优惠码状态
- if err := s.validatePromoCodeStatus(promoCode); err != nil {
- return err
- }
- // 在事务中检查用户是否已使用过此优惠码
- existing, err := s.promoRepo.GetUsageByPromoCodeAndUser(txCtx, promoCode.ID, userID)
- if err != nil {
- return fmt.Errorf("check existing usage: %w", err)
- }
- if existing != nil {
- return ErrPromoCodeAlreadyUsed
- }
- // 增加用户余额
- if err := s.userRepo.UpdateBalance(txCtx, userID, promoCode.BonusAmount); err != nil {
- return fmt.Errorf("update user balance: %w", err)
- }
- // 创建使用记录
- usage := &PromoCodeUsage{
- PromoCodeID: promoCode.ID,
- UserID: userID,
- BonusAmount: promoCode.BonusAmount,
- UsedAt: time.Now(),
- }
- if err := s.promoRepo.CreateUsage(txCtx, usage); err != nil {
- return fmt.Errorf("create usage record: %w", err)
- }
- // 增加使用次数
- if err := s.promoRepo.IncrementUsedCount(txCtx, promoCode.ID); err != nil {
- return fmt.Errorf("increment used count: %w", err)
- }
- if err := tx.Commit(); err != nil {
- return fmt.Errorf("commit transaction: %w", err)
- }
- s.invalidatePromoCaches(ctx, userID, promoCode.BonusAmount)
- // 失效余额缓存
- if s.billingCacheService != nil {
- go func() {
- cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- _ = s.billingCacheService.InvalidateUserBalance(cacheCtx, userID)
- }()
- }
- return nil
- }
- func (s *PromoService) invalidatePromoCaches(ctx context.Context, userID int64, bonusAmount float64) {
- if bonusAmount == 0 || s.authCacheInvalidator == nil {
- return
- }
- s.authCacheInvalidator.InvalidateAuthCacheByUserID(ctx, userID)
- }
- // GenerateRandomCode 生成随机优惠码
- func (s *PromoService) GenerateRandomCode() (string, error) {
- bytes := make([]byte, 8)
- if _, err := rand.Read(bytes); err != nil {
- return "", fmt.Errorf("generate random bytes: %w", err)
- }
- return strings.ToUpper(hex.EncodeToString(bytes)), nil
- }
- // Create 创建优惠码
- func (s *PromoService) Create(ctx context.Context, input *CreatePromoCodeInput) (*PromoCode, error) {
- code := strings.TrimSpace(input.Code)
- if code == "" {
- // 自动生成
- var err error
- code, err = s.GenerateRandomCode()
- if err != nil {
- return nil, err
- }
- }
- promoCode := &PromoCode{
- Code: strings.ToUpper(code),
- BonusAmount: input.BonusAmount,
- MaxUses: input.MaxUses,
- UsedCount: 0,
- Status: PromoCodeStatusActive,
- ExpiresAt: input.ExpiresAt,
- Notes: input.Notes,
- }
- if err := s.promoRepo.Create(ctx, promoCode); err != nil {
- return nil, fmt.Errorf("create promo code: %w", err)
- }
- return promoCode, nil
- }
- // GetByID 根据ID获取优惠码
- func (s *PromoService) GetByID(ctx context.Context, id int64) (*PromoCode, error) {
- code, err := s.promoRepo.GetByID(ctx, id)
- if err != nil {
- return nil, err
- }
- return code, nil
- }
- // Update 更新优惠码
- func (s *PromoService) Update(ctx context.Context, id int64, input *UpdatePromoCodeInput) (*PromoCode, error) {
- promoCode, err := s.promoRepo.GetByID(ctx, id)
- if err != nil {
- return nil, err
- }
- if input.Code != nil {
- promoCode.Code = strings.ToUpper(strings.TrimSpace(*input.Code))
- }
- if input.BonusAmount != nil {
- promoCode.BonusAmount = *input.BonusAmount
- }
- if input.MaxUses != nil {
- promoCode.MaxUses = *input.MaxUses
- }
- if input.Status != nil {
- promoCode.Status = *input.Status
- }
- if input.ExpiresAt != nil {
- if input.ExpiresAt.IsZero() {
- input.ExpiresAt = nil
- }
- promoCode.ExpiresAt = input.ExpiresAt
- }
- if input.Notes != nil {
- promoCode.Notes = *input.Notes
- }
- if err := s.promoRepo.Update(ctx, promoCode); err != nil {
- return nil, fmt.Errorf("update promo code: %w", err)
- }
- return promoCode, nil
- }
- // Delete 删除优惠码
- func (s *PromoService) Delete(ctx context.Context, id int64) error {
- if err := s.promoRepo.Delete(ctx, id); err != nil {
- return fmt.Errorf("delete promo code: %w", err)
- }
- return nil
- }
- // List 获取优惠码列表
- func (s *PromoService) List(ctx context.Context, params pagination.PaginationParams, status, search string) ([]PromoCode, *pagination.PaginationResult, error) {
- return s.promoRepo.ListWithFilters(ctx, params, status, search)
- }
- // ListUsages 获取使用记录
- func (s *PromoService) ListUsages(ctx context.Context, promoCodeID int64, params pagination.PaginationParams) ([]PromoCodeUsage, *pagination.PaginationResult, error) {
- return s.promoRepo.ListUsagesByPromoCode(ctx, promoCodeID, params)
- }
|