From f049cbce6c09b0a8ab059dc9bccd34ccce39edb9 Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Sat, 8 Nov 2025 01:27:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=9F=A5=E8=AF=A2=E6=BB=A1?= =?UTF-8?q?=E8=B6=B3=E5=8F=91=E9=80=81=E5=91=8A=E8=AD=A6=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E7=9A=84=E6=B4=BB=E8=B7=83=E5=91=8A=E8=AD=A6?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E7=9A=84=E4=BB=93=E5=BA=93=E5=B1=82=E6=96=B9?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infra/repository/alarm_repository.go | 151 ++++++++++++++++++ project_structure.txt | 4 + 2 files changed, 155 insertions(+) diff --git a/internal/infra/repository/alarm_repository.go b/internal/infra/repository/alarm_repository.go index c8ec5ee..35b6a99 100644 --- a/internal/infra/repository/alarm_repository.go +++ b/internal/infra/repository/alarm_repository.go @@ -2,6 +2,8 @@ package repository import ( "context" + "errors" + "fmt" "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" @@ -26,6 +28,15 @@ type AlarmRepository interface { // ListActiveAlarms 支持分页和过滤的活跃告警列表查询。 // 返回活跃告警列表、总记录数和错误。 ListActiveAlarms(ctx context.Context, opts ActiveAlarmListOptions, page, pageSize int) ([]models.ActiveAlarm, int64, error) + + // <-- 下列两个方法是为了性能做出的架构妥协, 业务逻辑入侵仓库层带来的收益远大于通过业务层进行数据筛选 --> + + // ListAlarmsForNotification 查询满足发送告警消息条件的活跃告警列表。 + // 返回活跃告警列表和错误。 + // intervalByLevel: key=SeverityLevel, value=interval_in_minutes + ListAlarmsForNotification(ctx context.Context, intervalByLevel map[models.SeverityLevel]int) ([]models.ActiveAlarm, error) + // 查询满足发送告警消息条件的记录总数 + CountAlarmsForNotification(ctx context.Context, intervalByLevel map[models.SeverityLevel]int) (int64, error) } // gormAlarmRepository 是 AlarmRepository 的 GORM 实现。 @@ -93,3 +104,143 @@ func (r *gormAlarmRepository) ListActiveAlarms(ctx context.Context, opts ActiveA return results, total, err } + +// CountAlarmsForNotification 查询满足发送告警消息条件的记录总数 +func (r *gormAlarmRepository) CountAlarmsForNotification(ctx context.Context, intervalByLevel map[models.SeverityLevel]int) (int64, error) { + repoCtx := logs.AddFuncName(ctx, r.ctx, "CountAlarmsForNotification") + var total int64 + + // 1. 构造基础查询对象 (包含 Context 和 Model) + baseTx := r.db.WithContext(repoCtx).Model(&models.ActiveAlarm{}) + + // 2. 传递给辅助函数应用所有 WHERE 逻辑 + query := r.buildNotificationBaseQuery(baseTx, intervalByLevel) + + // 3. 只执行 Count + err := query.Count(&total).Error + + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return 0, err + } + + return total, nil +} + +// ListAlarmsForNotification 查询满足发送告警消息条件的活跃告警列表 +func (r *gormAlarmRepository) ListAlarmsForNotification(ctx context.Context, intervalByLevel map[models.SeverityLevel]int) ([]models.ActiveAlarm, error) { + repoCtx := logs.AddFuncName(ctx, r.ctx, "ListAlarmsForNotification") + var results []models.ActiveAlarm + + // 1. 构造基础查询对象 (包含 Context 和 Model) + baseTx := r.db.WithContext(repoCtx).Model(&models.ActiveAlarm{}) + + // 2. 传递给辅助函数应用所有 WHERE 逻辑 + query := r.buildNotificationBaseQuery(baseTx, intervalByLevel) + + // 3. 执行 Find (不排序,高性能) + err := query.Find(&results).Error + + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + + return results, nil +} + +// buildNotificationBaseQuery 负责组合 Group A 和 Group B 的逻辑 +func (r *gormAlarmRepository) buildNotificationBaseQuery(tx *gorm.DB, intervalByLevel map[models.SeverityLevel]int) *gorm.DB { + + // 1. 获取所有配置的 Level 列表 + configuredLevels := make([]models.SeverityLevel, 0, len(intervalByLevel)) + for level := range intervalByLevel { + configuredLevels = append(configuredLevels, level) + } + + // 2. 构造 Group A (只发送一次) + // Group A 是一个独立的 GORM SubQuery,用于构建 OR 关系 + groupAQuery := r.buildGroupAClause(tx.Session(&gorm.Session{}), configuredLevels) + + // 3. 构造 Group B (间隔发送) + // Group B 也是一个独立的 GORM SubQuery + groupBQuery := r.buildGroupBClause(tx.Session(&gorm.Session{}), intervalByLevel, configuredLevels) + + // 4. 最终组合:Group A OR Group B + + // 核心逻辑:利用 GORM 的 Where(SubQuery) OR Where(SubQuery) 特性。 + // GORM 允许将 WHERE 或 OR 的参数写成 func(db *gorm.DB) *gorm.DB + // 这样可以确保子查询的括号被正确处理,实现 (A) OR (B) 结构。 + + // 注意:我们必须检查配置,因为如果 Group B 配置为空,我们不应该将其添加到 OR 关系中。 + if len(configuredLevels) == 0 { + // 只有 Group A 存在(即 Level NOT IN 的条件是 1=1) + return tx.Where(groupAQuery) + } + + // 存在 Group A 和 Group B,用 OR 连接 + return tx.Where(groupAQuery).Or(groupBQuery) +} + +// buildGroupAClause 构造 Group A 的 WHERE 语句和参数列表。 +// 针对 Level 缺失配置(或所有 Level)的告警,使用“只发送一次”逻辑:LastNotifiedAt IS NULL +// 参数 configuredLevels: 用于构建 Level NOT IN (?) 子句。 +func (r *gormAlarmRepository) buildGroupAClause(tx *gorm.DB, configuredLevels []models.SeverityLevel) *gorm.DB { + now := time.Now() + + // A.1. 构造 Level 范围检查子句 (Level NOT IN 或 1=1) + if len(configuredLevels) > 0 { + tx = tx.Where("level NOT IN (?)", configuredLevels) + } else { + // 如果配置列表为空,则所有 Level 都符合,使用 1=1 + tx = tx.Where("1 = 1") + } + + // A.2. 构造 Group A 核心逻辑 (LastNotifiedAt IS NULL 且满足忽略条件) + + // C_A_Ignored: 被忽略但忽略期结束 且 仅发送一次 + ignoredQuery := tx.Where("is_ignored = ? AND ignored_until <= ? AND last_notified_at IS NULL", true, now) + + // C_A_NotIgnored: 未被忽略 且 仅发送一次 + notIgnoredQuery := tx.Where("is_ignored = ? AND last_notified_at IS NULL", false) + + // A.3. 组合 Group A 核心逻辑: (C_A_Ignored OR C_A_NotIgnored) + return tx.Where(ignoredQuery).Or(notIgnoredQuery) +} + +// buildGroupBClause 构造 Group B 的 WHERE 语句和参数列表。 +// 针对 Level 存在配置的告警,使用“间隔发送”逻辑。 +func (r *gormAlarmRepository) buildGroupBClause(tx *gorm.DB, intervalByLevel map[models.SeverityLevel]int, configuredLevels []models.SeverityLevel) *gorm.DB { + now := time.Now() + + // B.1. 构造 Level IN 子句 + tx = tx.Where("level IN (?)", configuredLevels) + + // B.2. 构造 Level-Based 间隔检查 (OR 部分) + // 核心思想:利用 GORM 的 Or 链式调用构建 Level 间隔检查子句 + + // 初始化 Level 间隔检查查询 (ICC) + iccQuery := tx.Session(&gorm.Session{}) // 创建一个干净的子查询对象来构建 ICC + + // 动态添加 Level 间隔检查 OR 条件 + for level, minutes := range intervalByLevel { + // PostgreSQL 语法: last_notified_at + (5 * interval '1 minute') <= ? + sql := fmt.Sprintf("level = ? AND last_notified_at + (%d * interval '1 minute') <= ?", minutes) + + // 每次使用 Or 叠加新的 Level 检查 + iccQuery = iccQuery.Or(sql, level, now) + } + + // B.3. 组合 Group B 核心逻辑: (last_notified_at IS NULL OR [ICC]) + + // C_B_NotIgnored: 未被忽略 + notIgnoredQuery := tx.Where("is_ignored = ?", false).Where( + tx.Where("last_notified_at IS NULL").Or(iccQuery), // LastNotifiedAt IS NULL OR ICC + ) + + // C_B_Ignored: 被忽略但忽略期结束 + ignoredQuery := tx.Where("is_ignored = ? AND ignored_until <= ?", true, now).Where( + tx.Where("last_notified_at IS NULL").Or(iccQuery), // LastNotifiedAt IS NULL OR ICC + ) + + // B.4. 组合 Group B 核心逻辑: (C_B_NotIgnored OR C_B_Ignored) + return tx.Where(notIgnoredQuery).Or(ignoredQuery) +} diff --git a/project_structure.txt b/project_structure.txt index 479ca80..2b72491 100644 --- a/project_structure.txt +++ b/project_structure.txt @@ -81,6 +81,7 @@ internal/app/webhook/transport.go internal/core/application.go internal/core/component_initializers.go internal/core/data_initializer.go +internal/domain/alarm/alarm_service.go internal/domain/device/device_service.go internal/domain/device/general_device_service.go internal/domain/notify/notify.go @@ -96,6 +97,7 @@ internal/domain/plan/analysis_plan_task_manager.go internal/domain/plan/plan_execution_manager.go internal/domain/plan/plan_service.go internal/domain/plan/task.go +internal/domain/task/alarm_notification_task.go internal/domain/task/delay_task.go internal/domain/task/full_collection_task.go internal/domain/task/release_feed_weight_task.go @@ -106,6 +108,7 @@ internal/infra/database/storage.go internal/infra/logs/context.go internal/infra/logs/encoder.go internal/infra/logs/logs.go +internal/infra/models/alarm.go internal/infra/models/device.go internal/infra/models/device_template.go internal/infra/models/execution.go @@ -127,6 +130,7 @@ internal/infra/notify/log_notifier.go internal/infra/notify/notify.go internal/infra/notify/smtp.go internal/infra/notify/wechat.go +internal/infra/repository/alarm_repository.go internal/infra/repository/area_controller_repository.go internal/infra/repository/device_command_log_repository.go internal/infra/repository/device_repository.go