package usecase import ( "backend/pkg/notification/config" "backend/pkg/notification/domain" "backend/pkg/notification/domain/entity" "backend/pkg/notification/domain/repository" "backend/pkg/notification/domain/usecase" "context" "fmt" "math" "sort" "time" "backend/pkg/library/errs" "backend/pkg/library/errs/code" "github.com/zeromicro/go-zero/core/logx" ) // DeliveryUseCaseParam 傳送參數配置 type DeliveryUseCaseParam struct { SMSProviders []usecase.SMSProvider EmailProviders []usecase.EmailProvider DeliveryConfig config.DeliveryConfig HistoryRepo repository.HistoryRepository // 可選的歷史記錄 repository } // DeliveryUseCase 通知發送服務 type DeliveryUseCase struct { param DeliveryUseCaseParam } func MustDeliveryUseCase(param DeliveryUseCaseParam) usecase.DeliveryUseCase { // 設置默認配置 if param.DeliveryConfig.MaxRetries == 0 { param.DeliveryConfig.MaxRetries = 3 } if param.DeliveryConfig.InitialDelay == 0 { param.DeliveryConfig.InitialDelay = 100 * time.Millisecond } if param.DeliveryConfig.BackoffFactor == 0 { param.DeliveryConfig.BackoffFactor = 2.0 } if param.DeliveryConfig.MaxDelay == 0 { param.DeliveryConfig.MaxDelay = 30 * time.Second } if param.DeliveryConfig.Timeout == 0 { param.DeliveryConfig.Timeout = 30 * time.Second } return &DeliveryUseCase{ param: param, } } func (use *DeliveryUseCase) SendMessage(ctx context.Context, req usecase.SMSMessageRequest) error { // 創建歷史記錄 history := &entity.DeliveryHistory{ ID: generateID(), Type: "sms", Recipient: req.PhoneNumber, Subject: "", Content: req.MessageContent, Status: entity.DeliveryStatusPending, AttemptCount: 0, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil { if err := use.param.HistoryRepo.CreateHistory(ctx, history); err != nil { logx.WithContext(ctx).Errorf("Failed to create SMS history: %v", err) } } // 執行發送邏輯 return use.sendSMSWithRetry(ctx, req, history) } func (use *DeliveryUseCase) SendEmail(ctx context.Context, req usecase.MailReq) error { // 創建歷史記錄 history := &entity.DeliveryHistory{ ID: generateID(), Type: "email", Recipient: fmt.Sprintf("%v", req.To), Subject: req.Subject, Content: req.Body, Status: entity.DeliveryStatusPending, AttemptCount: 0, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil { if err := use.param.HistoryRepo.CreateHistory(ctx, history); err != nil { logx.WithContext(ctx).Errorf("Failed to create email history: %v", err) } } // 執行發送邏輯 return use.sendEmailWithRetry(ctx, req, history) } // sendSMSWithRetry 發送 SMS 並實現重試機制 func (use *DeliveryUseCase) sendSMSWithRetry(ctx context.Context, req usecase.SMSMessageRequest, history *entity.DeliveryHistory) error { // 根據 Sort 欄位對 SMSProviders 進行排序 providers := make([]usecase.SMSProvider, len(use.param.SMSProviders)) copy(providers, use.param.SMSProviders) sort.Slice(providers, func(i, j int) bool { return providers[i].Sort < providers[j].Sort }) var lastErr error totalAttempts := 0 // 嘗試所有 providers for providerIndex, provider := range providers { // 為每個 provider 嘗試發送 for attempt := 0; attempt < use.param.DeliveryConfig.MaxRetries; attempt++ { totalAttempts++ // 更新歷史記錄狀態 history.Status = entity.DeliveryStatusSending history.Provider = fmt.Sprintf("sms_provider_%d", providerIndex) history.AttemptCount = totalAttempts history.UpdatedAt = time.Now() use.updateHistory(ctx, history) // 記錄發送嘗試 attemptStart := time.Now() // 創建帶超時的 context sendCtx, cancel := context.WithTimeout(ctx, use.param.DeliveryConfig.Timeout) err := provider.Repo.SendSMS(sendCtx, repository.SMSMessageRequest{ PhoneNumber: req.PhoneNumber, RecipientName: req.RecipientName, MessageContent: req.MessageContent, }) cancel() // 記錄嘗試結果 attemptDuration := time.Since(attemptStart) attemptRecord := entity.DeliveryAttempt{ Provider: history.Provider, AttemptAt: attemptStart, Success: err == nil, ErrorMessage: "", Duration: attemptDuration.Milliseconds(), } if err != nil { attemptRecord.ErrorMessage = err.Error() lastErr = err logx.WithContext(ctx).Errorf("SMS send attempt %d failed for provider %d: %v", attempt+1, providerIndex, err) // 如果不是最後一次嘗試,等待後重試 if attempt < use.param.DeliveryConfig.MaxRetries-1 { delay := use.calculateDelay(attempt) history.Status = entity.DeliveryStatusRetrying use.updateHistory(ctx, history) use.addAttemptRecord(ctx, history.ID, attemptRecord) select { case <-ctx.Done(): return ctx.Err() case <-time.After(delay): continue } } } else { // 發送成功 history.Status = entity.DeliveryStatusSuccess history.UpdatedAt = time.Now() now := time.Now() history.CompletedAt = &now use.updateHistory(ctx, history) use.addAttemptRecord(ctx, history.ID, attemptRecord) logx.WithContext(ctx).Infof("SMS sent successfully after %d attempts", totalAttempts) return nil } use.addAttemptRecord(ctx, history.ID, attemptRecord) } } // 所有 providers 都失敗了 history.Status = entity.DeliveryStatusFailed history.ErrorMessage = fmt.Sprintf("All providers failed. Last error: %v", lastErr) history.UpdatedAt = time.Now() now := time.Now() history.CompletedAt = &now use.updateHistory(ctx, history) return errs.ThirdPartyError( code.CloudEPNotification, domain.FailedToSendSMSErrorCode, fmt.Sprintf("Failed to send SMS after %d attempts across %d providers", totalAttempts, len(providers))) } // sendEmailWithRetry 發送 Email 並實現重試機制 func (use *DeliveryUseCase) sendEmailWithRetry(ctx context.Context, req usecase.MailReq, history *entity.DeliveryHistory) error { // 根據 Sort 欄位對 EmailProviders 進行排序 providers := make([]usecase.EmailProvider, len(use.param.EmailProviders)) copy(providers, use.param.EmailProviders) sort.Slice(providers, func(i, j int) bool { return providers[i].Sort < providers[j].Sort }) var lastErr error totalAttempts := 0 // 嘗試所有 providers for providerIndex, provider := range providers { // 為每個 provider 嘗試發送 for attempt := 0; attempt < use.param.DeliveryConfig.MaxRetries; attempt++ { totalAttempts++ // 更新歷史記錄狀態 history.Status = entity.DeliveryStatusSending history.Provider = fmt.Sprintf("email_provider_%d", providerIndex) history.AttemptCount = totalAttempts history.UpdatedAt = time.Now() use.updateHistory(ctx, history) // 記錄發送嘗試 attemptStart := time.Now() // 創建帶超時的 context sendCtx, cancel := context.WithTimeout(ctx, use.param.DeliveryConfig.Timeout) err := provider.Repo.SendMail(sendCtx, repository.MailReq{ From: req.From, To: req.To, Subject: req.Subject, Body: req.Body, }) cancel() // 記錄嘗試結果 attemptDuration := time.Since(attemptStart) attemptRecord := entity.DeliveryAttempt{ Provider: history.Provider, AttemptAt: attemptStart, Success: err == nil, ErrorMessage: "", Duration: attemptDuration.Milliseconds(), } if err != nil { attemptRecord.ErrorMessage = err.Error() lastErr = err logx.WithContext(ctx).Errorf("Email send attempt %d failed for provider %d: %v", attempt+1, providerIndex, err) // 如果不是最後一次嘗試,等待後重試 if attempt < use.param.DeliveryConfig.MaxRetries-1 { delay := use.calculateDelay(attempt) history.Status = entity.DeliveryStatusRetrying use.updateHistory(ctx, history) use.addAttemptRecord(ctx, history.ID, attemptRecord) select { case <-ctx.Done(): return ctx.Err() case <-time.After(delay): continue } } } else { // 發送成功 history.Status = entity.DeliveryStatusSuccess history.UpdatedAt = time.Now() now := time.Now() history.CompletedAt = &now use.updateHistory(ctx, history) use.addAttemptRecord(ctx, history.ID, attemptRecord) logx.WithContext(ctx).Infof("Email sent successfully after %d attempts", totalAttempts) return nil } use.addAttemptRecord(ctx, history.ID, attemptRecord) } } // 所有 providers 都失敗了 history.Status = entity.DeliveryStatusFailed history.ErrorMessage = fmt.Sprintf("All providers failed. Last error: %v", lastErr) history.UpdatedAt = time.Now() now := time.Now() history.CompletedAt = &now use.updateHistory(ctx, history) return errs.ThirdPartyError( code.CloudEPNotification, domain.FailedToSendEmailErrorCode, fmt.Sprintf("Failed to send email after %d attempts across %d providers", totalAttempts, len(providers))) } // calculateDelay 計算指數退避延遲 func (use *DeliveryUseCase) calculateDelay(attempt int) time.Duration { delay := float64(use.param.DeliveryConfig.InitialDelay) * math.Pow(use.param.DeliveryConfig.BackoffFactor, float64(attempt)) if delay > float64(use.param.DeliveryConfig.MaxDelay) { delay = float64(use.param.DeliveryConfig.MaxDelay) } return time.Duration(delay) } // updateHistory 更新歷史記錄 func (use *DeliveryUseCase) updateHistory(ctx context.Context, history *entity.DeliveryHistory) { if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil { if err := use.param.HistoryRepo.UpdateHistory(ctx, history); err != nil { logx.WithContext(ctx).Errorf("Failed to update delivery history: %v", err) } } } // addAttemptRecord 添加發送嘗試記錄 func (use *DeliveryUseCase) addAttemptRecord(ctx context.Context, historyID string, attempt entity.DeliveryAttempt) { if use.param.DeliveryConfig.EnableHistory && use.param.HistoryRepo != nil { if err := use.param.HistoryRepo.AddAttempt(ctx, historyID, attempt); err != nil { logx.WithContext(ctx).Errorf("Failed to add attempt record: %v", err) } } } // generateID 生成唯一 ID (簡單實現,實際應該使用更好的 ID 生成器) func generateID() string { return fmt.Sprintf("delivery_%d", time.Now().UnixNano()) }