Files
pig-farm-controller/internal/infra/repository/alarm_repository.go

431 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repository
import (
"context"
"errors"
"fmt"
"time"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/logs"
"git.huangwc.com/pig/pig-farm-controller/internal/infra/models"
"gorm.io/gorm"
)
// ActiveAlarmListOptions 定义了查询活跃告警列表时的可选参数
type ActiveAlarmListOptions struct {
SourceType *models.AlarmSourceType // 按告警来源类型过滤
SourceID *uint // 按告警来源ID过滤
Level *models.SeverityLevel // 按告警严重性等级过滤
IsIgnored *bool // 按是否被忽略过滤
TriggerTime *time.Time // 告警触发时间范围 - 开始时间
EndTime *time.Time // 告警触发时间范围 - 结束时间
OrderBy string // 排序字段,例如 "trigger_time DESC"
}
// HistoricalAlarmListOptions 定义了查询历史告警列表时的可选参数
type HistoricalAlarmListOptions struct {
SourceType *models.AlarmSourceType // 按告警来源类型过滤
SourceID *uint // 按告警来源ID过滤
Level *models.SeverityLevel // 按告警严重性等级过滤
TriggerTimeStart *time.Time // 告警触发时间范围 - 开始时间
TriggerTimeEnd *time.Time // 告警触发时间范围 - 结束时间
ResolveTimeStart *time.Time // 告警解决时间范围 - 开始时间 (对应 models.HistoricalAlarm.ResolveTime)
ResolveTimeEnd *time.Time // 告警解决时间范围 - 结束时间 (对应 models.HistoricalAlarm.ResolveTime)
OrderBy string // 排序字段,例如 "trigger_time DESC"
}
// AlarmRepository 定义了对告警模型的数据库操作接口
type AlarmRepository interface {
// CreateActiveAlarm 创建一条新的活跃告警记录
CreateActiveAlarm(ctx context.Context, alarm *models.ActiveAlarm) error
// IsAlarmActiveInUse 检查具有相同来源和告警代码的告警当前是否处于活跃表中
IsAlarmActiveInUse(ctx context.Context, sourceType models.AlarmSourceType, sourceID uint, alarmCode models.AlarmCode) (bool, error)
// GetActiveAlarmByUniqueFieldsTx 在指定事务中根据唯一业务键获取一个活跃告警
GetActiveAlarmByUniqueFieldsTx(ctx context.Context, tx *gorm.DB, sourceType models.AlarmSourceType, sourceID uint, alarmCode models.AlarmCode) (*models.ActiveAlarm, error)
// CreateHistoricalAlarmTx 在指定事务中创建一条历史告警记录
CreateHistoricalAlarmTx(ctx context.Context, tx *gorm.DB, alarm *models.HistoricalAlarm) error
// DeleteActiveAlarmTx 在指定事务中根据主键 ID 删除一个活跃告警
DeleteActiveAlarmTx(ctx context.Context, tx *gorm.DB, id uint) error
// UpdateIgnoreStatus 更新指定告警的忽略状态
UpdateIgnoreStatus(ctx context.Context, id uint, isIgnored bool, ignoredUntil *time.Time) error
// ListActiveAlarms 支持分页和过滤的活跃告警列表查询。
// 返回活跃告警列表、总记录数和错误。
ListActiveAlarms(ctx context.Context, opts ActiveAlarmListOptions, page, pageSize int) ([]models.ActiveAlarm, int64, error)
// ListHistoricalAlarms 支持分页和过滤的历史告警列表查询。
// 返回历史告警列表、总记录数和错误。
ListHistoricalAlarms(ctx context.Context, opts HistoricalAlarmListOptions, page, pageSize int) ([]models.HistoricalAlarm, int64, error)
// UpdateAlarmNotificationStatus 显式更新告警的通知相关状态字段。
// lastNotifiedAt: 传入具体的发送时间。
// isIgnored: 告警新的忽略状态。
// ignoredUntil: 告警新的忽略截止时间 (nil 表示没有忽略截止时间/已取消忽略)。
UpdateAlarmNotificationStatus(ctx context.Context, alarmID uint, lastNotifiedAt time.Time, isIgnored bool, ignoredUntil *time.Time) error
// <-- 下列两个方法是为了性能做出的架构妥协, 业务逻辑入侵仓库层带来的收益远大于通过业务层进行数据筛选 -->
// ListAlarmsForNotification 查询满足发送告警消息条件的活跃告警列表。
// 返回活跃告警列表和错误。
// intervalByLevel: key=SeverityLevel, value=interval_in_minutes
ListAlarmsForNotification(ctx context.Context, intervalByLevel map[models.SeverityLevel]uint) ([]models.ActiveAlarm, error)
// 查询满足发送告警消息条件的记录总数
CountAlarmsForNotification(ctx context.Context, intervalByLevel map[models.SeverityLevel]uint) (int64, error)
}
// gormAlarmRepository 是 AlarmRepository 的 GORM 实现。
type gormAlarmRepository struct {
ctx context.Context
db *gorm.DB
}
// NewGormAlarmRepository 创建一个新的 AlarmRepository GORM 实现实例。
func NewGormAlarmRepository(ctx context.Context, db *gorm.DB) AlarmRepository {
return &gormAlarmRepository{
ctx: ctx,
db: db,
}
}
// CreateActiveAlarm 创建一条新的活跃告警记录
func (r *gormAlarmRepository) CreateActiveAlarm(ctx context.Context, alarm *models.ActiveAlarm) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateActiveAlarm")
return r.db.WithContext(repoCtx).Create(alarm).Error
}
// IsAlarmActiveInUse 检查具有相同来源和告警代码的告警当前是否处于活跃表中
func (r *gormAlarmRepository) IsAlarmActiveInUse(ctx context.Context, sourceType models.AlarmSourceType, sourceID uint, alarmCode models.AlarmCode) (bool, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "IsAlarmActiveInUse")
var count int64
err := r.db.WithContext(repoCtx).Model(&models.ActiveAlarm{}).
Where("source_type = ? AND source_id = ? AND alarm_code = ?", sourceType, sourceID, alarmCode).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
// GetActiveAlarmByUniqueFieldsTx 在指定事务中根据唯一业务键获取一个活跃告警
func (r *gormAlarmRepository) GetActiveAlarmByUniqueFieldsTx(ctx context.Context, tx *gorm.DB, sourceType models.AlarmSourceType, sourceID uint, alarmCode models.AlarmCode) (*models.ActiveAlarm, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "GetActiveAlarmByUniqueFieldsTx")
var alarm models.ActiveAlarm
err := tx.WithContext(repoCtx).
Where("source_type = ? AND source_id = ? AND alarm_code = ?", sourceType, sourceID, alarmCode).
First(&alarm).Error
return &alarm, err
}
// CreateHistoricalAlarmTx 在指定事务中创建一条历史告警记录
func (r *gormAlarmRepository) CreateHistoricalAlarmTx(ctx context.Context, tx *gorm.DB, alarm *models.HistoricalAlarm) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "CreateHistoricalAlarmTx")
return tx.WithContext(repoCtx).Create(alarm).Error
}
// DeleteActiveAlarmTx 在指定事务中根据主键 ID 删除一个活跃告警
func (r *gormAlarmRepository) DeleteActiveAlarmTx(ctx context.Context, tx *gorm.DB, id uint) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "DeleteActiveAlarmTx")
// 使用 Unscoped() 确保执行物理删除,而不是软删除
return tx.WithContext(repoCtx).Unscoped().Delete(&models.ActiveAlarm{}, id).Error
}
// UpdateIgnoreStatus 更新指定告警的忽略状态
func (r *gormAlarmRepository) UpdateIgnoreStatus(ctx context.Context, id uint, isIgnored bool, ignoredUntil *time.Time) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateIgnoreStatus")
updates := map[string]interface{}{
"is_ignored": isIgnored,
"ignored_until": ignoredUntil,
}
result := r.db.WithContext(repoCtx).
Model(&models.ActiveAlarm{}).
Where("id = ?", id).
Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// ListActiveAlarms 实现了分页和过滤查询活跃告警记录的功能
func (r *gormAlarmRepository) ListActiveAlarms(ctx context.Context, opts ActiveAlarmListOptions, page, pageSize int) ([]models.ActiveAlarm, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListActiveAlarms")
// --- 校验分页参数 ---
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
var results []models.ActiveAlarm
var total int64
query := r.db.WithContext(repoCtx).Model(&models.ActiveAlarm{})
// --- 应用过滤条件 ---
if opts.SourceType != nil {
query = query.Where("source_type = ?", *opts.SourceType)
}
if opts.SourceID != nil {
query = query.Where("source_id = ?", *opts.SourceID)
}
if opts.Level != nil {
query = query.Where("level = ?", *opts.Level)
}
if opts.IsIgnored != nil {
query = query.Where("is_ignored = ?", *opts.IsIgnored)
}
if opts.TriggerTime != nil {
query = query.Where("trigger_time >= ?", *opts.TriggerTime)
}
if opts.EndTime != nil {
query = query.Where("trigger_time <= ?", *opts.EndTime)
}
// --- 计算总数 ---
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// --- 应用排序条件 ---
orderBy := "trigger_time DESC" // 默认按触发时间倒序
if opts.OrderBy != "" {
orderBy = opts.OrderBy
}
query = query.Order(orderBy)
// --- 分页 ---
offset := (page - 1) * pageSize
err := query.Limit(pageSize).Offset(offset).Find(&results).Error
return results, total, err
}
// ListHistoricalAlarms 实现了分页和过滤查询历史告警记录的功能
func (r *gormAlarmRepository) ListHistoricalAlarms(ctx context.Context, opts HistoricalAlarmListOptions, page, pageSize int) ([]models.HistoricalAlarm, int64, error) {
repoCtx := logs.AddFuncName(ctx, r.ctx, "ListHistoricalAlarms")
// --- 校验分页参数 ---
if page <= 0 || pageSize <= 0 {
return nil, 0, ErrInvalidPagination
}
var results []models.HistoricalAlarm
var total int64
query := r.db.WithContext(repoCtx).Model(&models.HistoricalAlarm{})
// --- 应用过滤条件 ---
if opts.SourceType != nil {
query = query.Where("source_type = ?", *opts.SourceType)
}
if opts.SourceID != nil {
query = query.Where("source_id = ?", *opts.SourceID)
}
if opts.Level != nil {
query = query.Where("level = ?", *opts.Level)
}
if opts.TriggerTimeStart != nil {
query = query.Where("trigger_time >= ?", *opts.TriggerTimeStart)
}
if opts.TriggerTimeEnd != nil {
query = query.Where("trigger_time <= ?", *opts.TriggerTimeEnd)
}
if opts.ResolveTimeStart != nil { // 修改字段名
query = query.Where("resolve_time >= ?", *opts.ResolveTimeStart) // 修改查询字段名
}
if opts.ResolveTimeEnd != nil { // 修改字段名
query = query.Where("resolve_time <= ?", *opts.ResolveTimeEnd) // 修改查询字段名
}
// --- 计算总数 ---
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// --- 应用排序条件 ---
orderBy := "trigger_time DESC" // 默认按触发时间倒序
if opts.OrderBy != "" {
orderBy = opts.OrderBy
}
query = query.Order(orderBy)
// --- 分页 ---
offset := (page - 1) * pageSize
err := query.Limit(pageSize).Offset(offset).Find(&results).Error
return results, total, err
}
func (r *gormAlarmRepository) UpdateAlarmNotificationStatus(ctx context.Context, alarmID uint, lastNotifiedAt time.Time, isIgnored bool, ignoredUntil *time.Time) error {
repoCtx := logs.AddFuncName(ctx, r.ctx, "UpdateAlarmNotificationStatus")
// 1. 内部安全地构造 map将强类型参数转换为 GORM 需要的格式
// GORM 的 Updates 方法会正确处理 *time.Time (nil -> DB NULL)
updates := map[string]interface{}{
"last_notified_at": lastNotifiedAt, // time.Time 会被 GORM 视为非空时间
"is_ignored": isIgnored,
"ignored_until": ignoredUntil, // *time.Time (nil) 会被 GORM 写入 NULL
}
// 2. 执行更新
result := r.db.WithContext(repoCtx).
Model(&models.ActiveAlarm{}).
Where("id = ?", alarmID).
Updates(updates) // 仅更新 updates map 中指定的三个字段
if result.Error != nil {
return result.Error
}
return nil
}
// CountAlarmsForNotification 查询满足发送告警消息条件的记录总数
func (r *gormAlarmRepository) CountAlarmsForNotification(ctx context.Context, intervalByLevel map[models.SeverityLevel]uint) (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]uint) ([]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]uint) *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]uint, 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)
}