package controller import ( "net/http" "strconv" "strings" "time" "ezops/model" "ezops/pkg/siteprobe" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // SiteHandler 站点拨测管理 handler // Logan.Li type SiteHandler struct { db *gorm.DB worker *siteprobe.Worker } // NewSiteHandler 创建站点拨测 handler func NewSiteHandler(db *gorm.DB, worker *siteprobe.Worker) *SiteHandler { return &SiteHandler{db: db, worker: worker} } // ListTasks 列出拨测任务 // GET /api/v1/site/tasks func (h *SiteHandler) ListTasks(c *gin.Context) { var tasks []model.SiteTask q := h.db.Model(&model.SiteTask{}) if t := c.Query("type"); t != "" { q = q.Where("task_type = ?", strings.ToLower(t)) } if kw := c.Query("keyword"); kw != "" { q = q.Where("name LIKE ? OR url LIKE ?", "%"+kw+"%", "%"+kw+"%") } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "100")) var total int64 q.Count(&total) q.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&tasks) views := make([]SiteTaskView, 0, len(tasks)) for _, task := range tasks { if task.TaskType == "" { inferred := model.SiteTaskType(siteprobe.InferTaskTypePublic(task)) h.db.Model(&task).Update("task_type", inferred) task.TaskType = inferred } views = append(views, toSiteTaskView(task)) } enrichSiteTaskViewsSSL(h.db, views) enrichSiteTaskViewsAlert(h.db, views) c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": views, "total": total}}) } // CreateTask 创建拨测任务 // POST /api/v1/site/tasks func (h *SiteHandler) CreateTask(c *gin.Context) { var raw map[string]interface{} if err := c.ShouldBindJSON(&raw); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()}) return } task := parseCreateTaskRequest(raw) if task.Name == "" || task.URL == "" { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "名称和地址不能为空"}) return } task.Frequency = siteprobe.NormalizeProbeFrequency(string(task.TaskType), task.Frequency) if task.Timeout <= 0 { task.Timeout = 10 } if err := h.db.Create(&task).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()}) return } if h.worker != nil { go h.worker.RunTaskNow(task.ID) } c.JSON(http.StatusOK, model.Response{Code: 200, Data: toSiteTaskView(task), Message: "创建成功"}) } // UpdateTask 更新拨测任务(启用/停用或配置变更) // PUT /api/v1/site/tasks/:id func (h *SiteHandler) UpdateTask(c *gin.Context) { id := c.Param("id") var task model.SiteTask if err := h.db.First(&task, id).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "任务不存在"}) return } var raw map[string]interface{} if err := c.ShouldBindJSON(&raw); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()}) return } updates := map[string]interface{}{} if v, ok := raw["name"].(string); ok && strings.TrimSpace(v) != "" { updates["name"] = strings.TrimSpace(v) } if v, ok := raw["url"].(string); ok && strings.TrimSpace(v) != "" { updates["url"] = strings.TrimSpace(v) } if v, ok := raw["type"].(string); ok && v != "" { t := strings.ToLower(v) if t == "https" { t = "http" } updates["task_type"] = t } if v, ok := raw["frequency_sec"].(float64); ok { taskType := string(task.TaskType) if tv, ok := updates["task_type"].(string); ok { taskType = tv } updates["frequency"] = siteprobe.NormalizeProbeFrequency(taskType, int(v)) } if v, ok := raw["timeout_sec"].(float64); ok { updates["timeout"] = int(v) } if v, ok := raw["expected_status_code"].(float64); ok { updates["expected_status"] = int(v) } if v, ok := raw["enabled"].(bool); ok { if v { updates["status"] = model.SiteTaskStatusRunning } else { updates["status"] = model.SiteTaskStatusPaused } } if len(updates) > 0 { h.db.Model(&task).Updates(updates) h.db.First(&task, id) } c.JSON(http.StatusOK, model.Response{Code: 200, Data: toSiteTaskView(task), Message: "更新成功"}) } // deleteSiteTaskCascade 先删子表再删任务,避免外键约束 // Logan.Li func (h *SiteHandler) deleteSiteTaskCascade(taskID uint) error { return h.db.Transaction(func(tx *gorm.DB) error { if err := tx.Where("task_id = ?", taskID).Delete(&model.SiteResult{}).Error; err != nil { return err } if err := tx.Where("task_id = ?", taskID).Delete(&model.SiteAlert{}).Error; err != nil { return err } tx.Where("category = ? AND target_id = ?", model.MonitorAlertSite, taskID).Delete(&model.MonitorAlertRule{}) return tx.Delete(&model.SiteTask{}, taskID).Error }) } // DeleteTask 删除拨测任务 func (h *SiteHandler) DeleteTask(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效 ID"}) return } if err := h.deleteSiteTaskCascade(uint(id)); err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()}) return } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"}) } // BatchDeleteTasks 批量删除 func (h *SiteHandler) BatchDeleteTasks(c *gin.Context) { var req model.IDsRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()}) return } for _, tid := range req.IDs { if err := h.deleteSiteTaskCascade(uint(tid)); err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: err.Error()}) return } } c.JSON(http.StatusOK, model.Response{Code: 200, Message: "批量删除成功"}) } // ListResults 查询拨测结果 // GET /api/v1/site/results?task_id=N&limit=100 func (h *SiteHandler) ListResults(c *gin.Context) { var results []model.SiteResult q := h.db.Model(&model.SiteResult{}) if tid := c.Query("task_id"); tid != "" { q = q.Where("task_id = ?", tid) } if h := c.Query("hours"); h != "" { if hours, err := strconv.Atoi(h); err == nil && hours > 0 { if hours > 168 { hours = 168 } since := time.Now().Add(-time.Duration(hours) * time.Hour) q = q.Where("checked_at >= ?", since) } } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "500")) if limit <= 0 { limit = 500 } if limit > 2000 { limit = 2000 } q.Order("checked_at DESC").Limit(limit).Find(&results) if results == nil { results = []model.SiteResult{} } c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{"list": results, "total": len(results)}}) } // ProbeNow 立即触发拨测 // POST /api/v1/site/tasks/:id/probe func (h *SiteHandler) ProbeNow(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效 ID"}) return } if h.worker == nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "拨测引擎未启动"}) return } if err := h.worker.RunTaskNow(uint(id)); err != nil { c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "任务不存在"}) return } var task model.SiteTask h.db.First(&task, id) view := toSiteTaskView(task) enrichSiteTaskViewsSSL(h.db, []SiteTaskView{view}) enrichSiteTaskViewsAlert(h.db, []SiteTaskView{view}) c.JSON(http.StatusOK, model.Response{Code: 200, Data: view, Message: "拨测完成"}) } // GetSiteStats 获取拨测概览统计 // GET /api/v1/site/stats func (h *SiteHandler) GetSiteStats(c *gin.Context) { var total, enabled, upCount, downCount int64 h.db.Model(&model.SiteTask{}).Count(&total) h.db.Model(&model.SiteTask{}).Where("status = ?", model.SiteTaskStatusRunning).Count(&enabled) h.db.Model(&model.SiteTask{}).Where("last_result = ?", "success").Count(&upCount) h.db.Model(&model.SiteTask{}).Where("last_result IN ?", []string{"fail", "timeout"}).Count(&downCount) var availability float64 since := time.Now().Add(-24 * time.Hour) var successRecent, totalRecent int64 h.db.Model(&model.SiteResult{}).Where("checked_at >= ? AND status = ?", since, model.SiteResultSuccess).Count(&successRecent) h.db.Model(&model.SiteResult{}).Where("checked_at >= ?", since).Count(&totalRecent) if totalRecent > 0 { availability = float64(successRecent) / float64(totalRecent) } c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{ "total_tasks": total, "enabled_tasks": enabled, "up_count": upCount, "down_count": downCount, "availability": availability, "probe_count": totalRecent, }}) }