296 lines
9.6 KiB
Go
296 lines
9.6 KiB
Go
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)
|
||
}
|
||
}
|