chore: sync local updates
This commit is contained in:
@@ -1,418 +1,488 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"mengyaping-backend/config"
|
||||
"mengyaping-backend/models"
|
||||
"mengyaping-backend/storage"
|
||||
"mengyaping-backend/utils"
|
||||
)
|
||||
|
||||
// MonitorService 监控服务
|
||||
type MonitorService struct {
|
||||
httpClient *utils.HTTPClient
|
||||
storage *storage.Storage
|
||||
stopCh chan struct{}
|
||||
running bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var (
|
||||
monitorService *MonitorService
|
||||
monitorOnce sync.Once
|
||||
)
|
||||
|
||||
// GetMonitorService 获取监控服务单例
|
||||
func GetMonitorService() *MonitorService {
|
||||
monitorOnce.Do(func() {
|
||||
cfg := config.GetConfig()
|
||||
monitorService = &MonitorService{
|
||||
httpClient: utils.NewHTTPClient(cfg.Monitor.Timeout),
|
||||
storage: storage.GetStorage(),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
})
|
||||
return monitorService
|
||||
}
|
||||
|
||||
// Start 启动监控服务
|
||||
func (s *MonitorService) Start() {
|
||||
s.mu.Lock()
|
||||
if s.running {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.running = true
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Println("监控服务已启动")
|
||||
|
||||
// 立即执行一次检测
|
||||
go s.checkAll()
|
||||
|
||||
// 定时检测
|
||||
cfg := config.GetConfig()
|
||||
ticker := time.NewTicker(cfg.Monitor.Interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
go s.checkAll()
|
||||
case <-s.stopCh:
|
||||
log.Println("监控服务已停止")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止监控服务
|
||||
func (s *MonitorService) Stop() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.running {
|
||||
close(s.stopCh)
|
||||
s.running = false
|
||||
}
|
||||
}
|
||||
|
||||
// checkAll 检查所有网站(错峰执行,避免并发暴涨)
|
||||
func (s *MonitorService) checkAll() {
|
||||
websites := s.storage.GetWebsites()
|
||||
semaphore := make(chan struct{}, 3) // 最多 3 个并发检测
|
||||
|
||||
for i, website := range websites {
|
||||
// 每个网站之间间隔 1 秒,把检测分散开
|
||||
if i > 0 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, urlInfo := range website.URLs {
|
||||
wg.Add(1)
|
||||
go func(w models.Website, u models.URLInfo) {
|
||||
defer wg.Done()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
s.checkURL(w, u)
|
||||
}(website, urlInfo)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 检测完毕后,逐个解析 DNS
|
||||
s.resolveAllWebsiteIPs(websites)
|
||||
|
||||
// 保存记录
|
||||
s.storage.SaveAll()
|
||||
log.Printf("本轮检测完成,共 %d 个网站", len(websites))
|
||||
}
|
||||
|
||||
// resolveAllWebsiteIPs 逐个解析所有网站域名 IP(每次都刷新)
|
||||
func (s *MonitorService) resolveAllWebsiteIPs(websites []models.Website) {
|
||||
for i, website := range websites {
|
||||
if len(website.URLs) == 0 {
|
||||
continue
|
||||
}
|
||||
if i > 0 {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
s.resolveWebsiteIP(website)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveWebsiteIP 解析单个网站的域名 IP
|
||||
func (s *MonitorService) resolveWebsiteIP(website models.Website) {
|
||||
if len(website.URLs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ips, err := utils.ResolveDomainIPs(website.URLs[0].URL)
|
||||
if err != nil {
|
||||
log.Printf("DNS解析失败 [%s]: %v", website.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(ips) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
w := s.storage.GetWebsite(website.ID)
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
|
||||
w.IPAddresses = ips
|
||||
w.UpdatedAt = time.Now()
|
||||
s.storage.UpdateWebsite(*w)
|
||||
log.Printf("DNS解析 [%s] → %v", website.Name, ips)
|
||||
}
|
||||
|
||||
// checkURL 检查单个URL(带重试)
|
||||
func (s *MonitorService) checkURL(website models.Website, urlInfo models.URLInfo) {
|
||||
cfg := config.GetConfig()
|
||||
maxRetries := cfg.Monitor.RetryCount
|
||||
|
||||
var result utils.CheckResult
|
||||
|
||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
time.Sleep(time.Duration(attempt) * 2 * time.Second)
|
||||
log.Printf("重试 [%s] %s - 第 %d 次重试", website.Name, urlInfo.URL, attempt)
|
||||
}
|
||||
|
||||
result = s.httpClient.CheckWebsiteStatus(urlInfo.URL)
|
||||
if result.Error == nil && utils.IsSuccessStatus(result.StatusCode) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
record := models.MonitorRecord{
|
||||
WebsiteID: website.ID,
|
||||
URLID: urlInfo.ID,
|
||||
URL: urlInfo.URL,
|
||||
StatusCode: result.StatusCode,
|
||||
Latency: result.Latency.Milliseconds(),
|
||||
IsUp: result.Error == nil && utils.IsSuccessStatus(result.StatusCode),
|
||||
CheckedAt: time.Now(),
|
||||
}
|
||||
|
||||
if result.Error != nil {
|
||||
record.Error = result.Error.Error()
|
||||
}
|
||||
|
||||
s.storage.AddRecord(record)
|
||||
|
||||
// 仅当网站无标题时才做完整检测来获取元数据
|
||||
if record.IsUp && website.Title == "" {
|
||||
fullResult := s.httpClient.CheckWebsite(urlInfo.URL)
|
||||
if fullResult.Title != "" {
|
||||
w := s.storage.GetWebsite(website.ID)
|
||||
if w != nil {
|
||||
w.Title = fullResult.Title
|
||||
w.UpdatedAt = time.Now()
|
||||
s.storage.UpdateWebsite(*w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("检测 [%s] %s - 状态码: %d, 延迟: %dms, 可用: %v",
|
||||
website.Name, urlInfo.URL, result.StatusCode, result.Latency.Milliseconds(), record.IsUp)
|
||||
}
|
||||
|
||||
// CheckWebsiteNow 立即检查指定网站(状态 + DNS,等待完成后保存)
|
||||
func (s *MonitorService) CheckWebsiteNow(websiteID string) {
|
||||
website := s.storage.GetWebsite(websiteID)
|
||||
if website == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 逐个检测该网站的所有 URL
|
||||
for _, urlInfo := range website.URLs {
|
||||
s.checkURL(*website, urlInfo)
|
||||
}
|
||||
|
||||
// 刷新 DNS
|
||||
s.resolveWebsiteIP(*website)
|
||||
|
||||
s.storage.SaveAll()
|
||||
}
|
||||
|
||||
// GetWebsiteStatus 获取网站状态
|
||||
func (s *MonitorService) GetWebsiteStatus(websiteID string) *models.WebsiteStatus {
|
||||
website := s.storage.GetWebsite(websiteID)
|
||||
if website == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
status := &models.WebsiteStatus{
|
||||
Website: *website,
|
||||
URLStatuses: []models.URLStatus{},
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
since24h := now.Add(-24 * time.Hour)
|
||||
since7d := now.Add(-7 * 24 * time.Hour)
|
||||
since90d := now.Add(-90 * 24 * time.Hour)
|
||||
|
||||
var totalUptime24h, totalUptime7d float64
|
||||
var urlCount int
|
||||
var allRecords90d []models.MonitorRecord
|
||||
|
||||
for _, urlInfo := range website.URLs {
|
||||
urlStatus := s.getURLStatus(website.ID, urlInfo, since24h, since7d)
|
||||
status.URLStatuses = append(status.URLStatuses, urlStatus)
|
||||
|
||||
totalUptime24h += urlStatus.Uptime24h
|
||||
totalUptime7d += urlStatus.Uptime7d
|
||||
urlCount++
|
||||
|
||||
records90d := s.storage.GetRecords(website.ID, urlInfo.ID, since90d)
|
||||
allRecords90d = append(allRecords90d, records90d...)
|
||||
}
|
||||
|
||||
if urlCount > 0 {
|
||||
status.Uptime24h = totalUptime24h / float64(urlCount)
|
||||
status.Uptime7d = totalUptime7d / float64(urlCount)
|
||||
}
|
||||
|
||||
// 90 天逐日统计
|
||||
status.DailyHistory = s.aggregateByDay(allRecords90d)
|
||||
if len(allRecords90d) > 0 {
|
||||
upCount := 0
|
||||
for _, r := range allRecords90d {
|
||||
if r.IsUp {
|
||||
upCount++
|
||||
}
|
||||
}
|
||||
status.Uptime90d = float64(upCount) / float64(len(allRecords90d)) * 100
|
||||
}
|
||||
|
||||
// 获取最后检测时间
|
||||
for _, urlStatus := range status.URLStatuses {
|
||||
if urlStatus.CurrentState.CheckedAt.After(status.LastChecked) {
|
||||
status.LastChecked = urlStatus.CurrentState.CheckedAt
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
// getURLStatus 获取URL状态
|
||||
func (s *MonitorService) getURLStatus(websiteID string, urlInfo models.URLInfo, since24h, since7d time.Time) models.URLStatus {
|
||||
urlStatus := models.URLStatus{
|
||||
URLInfo: urlInfo,
|
||||
}
|
||||
|
||||
// 获取最新记录
|
||||
latest := s.storage.GetLatestRecord(websiteID, urlInfo.ID)
|
||||
if latest != nil {
|
||||
urlStatus.CurrentState = *latest
|
||||
}
|
||||
|
||||
// 获取24小时记录
|
||||
records24h := s.storage.GetRecords(websiteID, urlInfo.ID, since24h)
|
||||
urlStatus.History24h = records24h
|
||||
|
||||
// 计算24小时可用率
|
||||
if len(records24h) > 0 {
|
||||
upCount := 0
|
||||
var totalLatency int64
|
||||
for _, r := range records24h {
|
||||
if r.IsUp {
|
||||
upCount++
|
||||
}
|
||||
totalLatency += r.Latency
|
||||
}
|
||||
urlStatus.Uptime24h = float64(upCount) / float64(len(records24h)) * 100
|
||||
urlStatus.AvgLatency = totalLatency / int64(len(records24h))
|
||||
}
|
||||
|
||||
// 获取7天记录并按小时统计
|
||||
records7d := s.storage.GetRecords(websiteID, urlInfo.ID, since7d)
|
||||
urlStatus.History7d = s.aggregateByHour(records7d)
|
||||
|
||||
// 计算7天可用率
|
||||
if len(records7d) > 0 {
|
||||
upCount := 0
|
||||
for _, r := range records7d {
|
||||
if r.IsUp {
|
||||
upCount++
|
||||
}
|
||||
}
|
||||
urlStatus.Uptime7d = float64(upCount) / float64(len(records7d)) * 100
|
||||
}
|
||||
|
||||
return urlStatus
|
||||
}
|
||||
|
||||
// aggregateByHour 按小时聚合记录
|
||||
func (s *MonitorService) aggregateByHour(records []models.MonitorRecord) []models.HourlyStats {
|
||||
hourlyMap := make(map[string]*models.HourlyStats)
|
||||
|
||||
for _, r := range records {
|
||||
hourKey := r.CheckedAt.Truncate(time.Hour).Format(time.RFC3339)
|
||||
|
||||
if _, exists := hourlyMap[hourKey]; !exists {
|
||||
hourlyMap[hourKey] = &models.HourlyStats{
|
||||
Hour: r.CheckedAt.Truncate(time.Hour),
|
||||
}
|
||||
}
|
||||
|
||||
stats := hourlyMap[hourKey]
|
||||
stats.TotalCount++
|
||||
if r.IsUp {
|
||||
stats.UpCount++
|
||||
}
|
||||
stats.AvgLatency += r.Latency
|
||||
}
|
||||
|
||||
var result []models.HourlyStats
|
||||
for _, stats := range hourlyMap {
|
||||
if stats.TotalCount > 0 {
|
||||
stats.AvgLatency /= int64(stats.TotalCount)
|
||||
stats.Uptime = float64(stats.UpCount) / float64(stats.TotalCount) * 100
|
||||
}
|
||||
result = append(result, *stats)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// aggregateByDay 按天聚合记录
|
||||
func (s *MonitorService) aggregateByDay(records []models.MonitorRecord) []models.DailyStats {
|
||||
dayMap := make(map[string]*models.DailyStats)
|
||||
|
||||
for _, r := range records {
|
||||
dayTime := time.Date(r.CheckedAt.Year(), r.CheckedAt.Month(), r.CheckedAt.Day(), 0, 0, 0, 0, r.CheckedAt.Location())
|
||||
dayKey := dayTime.Format("2006-01-02")
|
||||
|
||||
if _, exists := dayMap[dayKey]; !exists {
|
||||
dayMap[dayKey] = &models.DailyStats{
|
||||
Date: dayTime,
|
||||
}
|
||||
}
|
||||
|
||||
stats := dayMap[dayKey]
|
||||
stats.TotalCount++
|
||||
if r.IsUp {
|
||||
stats.UpCount++
|
||||
}
|
||||
stats.AvgLatency += r.Latency
|
||||
}
|
||||
|
||||
result := make([]models.DailyStats, 0, len(dayMap))
|
||||
for _, stats := range dayMap {
|
||||
if stats.TotalCount > 0 {
|
||||
stats.AvgLatency /= int64(stats.TotalCount)
|
||||
stats.Uptime = float64(stats.UpCount) / float64(stats.TotalCount) * 100
|
||||
}
|
||||
result = append(result, *stats)
|
||||
}
|
||||
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].Date.Before(result[j].Date)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAllWebsiteStatuses 获取所有网站状态
|
||||
func (s *MonitorService) GetAllWebsiteStatuses() []models.WebsiteStatus {
|
||||
websites := s.storage.GetWebsites()
|
||||
var statuses []models.WebsiteStatus
|
||||
|
||||
for _, website := range websites {
|
||||
status := s.GetWebsiteStatus(website.ID)
|
||||
if status != nil {
|
||||
statuses = append(statuses, *status)
|
||||
}
|
||||
}
|
||||
|
||||
return statuses
|
||||
}
|
||||
package services
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"mengyaping-backend/config"
|
||||
"mengyaping-backend/models"
|
||||
"mengyaping-backend/storage"
|
||||
"mengyaping-backend/utils"
|
||||
)
|
||||
|
||||
// MonitorService 监控服务
|
||||
type MonitorService struct {
|
||||
httpClient *utils.HTTPClient
|
||||
storage *storage.Storage
|
||||
stopCh chan struct{}
|
||||
rescheduleCh chan struct{}
|
||||
running bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var (
|
||||
monitorService *MonitorService
|
||||
monitorOnce sync.Once
|
||||
)
|
||||
|
||||
// GetMonitorService 获取监控服务单例
|
||||
func GetMonitorService() *MonitorService {
|
||||
monitorOnce.Do(func() {
|
||||
st := storage.GetStorage()
|
||||
cfg := config.GetConfig()
|
||||
monitorService = &MonitorService{
|
||||
httpClient: utils.NewHTTPClient(cfg.Monitor.Timeout),
|
||||
storage: st,
|
||||
stopCh: make(chan struct{}),
|
||||
rescheduleCh: make(chan struct{}, 1),
|
||||
}
|
||||
})
|
||||
return monitorService
|
||||
}
|
||||
|
||||
// Start 启动监控服务
|
||||
func (s *MonitorService) Start() {
|
||||
s.mu.Lock()
|
||||
if s.running {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.running = true
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Println("监控服务已启动")
|
||||
go s.runLoop()
|
||||
}
|
||||
|
||||
// Reschedule 在检测间隔配置变更后唤醒主循环以使用新周期(非阻塞)
|
||||
func (s *MonitorService) Reschedule() {
|
||||
select {
|
||||
case s.rescheduleCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MonitorService) runLoop() {
|
||||
go s.checkAll()
|
||||
|
||||
d := config.GetConfig().Monitor.Interval
|
||||
if d <= 0 {
|
||||
d = 30 * time.Minute
|
||||
}
|
||||
timer := time.NewTimer(d)
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.stopCh:
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
log.Println("监控服务已停止")
|
||||
return
|
||||
case <-s.rescheduleCh:
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
d = config.GetConfig().Monitor.Interval
|
||||
if d <= 0 {
|
||||
d = 30 * time.Minute
|
||||
}
|
||||
timer.Reset(d)
|
||||
case <-timer.C:
|
||||
go s.checkAll()
|
||||
d = config.GetConfig().Monitor.Interval
|
||||
if d <= 0 {
|
||||
d = 30 * time.Minute
|
||||
}
|
||||
timer.Reset(d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止监控服务
|
||||
func (s *MonitorService) Stop() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.running {
|
||||
close(s.stopCh)
|
||||
s.running = false
|
||||
}
|
||||
}
|
||||
|
||||
// checkAll 检查所有网站(错峰执行,避免并发暴涨)
|
||||
func (s *MonitorService) checkAll() {
|
||||
websites := s.storage.GetWebsites()
|
||||
semaphore := make(chan struct{}, 3) // 最多 3 个并发检测
|
||||
|
||||
for i, website := range websites {
|
||||
// 每个网站之间间隔 1 秒,把检测分散开
|
||||
if i > 0 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, urlInfo := range website.URLs {
|
||||
wg.Add(1)
|
||||
go func(w models.Website, u models.URLInfo) {
|
||||
defer wg.Done()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
s.checkURL(w, u)
|
||||
}(website, urlInfo)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 检测完毕后,逐个解析 DNS
|
||||
s.resolveAllWebsiteIPs(websites)
|
||||
|
||||
// 保存记录
|
||||
s.storage.SaveAll()
|
||||
log.Printf("本轮检测完成,共 %d 个网站", len(websites))
|
||||
}
|
||||
|
||||
// resolveAllWebsiteIPs 逐个解析所有网站域名 IP(每次都刷新)
|
||||
func (s *MonitorService) resolveAllWebsiteIPs(websites []models.Website) {
|
||||
for i, website := range websites {
|
||||
if len(website.URLs) == 0 {
|
||||
continue
|
||||
}
|
||||
if i > 0 {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
s.resolveWebsiteIP(website)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveWebsiteIP 解析单个网站的域名 IP
|
||||
func (s *MonitorService) resolveWebsiteIP(website models.Website) {
|
||||
if len(website.URLs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if utils.IsIntranetURL(website.URLs[0].URL) {
|
||||
w := s.storage.GetWebsite(website.ID)
|
||||
if w != nil && len(w.IPAddresses) > 0 {
|
||||
w.IPAddresses = nil
|
||||
w.UpdatedAt = time.Now()
|
||||
s.storage.UpdateWebsite(*w)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ips, err := utils.ResolveDomainIPs(website.URLs[0].URL)
|
||||
if err != nil {
|
||||
log.Printf("DNS解析失败 [%s]: %v", website.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(ips) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
w := s.storage.GetWebsite(website.ID)
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
|
||||
w.IPAddresses = ips
|
||||
w.UpdatedAt = time.Now()
|
||||
s.storage.UpdateWebsite(*w)
|
||||
log.Printf("DNS解析 [%s] → %v", website.Name, ips)
|
||||
}
|
||||
|
||||
// checkURL 检查单个URL(带重试)
|
||||
func (s *MonitorService) checkURL(website models.Website, urlInfo models.URLInfo) {
|
||||
cfg := config.GetConfig()
|
||||
maxRetries := cfg.Monitor.RetryCount
|
||||
|
||||
var result utils.CheckResult
|
||||
|
||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
time.Sleep(time.Duration(attempt) * 2 * time.Second)
|
||||
log.Printf("重试 [%s] %s - 第 %d 次重试", website.Name, urlInfo.URL, attempt)
|
||||
}
|
||||
|
||||
result = s.httpClient.CheckWebsiteStatus(urlInfo.URL)
|
||||
if result.Error == nil && utils.IsSuccessStatus(result.StatusCode) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
record := models.MonitorRecord{
|
||||
WebsiteID: website.ID,
|
||||
URLID: urlInfo.ID,
|
||||
URL: urlInfo.URL,
|
||||
StatusCode: result.StatusCode,
|
||||
Latency: result.Latency.Milliseconds(),
|
||||
IsUp: result.Error == nil && utils.IsSuccessStatus(result.StatusCode),
|
||||
CheckedAt: time.Now(),
|
||||
}
|
||||
|
||||
if result.Error != nil {
|
||||
record.Error = result.Error.Error()
|
||||
}
|
||||
|
||||
s.storage.AddRecord(record)
|
||||
|
||||
// 仅当网站无标题时才做完整检测来获取元数据
|
||||
if record.IsUp && website.Title == "" {
|
||||
fullResult := s.httpClient.CheckWebsite(urlInfo.URL)
|
||||
if fullResult.Title != "" {
|
||||
w := s.storage.GetWebsite(website.ID)
|
||||
if w != nil {
|
||||
w.Title = fullResult.Title
|
||||
w.UpdatedAt = time.Now()
|
||||
s.storage.UpdateWebsite(*w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("检测 [%s] %s - 状态码: %d, 延迟: %dms, 可用: %v",
|
||||
website.Name, urlInfo.URL, result.StatusCode, result.Latency.Milliseconds(), record.IsUp)
|
||||
}
|
||||
|
||||
// CheckWebsiteNow 立即检查指定网站(状态 + DNS,等待完成后保存)
|
||||
func (s *MonitorService) CheckWebsiteNow(websiteID string) {
|
||||
website := s.storage.GetWebsite(websiteID)
|
||||
if website == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 逐个检测该网站的所有 URL
|
||||
for _, urlInfo := range website.URLs {
|
||||
s.checkURL(*website, urlInfo)
|
||||
}
|
||||
|
||||
// 刷新 DNS
|
||||
s.resolveWebsiteIP(*website)
|
||||
|
||||
s.storage.SaveAll()
|
||||
}
|
||||
|
||||
// GetWebsiteStatus 获取网站状态
|
||||
func (s *MonitorService) GetWebsiteStatus(websiteID string) *models.WebsiteStatus {
|
||||
website := s.storage.GetWebsite(websiteID)
|
||||
if website == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
isIntranet := len(website.URLs) > 0 && utils.IsIntranetURL(website.URLs[0].URL)
|
||||
status := &models.WebsiteStatus{
|
||||
Website: *website,
|
||||
IsIntranet: isIntranet,
|
||||
URLStatuses: []models.URLStatus{},
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
since24h := now.Add(-24 * time.Hour)
|
||||
since7d := now.Add(-7 * 24 * time.Hour)
|
||||
since90d := now.Add(-90 * 24 * time.Hour)
|
||||
|
||||
var totalUptime24h, totalUptime7d float64
|
||||
var urlCount int
|
||||
|
||||
for _, urlInfo := range website.URLs {
|
||||
urlStatus := s.getURLStatus(website.ID, urlInfo, since24h, since7d)
|
||||
status.URLStatuses = append(status.URLStatuses, urlStatus)
|
||||
|
||||
totalUptime24h += urlStatus.Uptime24h
|
||||
totalUptime7d += urlStatus.Uptime7d
|
||||
urlCount++
|
||||
}
|
||||
|
||||
if urlCount > 0 {
|
||||
status.Uptime24h = totalUptime24h / float64(urlCount)
|
||||
status.Uptime7d = totalUptime7d / float64(urlCount)
|
||||
}
|
||||
|
||||
status.DailyHistory = s.storage.GetWebsiteDailyAggregates(website.ID, since90d)
|
||||
if pt, ut := s.storage.GetWebsiteDayProbeTotals(website.ID, since90d); pt > 0 {
|
||||
status.Uptime90d = float64(ut) / float64(pt) * 100
|
||||
}
|
||||
|
||||
// 获取最后检测时间
|
||||
for _, urlStatus := range status.URLStatuses {
|
||||
if urlStatus.CurrentState.CheckedAt.After(status.LastChecked) {
|
||||
status.LastChecked = urlStatus.CurrentState.CheckedAt
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
// getURLStatus 获取URL状态:每个 URL 仅 1 次读小时表 + 1 次读 latest(在内存拆 24h/7d)
|
||||
func (s *MonitorService) getURLStatus(websiteID string, urlInfo models.URLInfo, since24h, since7d time.Time) models.URLStatus {
|
||||
var latest *models.MonitorRecord
|
||||
if rec := s.storage.GetLatestRecord(websiteID, urlInfo.ID); rec != nil {
|
||||
latest = rec
|
||||
}
|
||||
hRows := s.storage.GetProbeHourRowsForURL(websiteID, urlInfo.ID, since7d)
|
||||
return s.buildURLStatusFromHours(websiteID, urlInfo, since24h, since7d, latest, hRows)
|
||||
}
|
||||
|
||||
func hourlyStatFromProbeRow(h storage.MonitorProbeHour) models.HourlyStats {
|
||||
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
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
func hourWindowsFromRows(rows []storage.MonitorProbeHour, since24h, since7d time.Time) (
|
||||
stats24h []models.HourlyStats,
|
||||
stats7d []models.HourlyStats,
|
||||
t24, u24, l24, t7, u7 int64,
|
||||
) {
|
||||
for _, h := range rows {
|
||||
if !h.HourAt.Before(since7d) {
|
||||
t7 += int64(h.ProbeCount)
|
||||
u7 += int64(h.UpCount)
|
||||
stats7d = append(stats7d, hourlyStatFromProbeRow(h))
|
||||
}
|
||||
if !h.HourAt.Before(since24h) {
|
||||
t24 += int64(h.ProbeCount)
|
||||
u24 += int64(h.UpCount)
|
||||
l24 += h.LatencySum
|
||||
stats24h = append(stats24h, hourlyStatFromProbeRow(h))
|
||||
}
|
||||
}
|
||||
return stats24h, stats7d, t24, u24, l24, t7, u7
|
||||
}
|
||||
|
||||
func (s *MonitorService) buildURLStatusFromHours(
|
||||
websiteID string,
|
||||
urlInfo models.URLInfo,
|
||||
since24h, since7d time.Time,
|
||||
latest *models.MonitorRecord,
|
||||
hRows []storage.MonitorProbeHour,
|
||||
) models.URLStatus {
|
||||
urlStatus := models.URLStatus{URLInfo: urlInfo}
|
||||
if latest != nil {
|
||||
urlStatus.CurrentState = *latest
|
||||
}
|
||||
s24, s7, t24, u24, l24, t7, u7 := hourWindowsFromRows(hRows, since24h, since7d)
|
||||
urlStatus.History24h = hourlyStatsToMonitorRecords(websiteID, urlInfo.ID, s24)
|
||||
if t24 > 0 {
|
||||
urlStatus.Uptime24h = float64(u24) / float64(t24) * 100
|
||||
urlStatus.AvgLatency = l24 / t24
|
||||
}
|
||||
urlStatus.History7d = s7
|
||||
if t7 > 0 {
|
||||
urlStatus.Uptime7d = float64(u7) / float64(t7) * 100
|
||||
}
|
||||
return urlStatus
|
||||
}
|
||||
|
||||
func hourlyStatsToMonitorRecords(websiteID, urlID string, stats []models.HourlyStats) []models.MonitorRecord {
|
||||
out := make([]models.MonitorRecord, 0, len(stats))
|
||||
for _, h := range stats {
|
||||
isUp := h.TotalCount > 0 && h.UpCount*2 >= h.TotalCount
|
||||
out = append(out, models.MonitorRecord{
|
||||
WebsiteID: websiteID,
|
||||
URLID: urlID,
|
||||
Latency: h.AvgLatency,
|
||||
IsUp: isUp,
|
||||
CheckedAt: h.Hour,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetAllWebsiteStatuses 获取所有网站状态(批量查 probe 表,避免每个站点/URL 多次 SQL)
|
||||
func (s *MonitorService) GetAllWebsiteStatuses() []models.WebsiteStatus {
|
||||
websites := s.storage.GetWebsites()
|
||||
if len(websites) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
since24h := now.Add(-24 * time.Hour)
|
||||
since7d := now.Add(-7 * 24 * time.Hour)
|
||||
since90d := now.Add(-90 * 24 * time.Hour)
|
||||
since90dDay := time.Date(since90d.Year(), since90d.Month(), since90d.Day(), 0, 0, 0, 0, since90d.Location())
|
||||
|
||||
latestMap := s.storage.GetProbeLatestKeyMap()
|
||||
hourMap := s.storage.GroupProbeHoursSince(since7d)
|
||||
dailyBySite, uptime90dBySite := s.storage.LoadAllWebsiteDayRollups(since90dDay)
|
||||
|
||||
statuses := make([]models.WebsiteStatus, 0, len(websites))
|
||||
for _, w := range websites {
|
||||
daily := dailyBySite[w.ID]
|
||||
u90 := uptime90dBySite[w.ID]
|
||||
st := s.buildWebsiteStatusBulk(w, since24h, since7d, latestMap, hourMap, daily, u90)
|
||||
if st != nil {
|
||||
statuses = append(statuses, *st)
|
||||
}
|
||||
}
|
||||
return statuses
|
||||
}
|
||||
|
||||
func (s *MonitorService) buildWebsiteStatusBulk(
|
||||
w models.Website,
|
||||
since24h, since7d time.Time,
|
||||
latestMap map[string]models.MonitorRecord,
|
||||
hourMap map[string]map[string][]storage.MonitorProbeHour,
|
||||
daily []models.DailyStats,
|
||||
uptime90d float64,
|
||||
) *models.WebsiteStatus {
|
||||
isIntranet := len(w.URLs) > 0 && utils.IsIntranetURL(w.URLs[0].URL)
|
||||
status := &models.WebsiteStatus{
|
||||
Website: w,
|
||||
IsIntranet: isIntranet,
|
||||
URLStatuses: []models.URLStatus{},
|
||||
}
|
||||
|
||||
siteHours := hourMap[w.ID]
|
||||
if siteHours == nil {
|
||||
siteHours = map[string][]storage.MonitorProbeHour{}
|
||||
}
|
||||
|
||||
var totalUptime24h, totalUptime7d float64
|
||||
urlCount := 0
|
||||
for _, urlInfo := range w.URLs {
|
||||
key := w.ID + "\x00" + urlInfo.ID
|
||||
var latest *models.MonitorRecord
|
||||
if rec, ok := latestMap[key]; ok {
|
||||
r := rec
|
||||
latest = &r
|
||||
}
|
||||
hRows := siteHours[urlInfo.ID]
|
||||
urlSt := s.buildURLStatusFromHours(w.ID, urlInfo, since24h, since7d, latest, hRows)
|
||||
status.URLStatuses = append(status.URLStatuses, urlSt)
|
||||
totalUptime24h += urlSt.Uptime24h
|
||||
totalUptime7d += urlSt.Uptime7d
|
||||
urlCount++
|
||||
}
|
||||
if urlCount > 0 {
|
||||
status.Uptime24h = totalUptime24h / float64(urlCount)
|
||||
status.Uptime7d = totalUptime7d / float64(urlCount)
|
||||
}
|
||||
if daily != nil {
|
||||
status.DailyHistory = daily
|
||||
} else {
|
||||
status.DailyHistory = []models.DailyStats{}
|
||||
}
|
||||
status.Uptime90d = uptime90d
|
||||
for _, urlSt := range status.URLStatuses {
|
||||
if urlSt.CurrentState.CheckedAt.After(status.LastChecked) {
|
||||
status.LastChecked = urlSt.CurrentState.CheckedAt
|
||||
}
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
@@ -1,142 +1,177 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"mengyaping-backend/models"
|
||||
"mengyaping-backend/storage"
|
||||
"mengyaping-backend/utils"
|
||||
)
|
||||
|
||||
// WebsiteService 网站服务
|
||||
type WebsiteService struct {
|
||||
storage *storage.Storage
|
||||
}
|
||||
|
||||
// NewWebsiteService 创建网站服务
|
||||
func NewWebsiteService() *WebsiteService {
|
||||
return &WebsiteService{
|
||||
storage: storage.GetStorage(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWebsite 创建网站
|
||||
func (s *WebsiteService) CreateWebsite(req models.CreateWebsiteRequest) (*models.Website, error) {
|
||||
groups := req.Groups
|
||||
if len(groups) == 0 && req.Group != "" {
|
||||
groups = []string{req.Group}
|
||||
}
|
||||
|
||||
website := models.Website{
|
||||
ID: utils.GenerateID(),
|
||||
Name: req.Name,
|
||||
Groups: groups,
|
||||
URLs: make([]models.URLInfo, 0),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
for _, url := range req.URLs {
|
||||
urlInfo := models.URLInfo{
|
||||
ID: utils.GenerateShortID(),
|
||||
URL: url,
|
||||
}
|
||||
website.URLs = append(website.URLs, urlInfo)
|
||||
}
|
||||
|
||||
// 创建前先解析域名 IP
|
||||
if len(req.URLs) > 0 {
|
||||
if ips, err := utils.ResolveDomainIPs(req.URLs[0]); err == nil {
|
||||
website.IPAddresses = ips
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.storage.AddWebsite(website); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 立即检测该网站
|
||||
go GetMonitorService().CheckWebsiteNow(website.ID)
|
||||
|
||||
return &website, nil
|
||||
}
|
||||
|
||||
// GetWebsite 获取网站
|
||||
func (s *WebsiteService) GetWebsite(id string) *models.Website {
|
||||
return s.storage.GetWebsite(id)
|
||||
}
|
||||
|
||||
// GetAllWebsites 获取所有网站
|
||||
func (s *WebsiteService) GetAllWebsites() []models.Website {
|
||||
return s.storage.GetWebsites()
|
||||
}
|
||||
|
||||
// UpdateWebsite 更新网站
|
||||
func (s *WebsiteService) UpdateWebsite(id string, req models.UpdateWebsiteRequest) (*models.Website, error) {
|
||||
website := s.storage.GetWebsite(id)
|
||||
if website == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
website.Name = req.Name
|
||||
}
|
||||
if len(req.Groups) > 0 {
|
||||
website.Groups = req.Groups
|
||||
} else if req.Group != "" {
|
||||
website.Groups = []string{req.Group}
|
||||
}
|
||||
if len(req.URLs) > 0 {
|
||||
// 保留已有URL的ID,添加新URL
|
||||
existingURLs := make(map[string]models.URLInfo)
|
||||
for _, u := range website.URLs {
|
||||
existingURLs[u.URL] = u
|
||||
}
|
||||
|
||||
newURLs := make([]models.URLInfo, 0)
|
||||
for _, url := range req.URLs {
|
||||
if existing, ok := existingURLs[url]; ok {
|
||||
newURLs = append(newURLs, existing)
|
||||
} else {
|
||||
newURLs = append(newURLs, models.URLInfo{
|
||||
ID: utils.GenerateShortID(),
|
||||
URL: url,
|
||||
})
|
||||
}
|
||||
}
|
||||
website.URLs = newURLs
|
||||
website.IPAddresses = nil // URL 变更后清空 IP,等下次检测重新解析
|
||||
}
|
||||
|
||||
website.UpdatedAt = time.Now()
|
||||
|
||||
if err := s.storage.UpdateWebsite(*website); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return website, nil
|
||||
}
|
||||
|
||||
// DeleteWebsite 删除网站
|
||||
func (s *WebsiteService) DeleteWebsite(id string) error {
|
||||
return s.storage.DeleteWebsite(id)
|
||||
}
|
||||
|
||||
// GetGroups 获取所有分组
|
||||
func (s *WebsiteService) GetGroups() []models.Group {
|
||||
return s.storage.GetGroups()
|
||||
}
|
||||
|
||||
// AddGroup 添加分组
|
||||
func (s *WebsiteService) AddGroup(name string) (*models.Group, error) {
|
||||
group := models.Group{
|
||||
ID: utils.GenerateShortID(),
|
||||
Name: name,
|
||||
}
|
||||
|
||||
if err := s.storage.AddGroup(group); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &group, nil
|
||||
}
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"mengyaping-backend/models"
|
||||
"mengyaping-backend/storage"
|
||||
"mengyaping-backend/utils"
|
||||
)
|
||||
|
||||
// WebsiteService 网站服务
|
||||
type WebsiteService struct {
|
||||
storage *storage.Storage
|
||||
}
|
||||
|
||||
// NewWebsiteService 创建网站服务
|
||||
func NewWebsiteService() *WebsiteService {
|
||||
return &WebsiteService{
|
||||
storage: storage.GetStorage(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWebsite 创建网站
|
||||
func (s *WebsiteService) CreateWebsite(req models.CreateWebsiteRequest) (*models.Website, error) {
|
||||
groups := req.Groups
|
||||
if len(groups) == 0 && req.Group != "" {
|
||||
groups = []string{req.Group}
|
||||
}
|
||||
|
||||
website := models.Website{
|
||||
ID: utils.GenerateID(),
|
||||
Name: req.Name,
|
||||
Groups: groups,
|
||||
URLs: make([]models.URLInfo, 0),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
for _, url := range req.URLs {
|
||||
urlInfo := models.URLInfo{
|
||||
ID: utils.GenerateShortID(),
|
||||
URL: url,
|
||||
}
|
||||
website.URLs = append(website.URLs, urlInfo)
|
||||
}
|
||||
|
||||
if len(req.URLs) > 0 {
|
||||
if utils.IsIntranetURL(req.URLs[0]) {
|
||||
website.Favicon = models.DefaultIntranetFavicon
|
||||
website.IPAddresses = nil
|
||||
} else {
|
||||
if ips, err := utils.ResolveDomainIPs(req.URLs[0]); err == nil {
|
||||
website.IPAddresses = ips
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.storage.AddWebsite(website); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 立即检测该网站
|
||||
go GetMonitorService().CheckWebsiteNow(website.ID)
|
||||
|
||||
return &website, nil
|
||||
}
|
||||
|
||||
// GetWebsite 获取网站
|
||||
func (s *WebsiteService) GetWebsite(id string) *models.Website {
|
||||
return s.storage.GetWebsite(id)
|
||||
}
|
||||
|
||||
// GetAllWebsites 获取所有网站
|
||||
func (s *WebsiteService) GetAllWebsites() []models.Website {
|
||||
return s.storage.GetWebsites()
|
||||
}
|
||||
|
||||
// UpdateWebsite 更新网站
|
||||
func (s *WebsiteService) UpdateWebsite(id string, req models.UpdateWebsiteRequest) (*models.Website, error) {
|
||||
website := s.storage.GetWebsite(id)
|
||||
if website == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
website.Name = req.Name
|
||||
}
|
||||
if len(req.Groups) > 0 {
|
||||
website.Groups = req.Groups
|
||||
} else if req.Group != "" {
|
||||
website.Groups = []string{req.Group}
|
||||
}
|
||||
if len(req.URLs) > 0 {
|
||||
// 保留已有URL的ID,添加新URL
|
||||
existingURLs := make(map[string]models.URLInfo)
|
||||
for _, u := range website.URLs {
|
||||
existingURLs[u.URL] = u
|
||||
}
|
||||
|
||||
newURLs := make([]models.URLInfo, 0)
|
||||
for _, url := range req.URLs {
|
||||
if existing, ok := existingURLs[url]; ok {
|
||||
newURLs = append(newURLs, existing)
|
||||
} else {
|
||||
newURLs = append(newURLs, models.URLInfo{
|
||||
ID: utils.GenerateShortID(),
|
||||
URL: url,
|
||||
})
|
||||
}
|
||||
}
|
||||
website.URLs = newURLs
|
||||
website.IPAddresses = nil // URL 变更后清空 IP,等下次检测重新解析
|
||||
|
||||
if len(req.URLs) > 0 && utils.IsIntranetURL(req.URLs[0]) {
|
||||
website.Favicon = models.DefaultIntranetFavicon
|
||||
website.IPAddresses = nil
|
||||
}
|
||||
}
|
||||
|
||||
website.UpdatedAt = time.Now()
|
||||
|
||||
if err := s.storage.UpdateWebsite(*website); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return website, nil
|
||||
}
|
||||
|
||||
// DeleteWebsite 删除网站
|
||||
func (s *WebsiteService) DeleteWebsite(id string) error {
|
||||
return s.storage.DeleteWebsite(id)
|
||||
}
|
||||
|
||||
// GetGroups 获取所有分组
|
||||
func (s *WebsiteService) GetGroups() []models.Group {
|
||||
return s.storage.GetGroups()
|
||||
}
|
||||
|
||||
// AddGroup 添加分组
|
||||
func (s *WebsiteService) AddGroup(name string) (*models.Group, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("分类名称不能为空")
|
||||
}
|
||||
|
||||
group := models.Group{
|
||||
ID: utils.GenerateShortID(),
|
||||
Name: name,
|
||||
}
|
||||
|
||||
if err := s.storage.AddGroup(group); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
// GetGroup 获取单个分组
|
||||
func (s *WebsiteService) GetGroup(id string) *models.Group {
|
||||
return s.storage.GetGroupByID(id)
|
||||
}
|
||||
|
||||
// UpdateGroup 更新分类名称
|
||||
func (s *WebsiteService) UpdateGroup(id string, name string) (*models.Group, error) {
|
||||
if err := s.storage.UpdateGroup(id, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g := s.storage.GetGroupByID(id)
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// DeleteGroup 删除分类
|
||||
func (s *WebsiteService) DeleteGroup(id string) error {
|
||||
return s.storage.DeleteGroup(id)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user