diff --git a/internal/core/application.go b/internal/core/application.go index d51f5e6..dd0ddbb 100644 --- a/internal/core/application.go +++ b/internal/core/application.go @@ -90,6 +90,7 @@ func NewApplication(configPath string) (*Application, error) { pigSickPigLogRepo := repository.NewGormPigSickLogRepository(storage.GetDB()) medicationLogRepo := repository.NewGormMedicationLogRepository(storage.GetDB()) rawMaterialRepo := repository.NewGormRawMaterialRepository(storage.GetDB()) + notificationRepo := repository.NewGormNotificationRepository(storage.GetDB()) // 初始化事务管理器 unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger) @@ -123,7 +124,7 @@ func NewApplication(configPath string) (*Application, error) { auditService := audit.NewService(userActionLogRepo, logger) // 初始化通知服务 - notifyService, err := initNotifyService(cfg.Notify, logger, userRepo) + notifyService, err := initNotifyService(cfg.Notify, logger, userRepo, notificationRepo) if err != nil { return nil, fmt.Errorf("初始化通知服务失败: %w", err) } @@ -220,6 +221,7 @@ func initNotifyService( cfg config.NotifyConfig, log *logs.Logger, userRepo repository.UserRepository, + notificationRepo repository.NotificationRepository, ) (domain_notify.Service, error) { var availableNotifiers []notify.Notifier @@ -278,13 +280,14 @@ func initNotifyService( log.Warnf("配置的首选渠道 '%s' 未启用或未指定,已自动降级使用 '%s' 作为首选渠道。", cfg.Primary, primaryNotifier.Type()) } - // 4. 使用创建的 Notifier 列表来组装领域服务 + // 4. 使用创建的 Notifier 列表和 notificationRepo 来组装领域服务 notifyService, err := domain_notify.NewFailoverService( log, userRepo, availableNotifiers, primaryNotifier.Type(), cfg.FailureThreshold, + notificationRepo, ) if err != nil { return nil, fmt.Errorf("创建故障转移通知服务失败: %w", err) diff --git a/internal/domain/notify/notify.go b/internal/domain/notify/notify.go index d1d1e95..df13c1d 100644 --- a/internal/domain/notify/notify.go +++ b/internal/domain/notify/notify.go @@ -33,6 +33,7 @@ type failoverService struct { primaryNotifier notify.Notifier failureThreshold int failureCounters *sync.Map // 使用 sync.Map 来安全地并发读写失败计数, key: userID (uint), value: counter (int) + notificationRepo repository.NotificationRepository } // NewFailoverService 创建一个新的故障转移通知服务 @@ -42,6 +43,7 @@ func NewFailoverService( notifiers []notify.Notifier, primaryNotifierType notify.NotifierType, failureThreshold int, + notificationRepo repository.NotificationRepository, ) (Service, error) { notifierMap := make(map[notify.NotifierType]notify.Notifier) for _, n := range notifiers { @@ -60,6 +62,7 @@ func NewFailoverService( primaryNotifier: primaryNotifier, failureThreshold: failureThreshold, failureCounters: &sync.Map{}, + notificationRepo: notificationRepo, }, nil } @@ -128,11 +131,15 @@ func (s *failoverService) sendAlarmToUser(userID uint, content notify.AlarmConte primaryType := s.primaryNotifier.Type() addr := getAddressForNotifier(primaryType, user.Contact) if addr == "" { + // 记录跳过通知 + s.recordNotificationAttempt(userID, primaryType, content, "", models.NotificationStatusSkipped, fmt.Errorf("用户未配置首选通知方式 '%s' 的地址", primaryType)) return fmt.Errorf("用户未配置首选通知方式 '%s' 的地址", primaryType) } err = s.primaryNotifier.Send(content, addr) if err == nil { + // 记录成功通知 + s.recordNotificationAttempt(userID, primaryType, content, addr, models.NotificationStatusSuccess, nil) if failureCount > 0 { s.log.Infow("首选渠道发送恢复正常", "userID", userID, "notifierType", primaryType) s.failureCounters.Store(userID, 0) @@ -140,6 +147,8 @@ func (s *failoverService) sendAlarmToUser(userID uint, content notify.AlarmConte return nil } + // 记录失败通知 + s.recordNotificationAttempt(userID, primaryType, content, addr, models.NotificationStatusFailed, err) newFailureCount := failureCount + 1 s.failureCounters.Store(userID, newFailureCount) s.log.Warnw("首选渠道发送失败", "userID", userID, "notifierType", primaryType, "error", err, "failureCount", newFailureCount) @@ -152,13 +161,19 @@ func (s *failoverService) sendAlarmToUser(userID uint, content notify.AlarmConte for _, notifier := range s.notifiers { addr := getAddressForNotifier(notifier.Type(), user.Contact) if addr == "" { + // 记录跳过通知 + s.recordNotificationAttempt(userID, notifier.Type(), content, "", models.NotificationStatusSkipped, fmt.Errorf("用户未配置通知方式 '%s' 的地址", notifier.Type())) continue } if err := notifier.Send(content, addr); err == nil { + // 记录成功通知 + s.recordNotificationAttempt(userID, notifier.Type(), content, addr, models.NotificationStatusSuccess, nil) s.log.Infow("广播通知成功", "userID", userID, "notifierType", notifier.Type()) s.failureCounters.Store(userID, 0) return nil } + // 记录失败通知 + s.recordNotificationAttempt(userID, notifier.Type(), content, addr, models.NotificationStatusFailed, err) lastErr = err s.log.Warnw("广播通知:渠道发送失败", "userID", userID, "notifierType", notifier.Type(), "error", err) } @@ -185,6 +200,13 @@ func (s *failoverService) SendTestMessage(userID uint, notifierType notify.Notif addr := getAddressForNotifier(notifierType, user.Contact) if addr == "" { s.log.Warnw("发送测试消息失败:缺少地址", "userID", userID, "notifierType", notifierType) + // 记录跳过通知 + s.recordNotificationAttempt(userID, notifierType, notify.AlarmContent{ + Title: "通知服务测试", + Message: fmt.Sprintf("这是一条来自【%s】渠道的测试消息。如果您收到此消息,说明您的配置正确。", notifierType), + Level: zap.InfoLevel, + Timestamp: time.Now(), + }, "", models.NotificationStatusFailed, fmt.Errorf("用户未配置通知方式 '%s' 的地址", notifierType)) return fmt.Errorf("用户未配置通知方式 '%s' 的地址", notifierType) } @@ -199,10 +221,14 @@ func (s *failoverService) SendTestMessage(userID uint, notifierType notify.Notif err = notifier.Send(testContent, addr) if err != nil { s.log.Errorw("发送测试消息失败", "userID", userID, "notifierType", notifierType, "error", err) + // 记录失败通知 + s.recordNotificationAttempt(userID, notifierType, testContent, addr, models.NotificationStatusFailed, err) return err } s.log.Infow("发送测试消息成功", "userID", userID, "notifierType", notifierType) + // 记录成功通知 + s.recordNotificationAttempt(userID, notifierType, testContent, addr, models.NotificationStatusSuccess, nil) return nil } @@ -221,3 +247,46 @@ func getAddressForNotifier(notifierType notify.NotifierType, contact models.Cont return "" } } + +// recordNotificationAttempt 记录一次通知发送尝试的结果 +// userID: 接收通知的用户ID +// notifierType: 使用的通知器类型 +// content: 通知内容 +// toAddress: 实际发送到的地址 +// status: 发送尝试的状态 (成功、失败、跳过) +// err: 如果发送失败,记录的错误信息 +func (s *failoverService) recordNotificationAttempt( + userID uint, + notifierType notify.NotifierType, + content notify.AlarmContent, + toAddress string, + status models.NotificationStatus, + err error, +) { + errorMessage := "" + if err != nil { + errorMessage = err.Error() + } + + notification := &models.Notification{ + NotifierType: notifierType, + UserID: userID, + Title: content.Title, + Message: content.Message, + Level: content.Level, + AlarmTimestamp: content.Timestamp, + ToAddress: toAddress, + Status: status, + ErrorMessage: errorMessage, + } + + if saveErr := s.notificationRepo.Create(notification); saveErr != nil { + s.log.Errorw("无法保存通知发送记录到数据库", + "userID", userID, + "notifierType", notifierType, + "status", status, + "originalError", errorMessage, + "saveError", saveErr, + ) + } +} diff --git a/internal/infra/models/notify.go b/internal/infra/models/notify.go index a26db9f..9c126ca 100644 --- a/internal/infra/models/notify.go +++ b/internal/infra/models/notify.go @@ -8,6 +8,16 @@ import ( "gorm.io/gorm" ) +// NotificationStatus 定义了通知发送尝试的状态枚举。 +type NotificationStatus string + +const ( + NotificationStatusTest NotificationStatus = "测试通知" // 测试用通知 + NotificationStatusSuccess NotificationStatus = "发送成功" // 通知已成功发送 + NotificationStatusFailed NotificationStatus = "发送失败" // 通知发送失败 + NotificationStatusSkipped NotificationStatus = "已跳过" // 通知因某些原因被跳过(例如:用户未配置联系方式) +) + // Notification 表示已发送或尝试发送的通知记录。 type Notification struct { gorm.Model @@ -26,8 +36,8 @@ type Notification struct { AlarmTimestamp time.Time `gorm:"primaryKey;not null" json:"alarm_timestamp"` // ToAddress 接收地址 (例如:邮箱地址, 企业微信ID, 日志标识符) ToAddress string `gorm:"type:varchar(255);not null" json:"to_address"` - // Status 通知发送尝试的状态 (例如:"success", "failed", "pending") - Status string `gorm:"type:varchar(20);not null;default:'pending'" json:"status"` + // Status 通知发送尝试的状态 (例如:"待发送", "发送成功", "发送失败", "已跳过") + Status NotificationStatus `gorm:"type:varchar(20);not null;default:'待发送'" json:"status"` // ErrorMessage 如果通知发送失败,此字段存储错误信息 ErrorMessage string `gorm:"type:text" json:"error_message"` }