feat: Zabbix 数据源管理 — 完整 CRUD + 双 Tab 数据源页面

后端:
- models/zabbix_datasource.go: Zabbix 数据源 model (15字段, 参照 Grafana)
- handlers/zabbix_datasource.go: CRUD + 测试连接 (调用 Zabbix API Auth)
- routes.go: /zabbix-datasources 路由组
- database.go: AutoMigrate

前端:
- api.ts: zabbixDatasourceApi
- ZabbixDatasourceManager.tsx: 表单 (URL/用户名/密码/Trends设置/DB直连)
- DatasourcePage.tsx: Prometheus + Zabbix 双 Tab 包装
- App.tsx: /datasources 路由更新
This commit is contained in:
attacker committed 2026-06-18 14:14:40 +00:00
1 parent d2201fc8c0
commit c51d0b8b8b
8 files changed
+761 -2

No files matched your search

@@ -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,
},
})
}
+13
View File
@@ -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")
@@ -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"`
}
+1
View File
@@ -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