Files
mengyaping/mengyaping-backend/storage/probe.go
2026-05-16 19:03:32 +08:00

296 lines
9.6 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 storage
import (
"log"
"time"
"gorm.io/gorm"
"mengyaping-backend/config"
"mengyaping-backend/models"
)
func latestToRecord(p *MonitorProbeLatest) models.MonitorRecord {
return models.MonitorRecord{
WebsiteID: p.WebsiteID,
URLID: p.URLID,
URL: p.URL,
StatusCode: p.StatusCode,
Latency: p.LatencyMs,
IsUp: p.IsUp,
Error: p.ErrorText,
CheckedAt: p.CheckedAt,
}
}
// AddRecord 写入最新探测 + 按小时/按天汇总(不插逐条历史)
func (s *Storage) AddRecord(record models.MonitorRecord) error {
hourAt := record.CheckedAt.Truncate(time.Hour)
statDate := time.Date(record.CheckedAt.Year(), record.CheckedAt.Month(), record.CheckedAt.Day(),
0, 0, 0, 0, record.CheckedAt.Location())
upDelta := 0
if record.IsUp {
upDelta = 1
}
return s.db.Transaction(func(tx *gorm.DB) error {
lp := MonitorProbeLatest{
WebsiteID: record.WebsiteID,
URLID: record.URLID,
URL: record.URL,
StatusCode: record.StatusCode,
LatencyMs: record.Latency,
IsUp: record.IsUp,
ErrorText: record.Error,
CheckedAt: record.CheckedAt,
}
if err := tx.Save(&lp).Error; err != nil {
return err
}
if err := tx.Exec(`
INSERT INTO monitor_probe_hour (website_id, url_id, hour_at, probe_count, up_count, latency_sum)
VALUES (?, ?, ?, 1, ?, ?)
ON DUPLICATE KEY UPDATE
probe_count = probe_count + 1,
up_count = up_count + VALUES(up_count),
latency_sum = latency_sum + VALUES(latency_sum)
`, record.WebsiteID, record.URLID, hourAt, upDelta, record.Latency).Error; err != nil {
return err
}
return tx.Exec(`
INSERT INTO monitor_probe_day (website_id, url_id, stat_date, probe_count, up_count, latency_sum)
VALUES (?, ?, ?, 1, ?, ?)
ON DUPLICATE KEY UPDATE
probe_count = probe_count + 1,
up_count = up_count + VALUES(up_count),
latency_sum = latency_sum + VALUES(latency_sum)
`, record.WebsiteID, record.URLID, statDate.Format("2006-01-02"), upDelta, record.Latency).Error
})
}
// GetLatestRecord 最新一条(无记录时返回 nil用 Find 而非 First避免 GORM 将「未找到」打成 record not found 日志)
func (s *Storage) GetLatestRecord(websiteID, urlID string) *models.MonitorRecord {
var p MonitorProbeLatest
if err := s.db.Where("website_id = ? AND url_id = ?", websiteID, urlID).Limit(1).Find(&p).Error; err != nil {
return nil
}
if p.WebsiteID == "" {
return nil
}
m := latestToRecord(&p)
return &m
}
// GetRecords 兼容旧接口:按小时聚合展开为「代表点」(每探测窗口 1 条),仅用于仍依赖 slice 的逻辑;新逻辑请用汇总 API
func (s *Storage) GetRecords(websiteID, urlID string, since time.Time) []models.MonitorRecord {
rows := s.getProbeHourRows(websiteID, urlID, since)
var out []models.MonitorRecord
for _, h := range rows {
if h.ProbeCount <= 0 {
continue
}
avgLat := h.LatencySum / int64(h.ProbeCount)
isUp := h.UpCount*2 >= h.ProbeCount
out = append(out, models.MonitorRecord{
WebsiteID: websiteID,
URLID: urlID,
StatusCode: 0,
Latency: avgLat,
IsUp: isUp,
CheckedAt: h.HourAt,
})
}
return out
}
func (s *Storage) getProbeHourRows(websiteID, urlID string, since time.Time) []MonitorProbeHour {
var rows []MonitorProbeHour
s.db.Where("website_id = ? AND url_id = ? AND hour_at >= ?", websiteID, urlID, since).
Order("hour_at ASC").Find(&rows)
return rows
}
// GetProbeHourRowsForURL 某 URL 自 since 起的小时汇总行(升序)
func (s *Storage) GetProbeHourRowsForURL(websiteID, urlID string, since time.Time) []MonitorProbeHour {
return s.getProbeHourRows(websiteID, urlID, since)
}
// GetProbeLatestKeyMap 全表 latest每 URL 一行key = website_id + "\x00" + url_id
func (s *Storage) GetProbeLatestKeyMap() map[string]models.MonitorRecord {
var rows []MonitorProbeLatest
if err := s.db.Find(&rows).Error; err != nil {
log.Printf("GetProbeLatestKeyMap: %v", err)
return map[string]models.MonitorRecord{}
}
m := make(map[string]models.MonitorRecord, len(rows))
for i := range rows {
k := rows[i].WebsiteID + "\x00" + rows[i].URLID
m[k] = latestToRecord(&rows[i])
}
return m
}
// GroupProbeHoursSince 近窗口内全部小时汇总按站点、URL 分组(列表接口一次查出)
func (s *Storage) GroupProbeHoursSince(since time.Time) map[string]map[string][]MonitorProbeHour {
var rows []MonitorProbeHour
if err := s.db.Where("hour_at >= ?", since).Order("website_id, url_id, hour_at").Find(&rows).Error; err != nil {
log.Printf("GroupProbeHoursSince: %v", err)
return map[string]map[string][]MonitorProbeHour{}
}
out := make(map[string]map[string][]MonitorProbeHour)
for i := range rows {
h := rows[i]
if out[h.WebsiteID] == nil {
out[h.WebsiteID] = make(map[string][]MonitorProbeHour)
}
out[h.WebsiteID][h.URLID] = append(out[h.WebsiteID][h.URLID], h)
}
return out
}
// LoadAllWebsiteDayRollups 自 since 日起各站每日汇总 + 90d 整体可用率(单条 SQL 聚合)
func (s *Storage) LoadAllWebsiteDayRollups(since time.Time) (map[string][]models.DailyStats, map[string]float64) {
sinceDay := time.Date(since.Year(), since.Month(), since.Day(), 0, 0, 0, 0, since.Location())
sinceStr := sinceDay.Format("2006-01-02")
type aggRow struct {
Wid string `gorm:"column:wid"`
D time.Time `gorm:"column:d"`
Pc int64 `gorm:"column:pc"`
Uc int64 `gorm:"column:uc"`
Ls int64 `gorm:"column:ls"`
}
var agg []aggRow
if err := s.db.Raw(`
SELECT website_id AS wid, stat_date AS d, SUM(probe_count) AS pc, SUM(up_count) AS uc, SUM(latency_sum) AS ls
FROM monitor_probe_day
WHERE stat_date >= ?
GROUP BY website_id, stat_date
ORDER BY website_id, stat_date
`, sinceStr).Scan(&agg).Error; err != nil {
log.Printf("LoadAllWebsiteDayRollups: %v", err)
return map[string][]models.DailyStats{}, map[string]float64{}
}
daily := make(map[string][]models.DailyStats)
totP := make(map[string]int64)
totU := make(map[string]int64)
for _, r := range agg {
st := models.DailyStats{
Date: r.D,
TotalCount: int(r.Pc),
UpCount: int(r.Uc),
}
if r.Pc > 0 {
st.AvgLatency = r.Ls / r.Pc
st.Uptime = float64(r.Uc) / float64(r.Pc) * 100
}
daily[r.Wid] = append(daily[r.Wid], st)
totP[r.Wid] += r.Pc
totU[r.Wid] += r.Uc
}
uptime := make(map[string]float64)
for wid, p := range totP {
if p > 0 {
uptime[wid] = float64(totU[wid]) / float64(p) * 100
}
}
return daily, uptime
}
// GetProbeHourTotals 某 URL 在时间窗口内汇总(来自小时表)
func (s *Storage) GetProbeHourTotals(websiteID, urlID string, since time.Time) (probeTotal int64, upTotal int64, latencySum int64) {
type row struct {
P int64
U int64
L int64
}
var r row
s.db.Model(&MonitorProbeHour{}).
Select("COALESCE(SUM(probe_count),0), COALESCE(SUM(up_count),0), COALESCE(SUM(latency_sum),0)").
Where("website_id = ? AND url_id = ? AND hour_at >= ?", websiteID, urlID, since).
Scan(&r)
return r.P, r.U, r.L
}
// GetHourlyStatsForURL 7 天折线用(已聚合)
func (s *Storage) GetHourlyStatsForURL(websiteID, urlID string, since time.Time) []models.HourlyStats {
rows := s.getProbeHourRows(websiteID, urlID, since)
out := make([]models.HourlyStats, 0, len(rows))
for _, h := range rows {
st := models.HourlyStats{
Hour: h.HourAt,
TotalCount: h.ProbeCount,
UpCount: h.UpCount,
}
if h.ProbeCount > 0 {
st.AvgLatency = h.LatencySum / int64(h.ProbeCount)
st.Uptime = float64(h.UpCount) / float64(h.ProbeCount) * 100
}
out = append(out, st)
}
return out
}
// GetWebsiteDailyAggregates 站点维度按日合并(多 URL 汇总到同一天)
func (s *Storage) GetWebsiteDailyAggregates(websiteID string, since time.Time) []models.DailyStats {
type aggRow struct {
D time.Time `gorm:"column:d"`
Pc int64 `gorm:"column:pc"`
Uc int64 `gorm:"column:uc"`
Ls int64 `gorm:"column:ls"`
}
var rows []aggRow
sinceDay := time.Date(since.Year(), since.Month(), since.Day(), 0, 0, 0, 0, since.Location())
s.db.Raw(`
SELECT stat_date AS d, SUM(probe_count) AS pc, SUM(up_count) AS uc, SUM(latency_sum) AS ls
FROM monitor_probe_day
WHERE website_id = ? AND stat_date >= ?
GROUP BY stat_date
ORDER BY stat_date ASC
`, websiteID, sinceDay.Format("2006-01-02")).Scan(&rows)
out := make([]models.DailyStats, 0, len(rows))
for _, r := range rows {
st := models.DailyStats{
Date: r.D,
TotalCount: int(r.Pc),
UpCount: int(r.Uc),
}
if r.Pc > 0 {
st.AvgLatency = r.Ls / r.Pc
st.Uptime = float64(r.Uc) / float64(r.Pc) * 100
}
out = append(out, st)
}
return out
}
// GetWebsiteDayProbeTotals 整站在日期范围内探测次数汇总(用于 90 天可用率)
func (s *Storage) GetWebsiteDayProbeTotals(websiteID string, since time.Time) (probeTotal int64, upTotal int64) {
sinceDay := time.Date(since.Year(), since.Month(), since.Day(), 0, 0, 0, 0, since.Location())
type row struct {
P int64
U int64
}
var r row
s.db.Model(&MonitorProbeDay{}).
Select("COALESCE(SUM(probe_count),0), COALESCE(SUM(up_count),0)").
Where("website_id = ? AND stat_date >= ?", websiteID, sinceDay.Format("2006-01-02")).
Scan(&r)
return r.P, r.U
}
func (s *Storage) purgeProbeRollups() {
cfg := config.GetConfig()
now := time.Now()
dayCutoff := now.AddDate(0, 0, -cfg.Monitor.HistoryDays).Truncate(24 * time.Hour)
if err := s.db.Where("stat_date < ?", dayCutoff.Format("2006-01-02")).Delete(&MonitorProbeDay{}).Error; err != nil {
log.Printf("清理 monitor_probe_day: %v", err)
}
// 小时图保留约 10 天(覆盖 7 天曲线 + 余量)
hourCutoff := now.Add(-10 * 24 * time.Hour).Truncate(time.Hour)
if err := s.db.Where("hour_at < ?", hourCutoff).Delete(&MonitorProbeHour{}).Error; err != nil {
log.Printf("清理 monitor_probe_hour: %v", err)
}
}