All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 1h20m10s
GORM 默认把字段 ICCID 映射成 icc_id: - 观测序列设备换卡的本地预期判定读不到当前卡 ICCID,永远判不成立,只能靠调网关重试到上限 - 拒绝停机告警列表 ICCID 一直为空 补列名测试防回归。
123 lines
4.7 KiB
Go
123 lines
4.7 KiB
Go
// 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 `gorm:"column:iccid"` // 显式列名:GORM 默认映射成 icc_id
|
||
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
|
||
}
|