package handler import ( "log" "net/http" "strconv" "strings" "time" "go-backend/internal/http/response" "go-backend/internal/monitoring" "go-backend/internal/store/model" ) const ( defaultMetricsRangeMs = int64(60 * 60 * 1000) // 1h maxMetricsRangeMs = int64(24 * 60 * 60 * 1000) // 24h ) func (h *Handler) resolveServiceMonitorLimits() monitoring.ServiceMonitorLimits { defaults := monitoring.DefaultServiceMonitorLimits() if h == nil || h.repo == nil { return defaults } cfg, err := h.repo.GetConfigsByNames([]string{ monitoring.ConfigServiceMonitorCheckerScanIntervalSec, monitoring.ConfigServiceMonitorWorkerLimit, monitoring.ConfigServiceMonitorMinIntervalSec, monitoring.ConfigServiceMonitorDefaultIntervalSec, monitoring.ConfigServiceMonitorMinTimeoutSec, monitoring.ConfigServiceMonitorDefaultTimeoutSec, monitoring.ConfigServiceMonitorMaxTimeoutSec, }) if err != nil { return defaults } return monitoring.ServiceMonitorLimitsFromConfigMap(cfg) } func (h *Handler) monitorNodeMetricsHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureMonitoringAccess(w, r) { return } path := r.URL.Path prefix := "/api/v1/monitor/nodes/" if !strings.HasPrefix(path, prefix) { response.WriteJSON(w, response.ErrDefault("无效的路径")) return } rest := strings.TrimPrefix(path, prefix) if strings.HasSuffix(rest, "/metrics/latest") { h.handleNodeMetricsLatest(w, r, strings.TrimSuffix(rest, "/metrics/latest")) return } if strings.HasSuffix(rest, "/metrics") { h.handleNodeMetrics(w, r, strings.TrimSuffix(rest, "/metrics")) return } response.WriteJSON(w, response.ErrDefault("无效的路径")) } type monitorNodeListItem struct { ID int64 `json:"id"` Inx int `json:"inx"` Name string `json:"name"` Status int `json:"status"` UpdatedTime int64 `json:"updatedTime"` } func (h *Handler) monitorNodeListHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureMonitoringAccess(w, r) { return } nodes, err := h.repo.ListMonitorNodes() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } items := make([]monitorNodeListItem, 0, len(nodes)) for _, n := range nodes { updated := int64(0) if n.UpdatedTime.Valid { updated = n.UpdatedTime.Int64 } items = append(items, monitorNodeListItem{ ID: n.ID, Inx: n.Inx, Name: n.Name, Status: n.Status, UpdatedTime: updated, }) } response.WriteJSON(w, response.OK(items)) } type monitorTunnelListItem struct { ID int64 `json:"id"` Inx int `json:"inx"` Name string `json:"name"` Status int `json:"status"` UpdatedTime int64 `json:"updatedTime"` } func (h *Handler) monitorTunnelListHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureMonitoringAccess(w, r) { return } tunnels, err := h.repo.ListMonitorTunnels() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } items := make([]monitorTunnelListItem, 0, len(tunnels)) for _, t := range tunnels { items = append(items, monitorTunnelListItem{ ID: t.ID, Inx: t.Inx, Name: t.Name, Status: t.Status, UpdatedTime: t.UpdatedTime, }) } response.WriteJSON(w, response.OK(items)) } func (h *Handler) handleNodeMetrics(w http.ResponseWriter, r *http.Request, nodeIDStr string) { nodeID, err := strconv.ParseInt(nodeIDStr, 10, 64) if err != nil || nodeID <= 0 { response.WriteJSON(w, response.ErrDefault("无效的节点ID")) return } now := time.Now().UnixMilli() startMs := now - defaultMetricsRangeMs endMs := now if s := r.URL.Query().Get("start"); s != "" { if v, err := strconv.ParseInt(s, 10, 64); err == nil { startMs = v } } if e := r.URL.Query().Get("end"); e != "" { if v, err := strconv.ParseInt(e, 10, 64); err == nil { endMs = v } } if startMs <= 0 || endMs <= 0 { response.WriteJSON(w, response.ErrDefault("无效的时间范围")) return } if endMs < startMs { response.WriteJSON(w, response.ErrDefault("无效的时间范围")) return } if endMs-startMs > maxMetricsRangeMs { response.WriteJSON(w, response.ErrDefault("时间范围过大")) return } metrics, err := h.repo.GetNodeMetrics(nodeID, startMs, endMs) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OK(metrics)) } func (h *Handler) handleNodeMetricsLatest(w http.ResponseWriter, _ *http.Request, nodeIDStr string) { nodeID, err := strconv.ParseInt(nodeIDStr, 10, 64) if err != nil || nodeID <= 0 { response.WriteJSON(w, response.ErrDefault("无效的节点ID")) return } metric, err := h.repo.GetLatestNodeMetric(nodeID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if metric == nil { response.WriteJSON(w, response.OK(nil)) return } response.WriteJSON(w, response.OK(metric)) } func (h *Handler) monitorTunnelMetrics(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureMonitoringAccess(w, r) { return } tunnelIDStr := extractPathParam(r.URL.Path, "/api/v1/monitor/tunnels/", "/metrics") tunnelID, err := strconv.ParseInt(tunnelIDStr, 10, 64) if err != nil || tunnelID <= 0 { response.WriteJSON(w, response.ErrDefault("无效的隧道ID")) return } now := time.Now().UnixMilli() startMs := now - defaultMetricsRangeMs endMs := now if s := r.URL.Query().Get("start"); s != "" { if v, err := strconv.ParseInt(s, 10, 64); err == nil { startMs = v } } if e := r.URL.Query().Get("end"); e != "" { if v, err := strconv.ParseInt(e, 10, 64); err == nil { endMs = v } } if startMs <= 0 || endMs <= 0 { response.WriteJSON(w, response.ErrDefault("无效的时间范围")) return } if endMs < startMs { response.WriteJSON(w, response.ErrDefault("无效的时间范围")) return } if endMs-startMs > maxMetricsRangeMs { response.WriteJSON(w, response.ErrDefault("时间范围过大")) return } metrics, err := h.repo.GetTunnelMetricsAggregated(tunnelID, startMs, endMs) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OK(metrics)) } func (h *Handler) monitorServiceListHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureMonitoringAccess(w, r) { return } monitors, err := h.repo.ListServiceMonitors() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OK(monitors)) } type createServiceMonitorRequest struct { Name string `json:"name"` Type string `json:"type"` Target string `json:"target"` IntervalSec int `json:"intervalSec"` TimeoutSec int `json:"timeoutSec"` NodeID int64 `json:"nodeId"` Enabled *int `json:"enabled"` } func (h *Handler) monitorServiceCreate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureMonitoringAccess(w, r) { return } var req createServiceMonitorRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("请求参数错误")) return } name := strings.TrimSpace(req.Name) if name == "" { response.WriteJSON(w, response.ErrDefault("名称不能为空")) return } monitorType := strings.ToLower(strings.TrimSpace(req.Type)) if monitorType != "tcp" && monitorType != "icmp" { response.WriteJSON(w, response.ErrDefault("类型必须是 tcp 或 icmp")) return } target := strings.TrimSpace(req.Target) if target == "" { response.WriteJSON(w, response.ErrDefault("目标地址不能为空")) return } limits := h.resolveServiceMonitorLimits() intervalSec := req.IntervalSec if intervalSec <= 0 { intervalSec = limits.DefaultIntervalSec } if intervalSec < limits.MinIntervalSec { intervalSec = limits.MinIntervalSec } timeoutSec := req.TimeoutSec if timeoutSec <= 0 { timeoutSec = limits.DefaultTimeoutSec } if timeoutSec < limits.MinTimeoutSec { timeoutSec = limits.MinTimeoutSec } if timeoutSec > limits.MaxTimeoutSec { timeoutSec = limits.MaxTimeoutSec } enabled := 1 if req.Enabled != nil { if *req.Enabled == 0 || *req.Enabled == 1 { enabled = *req.Enabled } } now := time.Now().UnixMilli() if req.NodeID > 0 { n, err := h.repo.GetNodeByID(req.NodeID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if n == nil { response.WriteJSON(w, response.ErrDefault("节点不存在")) return } } m := &model.ServiceMonitor{ Name: name, Type: monitorType, Target: target, IntervalSec: intervalSec, TimeoutSec: timeoutSec, NodeID: req.NodeID, Enabled: enabled, CreatedTime: now, UpdatedTime: now, } if m.Type == "icmp" && m.NodeID <= 0 { response.WriteJSON(w, response.ErrDefault("ICMP 监控必须选择执行节点")) return } // enabled is already normalized above. if err := h.repo.CreateServiceMonitor(m); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OK(m)) } type updateServiceMonitorRequest struct { ID int64 `json:"id"` Name string `json:"name"` Type string `json:"type"` Target string `json:"target"` IntervalSec int `json:"intervalSec"` TimeoutSec int `json:"timeoutSec"` NodeID *int64 `json:"nodeId"` Enabled *int `json:"enabled"` } func (h *Handler) monitorServiceUpdate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureMonitoringAccess(w, r) { return } var req updateServiceMonitorRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("请求参数错误")) return } if req.ID <= 0 { response.WriteJSON(w, response.ErrDefault("无效的监控ID")) return } existing, err := h.repo.GetServiceMonitor(req.ID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if existing == nil { response.WriteJSON(w, response.ErrDefault("监控不存在")) return } name := strings.TrimSpace(req.Name) if name != "" { existing.Name = name } monitorType := strings.ToLower(strings.TrimSpace(req.Type)) if monitorType == "tcp" || monitorType == "icmp" { existing.Type = monitorType } target := strings.TrimSpace(req.Target) if target != "" { existing.Target = target } limits := h.resolveServiceMonitorLimits() if req.IntervalSec > 0 { intervalSec := req.IntervalSec if intervalSec < limits.MinIntervalSec { intervalSec = limits.MinIntervalSec } existing.IntervalSec = intervalSec } if req.TimeoutSec > 0 { timeoutSec := req.TimeoutSec if timeoutSec < limits.MinTimeoutSec { timeoutSec = limits.MinTimeoutSec } if timeoutSec > limits.MaxTimeoutSec { timeoutSec = limits.MaxTimeoutSec } existing.TimeoutSec = timeoutSec } if req.NodeID != nil { existing.NodeID = *req.NodeID } if req.Enabled != nil { if *req.Enabled == 0 || *req.Enabled == 1 { existing.Enabled = *req.Enabled } } existing.UpdatedTime = time.Now().UnixMilli() if existing.Type == "icmp" && existing.NodeID <= 0 { response.WriteJSON(w, response.ErrDefault("ICMP 监控必须选择执行节点")) return } if existing.NodeID > 0 { n, err := h.repo.GetNodeByID(existing.NodeID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if n == nil { response.WriteJSON(w, response.ErrDefault("节点不存在")) return } } if err := h.repo.UpdateServiceMonitor(existing); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OK(existing)) } type deleteServiceMonitorRequest struct { ID int64 `json:"id"` } func (h *Handler) monitorServiceDelete(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureMonitoringAccess(w, r) { return } var req deleteServiceMonitorRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("请求参数错误")) return } if req.ID <= 0 { response.WriteJSON(w, response.ErrDefault("无效的监控ID")) return } if err := h.repo.DeleteServiceMonitor(req.ID); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OKEmpty()) } func (h *Handler) monitorServiceRun(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureMonitoringAccess(w, r) { return } if h.healthCheck == nil { response.WriteJSON(w, response.ErrDefault("监控服务不可用")) return } var req deleteServiceMonitorRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("请求参数错误")) return } if req.ID <= 0 { response.WriteJSON(w, response.ErrDefault("无效的监控ID")) return } m, err := h.repo.GetServiceMonitor(req.ID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if m == nil { response.WriteJSON(w, response.ErrDefault("监控不存在")) return } res, err := h.healthCheck.RunOnce(m) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if err := h.repo.InsertServiceMonitorResult(res); err != nil { log.Printf("monitoring write failed op=service_monitor_result.manual_insert monitor_id=%d err=%v", res.MonitorID, err) } response.WriteJSON(w, response.OK(res)) } func (h *Handler) monitorServiceResultsHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureMonitoringAccess(w, r) { return } monitorIDStr := extractPathParam(r.URL.Path, "/api/v1/monitor/services/", "/results") monitorID, err := strconv.ParseInt(monitorIDStr, 10, 64) if err != nil || monitorID <= 0 { response.WriteJSON(w, response.ErrDefault("无效的监控ID")) return } limit := 100 if l := r.URL.Query().Get("limit"); l != "" { if v, err := strconv.Atoi(l); err == nil && v > 0 && v <= 1000 { limit = v } } results, err := h.repo.GetServiceMonitorResults(monitorID, limit) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OK(results)) } func (h *Handler) monitorServiceLatestResultsHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureMonitoringAccess(w, r) { return } results, err := h.repo.GetLatestServiceMonitorResults() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OK(results)) } func (h *Handler) monitorServiceLimitsHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureMonitoringAccess(w, r) { return } response.WriteJSON(w, response.OK(h.resolveServiceMonitorLimits())) } func extractPathParam(path, prefix, suffix string) string { if !strings.HasPrefix(path, prefix) { return "" } rest := strings.TrimPrefix(path, prefix) if suffix != "" { rest = strings.TrimSuffix(rest, suffix) } return rest } type monitorAccessData struct { Allowed bool `json:"allowed"` Reason string `json:"reason,omitempty"` } // monitorAccessHandler is a lightweight capability check for frontend navigation. // It does NOT replace authorization on the actual monitoring endpoints. func (h *Handler) monitorAccessHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { response.WriteJSON(w, response.ErrDefault("请求失败")) return } userID, roleID, err := userRoleFromRequest(r) if err != nil { response.WriteJSON(w, response.Err(401, "未登录或token已过期")) return } if roleID == 0 { response.WriteJSON(w, response.OK(monitorAccessData{Allowed: true})) return } allowed, err := h.repo.HasMonitorPermission(userID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } data := monitorAccessData{Allowed: allowed} if !allowed { data.Reason = "need_admin_grant" } response.WriteJSON(w, response.OK(data)) } func (h *Handler) ensureAdminAccess(w http.ResponseWriter, r *http.Request) bool { _, roleID, err := userRoleFromRequest(r) if err != nil { response.WriteJSON(w, response.Err(401, "未登录或token已过期")) return false } if roleID != 0 { response.WriteJSON(w, response.Err(403, "权限不足,仅管理员可操作")) return false } return true } func (h *Handler) ensureMonitoringAccess(w http.ResponseWriter, r *http.Request) bool { userID, roleID, err := userRoleFromRequest(r) if err != nil { response.WriteJSON(w, response.Err(401, "未登录或token已过期")) return false } if roleID == 0 { return true } allowed, err := h.repo.HasMonitorPermission(userID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return false } if !allowed { response.WriteJSON(w, response.Err(403, "权限不足:当前账户非管理员,且未被授予监控权限。请联系管理员在用户管理中授权监控权限。")) return false } return true } func (h *Handler) monitorPermissionList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureAdminAccess(w, r) { return } items, err := h.repo.ListMonitorPermissions() if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OK(items)) } type monitorPermissionMutationRequest struct { UserID int64 `json:"userId"` } func (h *Handler) monitorPermissionAssign(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureAdminAccess(w, r) { return } var req monitorPermissionMutationRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("请求参数错误")) return } if req.UserID <= 0 { response.WriteJSON(w, response.ErrDefault("无效的用户ID")) return } u, err := h.repo.GetUserByID(req.UserID) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } if u == nil { response.WriteJSON(w, response.ErrDefault("用户不存在")) return } if err := h.repo.InsertMonitorPermission(req.UserID, time.Now().UnixMilli()); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OKEmpty()) } func (h *Handler) monitorPermissionRemove(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) return } if !h.ensureAdminAccess(w, r) { return } var req monitorPermissionMutationRequest if err := decodeJSON(r.Body, &req); err != nil { response.WriteJSON(w, response.ErrDefault("请求参数错误")) return } if req.UserID <= 0 { response.WriteJSON(w, response.ErrDefault("无效的用户ID")) return } if err := h.repo.DeleteMonitorPermission(req.UserID); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } response.WriteJSON(w, response.OKEmpty()) }