diff --git a/backend/internal/api/handlers/zabbix_datasource.go b/backend/internal/api/handlers/zabbix_datasource.go
new file mode 100644
index 0000000..f22b4c2
--- /dev/null
+++ b/backend/internal/api/handlers/zabbix_datasource.go
@@ -0,0 +1,205 @@
+package handlers
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "ezops/internal/models"
+ "ezops/pkg/zabbix"
+
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+)
+
+// ZabbixDSHandler Zabbix 数据源管理 handler // Logan.Li
+type ZabbixDSHandler struct {
+ db *gorm.DB
+}
+
+// NewZabbixDSHandler 创建 Zabbix 数据源 handler
+func NewZabbixDSHandler(db *gorm.DB) *ZabbixDSHandler {
+ return &ZabbixDSHandler{db: db}
+}
+
+// ============================================
+// CRUD
+// ============================================
+
+// List 数据源列表
+// GET /api/v1/zabbix-datasources
+func (h *ZabbixDSHandler) List(c *gin.Context) {
+ var sources []models.ZabbixDataSource
+ if err := h.db.Order("is_default DESC, id ASC").Find(&sources).Error; err != nil {
+ c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败: " + err.Error()})
+ return
+ }
+ if sources == nil {
+ sources = []models.ZabbixDataSource{}
+ }
+ c.JSON(http.StatusOK, models.Response{Code: 200, Data: sources})
+}
+
+// Get 获取单个数据源
+// GET /api/v1/zabbix-datasources/:id
+func (h *ZabbixDSHandler) Get(c *gin.Context) {
+ id := c.Param("id")
+ var ds models.ZabbixDataSource
+ if err := h.db.First(&ds, id).Error; err != nil {
+ c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "数据源不存在"})
+ return
+ }
+ c.JSON(http.StatusOK, models.Response{Code: 200, Data: ds})
+}
+
+// Create 创建数据源
+// POST /api/v1/zabbix-datasources
+func (h *ZabbixDSHandler) Create(c *gin.Context) {
+ var req models.CreateZabbixDSReq
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
+ return
+ }
+
+ if req.IsDefault {
+ h.db.Model(&models.ZabbixDataSource{}).Where("is_default = ?", true).Update("is_default", false)
+ }
+
+ ds := models.ZabbixDataSource{
+ Name: req.Name,
+ URL: req.URL,
+ Username: req.Username,
+ Password: req.Password,
+ TrendsAfter: req.TrendsAfter,
+ TrendsRange: req.TrendsRange,
+ TrendsCacheTTL: req.TrendsCacheTTL,
+ TrendsTimeout: req.TrendsTimeout,
+ DirectDB: req.DirectDB,
+ DisableAcknowledges: req.DisableAcknowledges,
+ DisableDataAlignment: req.DisableDataAlignment,
+ IsDefault: req.IsDefault,
+ Status: 1,
+ }
+
+ if ds.TrendsCacheTTL <= 0 {
+ ds.TrendsCacheTTL = 3600
+ }
+ if ds.TrendsTimeout <= 0 {
+ ds.TrendsTimeout = 30
+ }
+
+ if err := h.db.Create(&ds).Error; err != nil {
+ c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败: " + err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, models.Response{Code: 200, Data: ds, Message: "创建成功"})
+}
+
+// Update 更新数据源
+// PUT /api/v1/zabbix-datasources/:id
+func (h *ZabbixDSHandler) Update(c *gin.Context) {
+ id := c.Param("id")
+ var ds models.ZabbixDataSource
+ if err := h.db.First(&ds, id).Error; err != nil {
+ c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "数据源不存在"})
+ return
+ }
+
+ var req models.UpdateZabbixDSReq
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
+ return
+ }
+
+ updates := map[string]interface{}{}
+ if req.Name != nil { updates["name"] = *req.Name }
+ if req.URL != nil { updates["url"] = *req.URL }
+ if req.Username != nil { updates["username"] = *req.Username }
+ if req.Password != nil && *req.Password != "" { updates["password"] = *req.Password }
+ if req.TrendsAfter != nil { updates["trends_after"] = *req.TrendsAfter }
+ if req.TrendsRange != nil { updates["trends_range"] = *req.TrendsRange }
+ if req.TrendsCacheTTL != nil { updates["trends_cache_ttl"] = *req.TrendsCacheTTL }
+ if req.TrendsTimeout != nil { updates["trends_timeout"] = *req.TrendsTimeout }
+ if req.DirectDB != nil { updates["direct_db"] = *req.DirectDB }
+ if req.DisableAcknowledges != nil { updates["disable_acknowledges"] = *req.DisableAcknowledges }
+ if req.DisableDataAlignment != nil { updates["disable_data_alignment"] = *req.DisableDataAlignment }
+ if req.IsDefault != nil {
+ updates["is_default"] = *req.IsDefault
+ if *req.IsDefault {
+ h.db.Model(&models.ZabbixDataSource{}).Where("is_default = ? AND id != ?", true, id).Update("is_default", false)
+ }
+ }
+ if req.Status != nil { updates["status"] = *req.Status }
+
+ if len(updates) > 0 {
+ if err := h.db.Model(&ds).Updates(updates).Error; err != nil {
+ c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败: " + err.Error()})
+ return
+ }
+ }
+
+ c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功"})
+}
+
+// Delete 删除数据源
+// DELETE /api/v1/zabbix-datasources/:id
+func (h *ZabbixDSHandler) Delete(c *gin.Context) {
+ id := c.Param("id")
+ if err := h.db.Delete(&models.ZabbixDataSource{}, id).Error; err != nil {
+ c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败: " + err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"})
+}
+
+// SetDefault 设为默认
+// POST /api/v1/zabbix-datasources/:id/default
+func (h *ZabbixDSHandler) SetDefault(c *gin.Context) {
+ id := c.Param("id")
+ h.db.Model(&models.ZabbixDataSource{}).Where("is_default = ?", true).Update("is_default", false)
+ if err := h.db.Model(&models.ZabbixDataSource{}).Where("id = ?", id).Update("is_default", true).Error; err != nil {
+ c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "设置失败: " + err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已设为默认"})
+}
+
+// TestConnection 测试 Zabbix 连接
+// POST /api/v1/zabbix-datasources/test
+func (h *ZabbixDSHandler) TestConnection(c *gin.Context) {
+ var req models.TestZabbixDSReq
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()})
+ return
+ }
+
+ client := zabbix.NewClient(req.URL, req.Username, req.Password,
+ zabbix.WithHTTPTimeout(10*time.Second),
+ )
+
+ ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
+ defer cancel()
+
+ if err := client.Auth(ctx); err != nil {
+ c.JSON(http.StatusOK, models.Response{
+ Code: 200,
+ Data: map[string]interface{}{
+ "success": false,
+ "message": "认证失败: " + err.Error(),
+ },
+ })
+ return
+ }
+
+ token := client.GetAuthToken()
+
+ c.JSON(http.StatusOK, models.Response{
+ Code: 200,
+ Data: map[string]interface{}{
+ "success": true,
+ "message": "连接成功,Zabbix 认证通过",
+ "session_id": token,
+ },
+ })
+}
diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go
index 4102144..80e1ba9 100644
--- a/backend/internal/api/routes/routes.go
+++ b/backend/internal/api/routes/routes.go
@@ -521,6 +521,19 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c
promDSGroup.POST("/test", promDSHandler.TestConnection)
}
+ // Zabbix 数据源管理 — CRUD + 连接测试 // Logan.Li
+ zabbixDSHandler := handlers.NewZabbixDSHandler(db)
+ zabbixDSGroup := authorized.Group("/zabbix-datasources")
+ {
+ zabbixDSGroup.GET("", zabbixDSHandler.List)
+ zabbixDSGroup.GET("/:id", zabbixDSHandler.Get)
+ zabbixDSGroup.POST("", zabbixDSHandler.Create)
+ zabbixDSGroup.PUT("/:id", zabbixDSHandler.Update)
+ zabbixDSGroup.DELETE("/:id", zabbixDSHandler.Delete)
+ zabbixDSGroup.POST("/:id/default", zabbixDSHandler.SetDefault)
+ zabbixDSGroup.POST("/test", zabbixDSHandler.TestConnection)
+ }
+
// 站点拨测 — HTTP/TCP/DNS/PING 主动监控 // Logan.Li
siteHandler := handlers.NewSiteHandler(db)
siteGroup := authorized.Group("/site")
diff --git a/backend/internal/models/zabbix_datasource.go b/backend/internal/models/zabbix_datasource.go
new file mode 100644
index 0000000..7929054
--- /dev/null
+++ b/backend/internal/models/zabbix_datasource.go
@@ -0,0 +1,82 @@
+package models
+
+import "time"
+
+// ZabbixDataSource Zabbix 数据源配置(参考 Grafana 界面设计) // Logan.Li
+type ZabbixDataSource struct {
+ ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"`
+ Name string `gorm:"type:varchar(128);not null;uniqueIndex;comment:数据源名称" json:"name"`
+ URL string `gorm:"type:varchar(512);not null;comment:Zabbix API地址" json:"url"`
+ Username string `gorm:"type:varchar(128);not null;comment:用户名" json:"username"`
+ Password string `gorm:"type:varchar(256);not null;comment:密码" json:"password,omitempty"`
+
+ // Trends 设置
+ TrendsAfter int `gorm:"default:0;comment:趋势数据起始时间(天前)" json:"trends_after"`
+ TrendsRange int `gorm:"default:0;comment:趋势数据范围(秒)" json:"trends_range"`
+ TrendsCacheTTL int `gorm:"default:3600;comment:趋势缓存TTL(秒)" json:"trends_cache_ttl"`
+ TrendsTimeout int `gorm:"default:30;comment:趋势查询超时(秒)" json:"trends_timeout"`
+
+ // Direct DB Connection
+ DirectDB bool `gorm:"default:false;comment:启用直连数据库" json:"direct_db"`
+
+ // Other
+ DisableAcknowledges bool `gorm:"default:false;comment:只读用户禁用确认" json:"disable_acknowledges"`
+ DisableDataAlignment bool `gorm:"default:false;comment:禁用数据对齐" json:"disable_data_alignment"`
+
+ // 状态
+ IsDefault bool `gorm:"default:false;index;comment:是否默认数据源" json:"is_default"`
+ Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"`
+
+ CreatedAt time.Time `gorm:"not null;index;comment:创建时间" json:"created_at"`
+ UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"`
+ DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"`
+}
+
+// TableName 指定表名
+func (ZabbixDataSource) TableName() string {
+ return "zabbix_datasources"
+}
+
+// ============================================
+// 请求/响应 struct
+// ============================================
+
+// CreateZabbixDSReq 创建 Zabbix 数据源请求
+type CreateZabbixDSReq struct {
+ Name string `json:"name" binding:"required"`
+ URL string `json:"url" binding:"required"`
+ Username string `json:"username" binding:"required"`
+ Password string `json:"password" binding:"required"`
+ TrendsAfter int `json:"trends_after"`
+ TrendsRange int `json:"trends_range"`
+ TrendsCacheTTL int `json:"trends_cache_ttl"`
+ TrendsTimeout int `json:"trends_timeout"`
+ DirectDB bool `json:"direct_db"`
+ DisableAcknowledges bool `json:"disable_acknowledges"`
+ DisableDataAlignment bool `json:"disable_data_alignment"`
+ IsDefault bool `json:"is_default"`
+}
+
+// UpdateZabbixDSReq 更新 Zabbix 数据源请求
+type UpdateZabbixDSReq struct {
+ Name *string `json:"name"`
+ URL *string `json:"url"`
+ Username *string `json:"username"`
+ Password *string `json:"password"`
+ TrendsAfter *int `json:"trends_after"`
+ TrendsRange *int `json:"trends_range"`
+ TrendsCacheTTL *int `json:"trends_cache_ttl"`
+ TrendsTimeout *int `json:"trends_timeout"`
+ DirectDB *bool `json:"direct_db"`
+ DisableAcknowledges *bool `json:"disable_acknowledges"`
+ DisableDataAlignment *bool `json:"disable_data_alignment"`
+ IsDefault *bool `json:"is_default"`
+ Status *int `json:"status"`
+}
+
+// TestZabbixDSReq 测试 Zabbix 连接请求
+type TestZabbixDSReq struct {
+ URL string `json:"url" binding:"required"`
+ Username string `json:"username" binding:"required"`
+ Password string `json:"password" binding:"required"`
+}
diff --git a/backend/pkg/database/database.go b/backend/pkg/database/database.go
index 71edb8e..b597704 100644
--- a/backend/pkg/database/database.go
+++ b/backend/pkg/database/database.go
@@ -131,6 +131,7 @@ func AutoMigrate(db *gorm.DB) error {
&models.AlertSilence{}, // 告警静默 // Logan.Li
&models.AlertEventV2{}, // 告警事件V2 // Logan.Li
&models.PrometheusDataSource{}, // Prometheus 数据源配置 // Logan.Li
+ &models.ZabbixDataSource{}, // Zabbix 数据源配置 // Logan.Li
}
var lastErr error
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index aea4323..c4e5215 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -33,7 +33,7 @@ import TraceManager from '@/components/monitor/trace/TraceManager';
import HostMonitorDashboard from '@/components/monitor/dashboard/HostMonitorDashboard';
import { MonitorCenter } from '@/pages/MonitorCenter';
import { MetricsExplorer } from '@/pages/MetricsExplorer';
-import PrometheusDatasourceManager from '@/components/monitor/datasource/PrometheusDatasourceManager';
+import DatasourcePage from '@/components/monitor/datasource/DatasourcePage';
const queryClient = new QueryClient({
defaultOptions: {
@@ -75,7 +75,7 @@ function App() {
配置 Zabbix API 连接,获取主机监控数据
+暂无 Zabbix 数据源
+ +{ds.url}
+ 用户: {ds.username}
+ {ds.direct_db &&