// Package pollingalert 是失败告警"停机被拒"视图的只读查询,供后台列表与导出共用(add-polling-pause-list-and-batch-toggle)。 package pollingalert import ( "context" "time" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // StopRefusedFilter 是停机被拒视图的附加筛选。 type StopRefusedFilter struct { CarrierID *uint Status *int ICCID string StartTime *time.Time EndTime *time.Time } // StopRefusedRow 是停机被拒告警与卡当前状态的投影。 type StopRefusedRow struct { ID uint CardID uint ICCID string CarrierID uint CarrierName string DeviceID uint NetworkStatus int StopReason string EnablePolling bool HasPackage bool ErrorCategory string FailCount int FirstFailedAt *time.Time LastFailedAt *time.Time LastError string Status int HandledBy uint HandledByName string HandledAt *time.Time HandleRemark string } // ApplyStopRefused 在告警查询上叠加"停机被拒"视图条件:停复机方向为停机,且错误类型为终态或业务错误、或停机失败次数达到阈值。 // 停复机方向告警的最近错误以 "stop: " 开头(见停复机指令执行器)。 func ApplyStopRefused(query *gorm.DB, table string, threshold int) *gorm.DB { if threshold <= 0 { threshold = 5 } prefix := "" if table != "" { prefix = table + "." } return query.Where(prefix+"alert_level = ? AND "+prefix+"check_type = ? AND "+prefix+"last_error LIKE ?", constants.PollingAlertLevelCard, constants.CheckTypeStopResume, constants.StopResumeActionStop+":%"). Where("("+prefix+"error_category IN ? OR "+prefix+"fail_count >= ?)", []string{constants.GatewayClassTerminal, constants.GatewayClassBusiness}, threshold) } // Query 停机被拒视图查询。 type Query struct { db *gorm.DB } // NewQuery 创建停机被拒视图查询。 func NewQuery(db *gorm.DB) *Query { return &Query{db: db} } func (q *Query) base(ctx context.Context, filter StopRefusedFilter, threshold int) *gorm.DB { query := ApplyStopRefused(q.db.WithContext(ctx).Table("tb_polling_failure_alert AS a"), "a", threshold) if filter.CarrierID != nil { query = query.Where("a.carrier_id = ?", *filter.CarrierID) } if filter.Status != nil { query = query.Where("a.status = ?", *filter.Status) } if filter.ICCID != "" { query = query.Where("a.iccid = ?", filter.ICCID) } if filter.StartTime != nil { query = query.Where("a.last_failed_at >= ?", *filter.StartTime) } if filter.EndTime != nil { query = query.Where("a.last_failed_at <= ?", *filter.EndTime) } return query } // Count 统计停机被拒告警数。 func (q *Query) Count(ctx context.Context, filter StopRefusedFilter, threshold int) (int64, error) { var total int64 if err := q.base(ctx, filter, threshold).Count(&total).Error; err != nil { return 0, errors.Wrap(errors.CodeDatabaseError, err, "统计停机被拒告警失败") } return total, nil } // Page 返回一页停机被拒告警,附带所属设备、网络状态、停机原因、轮询开关、是否有生效套餐与处理人。 func (q *Query) Page(ctx context.Context, filter StopRefusedFilter, threshold, offset, limit int) ([]StopRefusedRow, error) { var rows []StopRefusedRow err := q.base(ctx, filter, threshold). Select(`a.id, a.card_id, a.iccid, a.carrier_id, COALESCE(ca.carrier_name, '') AS carrier_name, COALESCE(b.device_id, 0) AS device_id, COALESCE(c.network_status, 0) AS network_status, COALESCE(c.stop_reason, '') AS stop_reason, COALESCE(c.enable_polling, FALSE) AS enable_polling, EXISTS (SELECT 1 FROM tb_package_usage u WHERE u.status = ? AND u.master_usage_id IS NULL AND u.deleted_at IS NULL AND ((b.device_id IS NOT NULL AND u.device_id = b.device_id) OR (b.device_id IS NULL AND u.iot_card_id = a.card_id))) AS has_package, a.error_category, a.fail_count, a.first_failed_at, a.last_failed_at, a.last_error, a.status, a.handled_by, COALESCE(ac.username, '') AS handled_by_name, a.handled_at, a.handle_remark`, constants.PackageUsageStatusActive). Joins("LEFT JOIN tb_iot_card c ON c.id = a.card_id"). Joins("LEFT JOIN tb_device_sim_binding b ON b.iot_card_id = a.card_id AND b.bind_status = ? AND b.deleted_at IS NULL", constants.BindStatusBound). Joins("LEFT JOIN tb_carrier ca ON ca.id = a.carrier_id"). Joins("LEFT JOIN tb_account ac ON ac.id = a.handled_by"). Order("a.last_failed_at DESC NULLS LAST, a.id DESC").Offset(offset).Limit(limit).Scan(&rows).Error if err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询停机被拒告警失败") } return rows, nil }