发送通知时写入数据库

This commit is contained in:
2025-10-25 14:15:17 +08:00
parent d6f275b2d1
commit f33e14f60f
3 changed files with 86 additions and 4 deletions

View File

@@ -90,6 +90,7 @@ func NewApplication(configPath string) (*Application, error) {
pigSickPigLogRepo := repository.NewGormPigSickLogRepository(storage.GetDB()) pigSickPigLogRepo := repository.NewGormPigSickLogRepository(storage.GetDB())
medicationLogRepo := repository.NewGormMedicationLogRepository(storage.GetDB()) medicationLogRepo := repository.NewGormMedicationLogRepository(storage.GetDB())
rawMaterialRepo := repository.NewGormRawMaterialRepository(storage.GetDB()) rawMaterialRepo := repository.NewGormRawMaterialRepository(storage.GetDB())
notificationRepo := repository.NewGormNotificationRepository(storage.GetDB())
// 初始化事务管理器 // 初始化事务管理器
unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger) unitOfWork := repository.NewGormUnitOfWork(storage.GetDB(), logger)
@@ -123,7 +124,7 @@ func NewApplication(configPath string) (*Application, error) {
auditService := audit.NewService(userActionLogRepo, logger) auditService := audit.NewService(userActionLogRepo, logger)
// 初始化通知服务 // 初始化通知服务
notifyService, err := initNotifyService(cfg.Notify, logger, userRepo) notifyService, err := initNotifyService(cfg.Notify, logger, userRepo, notificationRepo)
if err != nil { if err != nil {
return nil, fmt.Errorf("初始化通知服务失败: %w", err) return nil, fmt.Errorf("初始化通知服务失败: %w", err)
} }
@@ -220,6 +221,7 @@ func initNotifyService(
cfg config.NotifyConfig, cfg config.NotifyConfig,
log *logs.Logger, log *logs.Logger,
userRepo repository.UserRepository, userRepo repository.UserRepository,
notificationRepo repository.NotificationRepository,
) (domain_notify.Service, error) { ) (domain_notify.Service, error) {
var availableNotifiers []notify.Notifier var availableNotifiers []notify.Notifier
@@ -278,13 +280,14 @@ func initNotifyService(
log.Warnf("配置的首选渠道 '%s' 未启用或未指定,已自动降级使用 '%s' 作为首选渠道。", cfg.Primary, primaryNotifier.Type()) log.Warnf("配置的首选渠道 '%s' 未启用或未指定,已自动降级使用 '%s' 作为首选渠道。", cfg.Primary, primaryNotifier.Type())
} }
// 4. 使用创建的 Notifier 列表来组装领域服务 // 4. 使用创建的 Notifier 列表和 notificationRepo 来组装领域服务
notifyService, err := domain_notify.NewFailoverService( notifyService, err := domain_notify.NewFailoverService(
log, log,
userRepo, userRepo,
availableNotifiers, availableNotifiers,
primaryNotifier.Type(), primaryNotifier.Type(),
cfg.FailureThreshold, cfg.FailureThreshold,
notificationRepo,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("创建故障转移通知服务失败: %w", err) return nil, fmt.Errorf("创建故障转移通知服务失败: %w", err)

View File

@@ -33,6 +33,7 @@ type failoverService struct {
primaryNotifier notify.Notifier primaryNotifier notify.Notifier
failureThreshold int failureThreshold int
failureCounters *sync.Map // 使用 sync.Map 来安全地并发读写失败计数, key: userID (uint), value: counter (int) failureCounters *sync.Map // 使用 sync.Map 来安全地并发读写失败计数, key: userID (uint), value: counter (int)
notificationRepo repository.NotificationRepository
} }
// NewFailoverService 创建一个新的故障转移通知服务 // NewFailoverService 创建一个新的故障转移通知服务
@@ -42,6 +43,7 @@ func NewFailoverService(
notifiers []notify.Notifier, notifiers []notify.Notifier,
primaryNotifierType notify.NotifierType, primaryNotifierType notify.NotifierType,
failureThreshold int, failureThreshold int,
notificationRepo repository.NotificationRepository,
) (Service, error) { ) (Service, error) {
notifierMap := make(map[notify.NotifierType]notify.Notifier) notifierMap := make(map[notify.NotifierType]notify.Notifier)
for _, n := range notifiers { for _, n := range notifiers {
@@ -60,6 +62,7 @@ func NewFailoverService(
primaryNotifier: primaryNotifier, primaryNotifier: primaryNotifier,
failureThreshold: failureThreshold, failureThreshold: failureThreshold,
failureCounters: &sync.Map{}, failureCounters: &sync.Map{},
notificationRepo: notificationRepo,
}, nil }, nil
} }
@@ -128,11 +131,15 @@ func (s *failoverService) sendAlarmToUser(userID uint, content notify.AlarmConte
primaryType := s.primaryNotifier.Type() primaryType := s.primaryNotifier.Type()
addr := getAddressForNotifier(primaryType, user.Contact) addr := getAddressForNotifier(primaryType, user.Contact)
if addr == "" { if addr == "" {
// 记录跳过通知
s.recordNotificationAttempt(userID, primaryType, content, "", models.NotificationStatusSkipped, fmt.Errorf("用户未配置首选通知方式 '%s' 的地址", primaryType))
return fmt.Errorf("用户未配置首选通知方式 '%s' 的地址", primaryType) return fmt.Errorf("用户未配置首选通知方式 '%s' 的地址", primaryType)
} }
err = s.primaryNotifier.Send(content, addr) err = s.primaryNotifier.Send(content, addr)
if err == nil { if err == nil {
// 记录成功通知
s.recordNotificationAttempt(userID, primaryType, content, addr, models.NotificationStatusSuccess, nil)
if failureCount > 0 { if failureCount > 0 {
s.log.Infow("首选渠道发送恢复正常", "userID", userID, "notifierType", primaryType) s.log.Infow("首选渠道发送恢复正常", "userID", userID, "notifierType", primaryType)
s.failureCounters.Store(userID, 0) s.failureCounters.Store(userID, 0)
@@ -140,6 +147,8 @@ func (s *failoverService) sendAlarmToUser(userID uint, content notify.AlarmConte
return nil return nil
} }
// 记录失败通知
s.recordNotificationAttempt(userID, primaryType, content, addr, models.NotificationStatusFailed, err)
newFailureCount := failureCount + 1 newFailureCount := failureCount + 1
s.failureCounters.Store(userID, newFailureCount) s.failureCounters.Store(userID, newFailureCount)
s.log.Warnw("首选渠道发送失败", "userID", userID, "notifierType", primaryType, "error", err, "failureCount", 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 { for _, notifier := range s.notifiers {
addr := getAddressForNotifier(notifier.Type(), user.Contact) addr := getAddressForNotifier(notifier.Type(), user.Contact)
if addr == "" { if addr == "" {
// 记录跳过通知
s.recordNotificationAttempt(userID, notifier.Type(), content, "", models.NotificationStatusSkipped, fmt.Errorf("用户未配置通知方式 '%s' 的地址", notifier.Type()))
continue continue
} }
if err := notifier.Send(content, addr); err == nil { 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.log.Infow("广播通知成功", "userID", userID, "notifierType", notifier.Type())
s.failureCounters.Store(userID, 0) s.failureCounters.Store(userID, 0)
return nil return nil
} }
// 记录失败通知
s.recordNotificationAttempt(userID, notifier.Type(), content, addr, models.NotificationStatusFailed, err)
lastErr = err lastErr = err
s.log.Warnw("广播通知:渠道发送失败", "userID", userID, "notifierType", notifier.Type(), "error", 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) addr := getAddressForNotifier(notifierType, user.Contact)
if addr == "" { if addr == "" {
s.log.Warnw("发送测试消息失败:缺少地址", "userID", userID, "notifierType", notifierType) 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) return fmt.Errorf("用户未配置通知方式 '%s' 的地址", notifierType)
} }
@@ -199,10 +221,14 @@ func (s *failoverService) SendTestMessage(userID uint, notifierType notify.Notif
err = notifier.Send(testContent, addr) err = notifier.Send(testContent, addr)
if err != nil { if err != nil {
s.log.Errorw("发送测试消息失败", "userID", userID, "notifierType", notifierType, "error", err) s.log.Errorw("发送测试消息失败", "userID", userID, "notifierType", notifierType, "error", err)
// 记录失败通知
s.recordNotificationAttempt(userID, notifierType, testContent, addr, models.NotificationStatusFailed, err)
return err return err
} }
s.log.Infow("发送测试消息成功", "userID", userID, "notifierType", notifierType) s.log.Infow("发送测试消息成功", "userID", userID, "notifierType", notifierType)
// 记录成功通知
s.recordNotificationAttempt(userID, notifierType, testContent, addr, models.NotificationStatusSuccess, nil)
return nil return nil
} }
@@ -221,3 +247,46 @@ func getAddressForNotifier(notifierType notify.NotifierType, contact models.Cont
return "" 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,
)
}
}

View File

@@ -8,6 +8,16 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// NotificationStatus 定义了通知发送尝试的状态枚举。
type NotificationStatus string
const (
NotificationStatusTest NotificationStatus = "测试通知" // 测试用通知
NotificationStatusSuccess NotificationStatus = "发送成功" // 通知已成功发送
NotificationStatusFailed NotificationStatus = "发送失败" // 通知发送失败
NotificationStatusSkipped NotificationStatus = "已跳过" // 通知因某些原因被跳过(例如:用户未配置联系方式)
)
// Notification 表示已发送或尝试发送的通知记录。 // Notification 表示已发送或尝试发送的通知记录。
type Notification struct { type Notification struct {
gorm.Model gorm.Model
@@ -26,8 +36,8 @@ type Notification struct {
AlarmTimestamp time.Time `gorm:"primaryKey;not null" json:"alarm_timestamp"` AlarmTimestamp time.Time `gorm:"primaryKey;not null" json:"alarm_timestamp"`
// ToAddress 接收地址 (例如:邮箱地址, 企业微信ID, 日志标识符) // ToAddress 接收地址 (例如:邮箱地址, 企业微信ID, 日志标识符)
ToAddress string `gorm:"type:varchar(255);not null" json:"to_address"` ToAddress string `gorm:"type:varchar(255);not null" json:"to_address"`
// Status 通知发送尝试的状态 (例如:"success", "failed", "pending") // Status 通知发送尝试的状态 (例如:"待发送", "发送成功", "发送失败", "已跳过")
Status string `gorm:"type:varchar(20);not null;default:'pending'" json:"status"` Status NotificationStatus `gorm:"type:varchar(20);not null;default:'待发送'" json:"status"`
// ErrorMessage 如果通知发送失败,此字段存储错误信息 // ErrorMessage 如果通知发送失败,此字段存储错误信息
ErrorMessage string `gorm:"type:text" json:"error_message"` ErrorMessage string `gorm:"type:text" json:"error_message"`
} }