package controller import ( "context" "encoding/json" "net/http" "strconv" "time" "ezops/model" "ezops/pkg/crypto" ezredis "ezops/pkg/redis" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // ServerHandler 服务器处理器 type ServerHandler struct { db *gorm.DB cache ezredis.Cache crypt *crypto.SecretBox } // NewServerHandler 创建服务器处理器 func NewServerHandler(db *gorm.DB, masterKey string, cache ezredis.Cache) *ServerHandler { return &ServerHandler{db: db, crypt: crypto.NewSecretBox(masterKey), cache: cache} } // recordServerAudit 记录服务器操作审计日志 // Logan.Li func (h *ServerHandler) recordServerAudit(c *gin.Context, action, resource string, resourceID string, status int, errMsg string) { userID, _ := c.Get("userID") username, _ := c.Get("username") uid, _ := userID.(uint) uname, _ := username.(string) h.db.Create(&model.AuditLog{ UserID: &uid, Username: uname, IP: c.ClientIP(), UserAgent: c.Request.UserAgent(), Module: "server", Action: action, Resource: resource, ResourceID: resourceID, Status: status, Error: errMsg, }) } // ListServers 获取服务器列表 // @Summary 获取服务器列表 // @Description 获取所有服务器列表 // @Tags 服务器 // @Produce json // @Security Bearer // @Param page query int false "页码" // @Param page_size query int false "每页数量" // @Param keyword query string false "搜索关键词" // @Param status query string false "状态过滤" // @Param region_id query int false "区域ID" // @Success 200 {object} model.Response{data=model.ListResponse} // @Router /servers [get] func (h *ServerHandler) ListServers(c *gin.Context) { var query model.PageQuery if err := c.ShouldBindQuery(&query); err != nil { c.JSON(http.StatusBadRequest, model.Response{ Code: 400, Message: "请求参数错误", }) return } status := c.Query("status") regionID := c.Query("region_id") // 构建缓存键(按查询参数区分) // Logan.Li cacheKey := "servers:list:" + query.Keyword + ":" + status + ":" + regionID + ":" + query.Sort + ":" + query.Order + ":" + strconv.Itoa(query.Page) + ":" + strconv.Itoa(query.PageSize) const cacheTTL = 10 * time.Second // 尝试从 Redis 缓存读取 if h.cache != nil { if cached, err := h.cache.Get(c.Request.Context(), cacheKey); err == nil && cached != "" { var resp model.Response if json.Unmarshal([]byte(cached), &resp) == nil { c.JSON(http.StatusOK, resp) return } } } db := h.db.Model(&model.Server{}) // 关键词搜索 if query.Keyword != "" { db = db.Where("hostname LIKE ? OR ip LIKE ? OR public_ip LIKE ?", "%"+query.Keyword+"%", "%"+query.Keyword+"%", "%"+query.Keyword+"%") } // 状态过滤 if status := c.Query("status"); status != "" { db = db.Where("status = ?", status) } // 区域过滤 if regionID := c.Query("region_id"); regionID != "" { db = db.Where("region_id = ?", regionID) } var total int64 db.Count(&total) var servers []model.Server db.Preload("Region").Preload("Cloud"). Offset(query.GetOffset()).Limit(query.GetLimit()). Order(serverOrderClause(query.Sort, query.Order)). Find(&servers) // 填充 has_auth 字段(密码和SSH密钥在JSON中隐藏,但前端需知道是否有凭据) // Logan.Li for i := range servers { normalizeServerRemoteAccess(&servers[i]) servers[i].HasAuth = servers[i].Password != "" || servers[i].SSHKey != "" } resp := model.Response{ Code: 200, Message: "success", Data: model.ListResponse{ List: servers, Total: total, }, } // 写入 Redis 缓存(异步,缓存失败不影响响应) if h.cache != nil { if data, err := json.Marshal(resp); err == nil { _ = h.cache.Set(context.Background(), cacheKey, string(data), cacheTTL) } } c.JSON(http.StatusOK, resp) } // serverOrderClause 返回服务器列表允许使用的排序表达式,避免直接拼接请求参数。 // Logan.Li func serverOrderClause(sortField, order string) string { if sortField != "hostname" { return "created_at DESC" } if order == "desc" { return "LOWER(hostname) DESC, id DESC" } return "LOWER(hostname) ASC, id ASC" } // GetServer 获取服务器详情 // @Summary 获取服务器详情 // @Description 获取指定服务器的详细信息 // @Tags 服务器 // @Produce json // @Security Bearer // @Param id path int true "服务器ID" // @Success 200 {object} model.Response{data=model.Server} // @Router /servers/{id} [get] func (h *ServerHandler) GetServer(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var server model.Server if err := h.db.Preload("Region").Preload("Cloud").First(&server, id).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{ Code: 404, Message: "服务器不存在", }) return } normalizeServerRemoteAccess(&server) server.HasAuth = server.Password != "" || server.SSHKey != "" c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "success", Data: server, }) } // CreateServer 创建服务器 // @Summary 创建服务器 // @Description 创建新服务器 // @Tags 服务器 // @Accept json // @Produce json // @Security Bearer // @Param request body model.CreateServerRequest true "创建请求" // @Success 200 {object} model.Response{data=model.Server} // @Router /servers [post] func (h *ServerHandler) CreateServer(c *gin.Context) { var req model.CreateServerRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{ Code: 400, Message: "请求参数错误: " + err.Error(), }) return } server := model.Server{ Hostname: req.Hostname, IP: req.IP, PublicIP: req.PublicIP, Port: req.Port, Username: req.Username, Password: req.Password, SSHKey: req.SSHKey, AuthType: req.AuthType, OS: req.OS, CPU: req.CPU, Memory: req.Memory, Disk: req.Disk, RegionID: req.RegionID, CloudID: req.CloudID, Tags: req.Tags, Description: req.Description, Status: "stopped", } if req.Status != "" { server.Status = req.Status } if server.Port == 0 { server.Port = 22 } if server.Username == "" { server.Username = "root" } if server.AuthType == "" { server.AuthType = "password" } // 关联 SSH 凭证:解密后覆盖认证字段(同 AssignCredential,避免单台 CRUD 忽略 credential_id) // Logan.Li if req.CredentialID != nil && *req.CredentialID > 0 { var cred model.Credential if err := h.db.First(&cred, *req.CredentialID).Error; err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "凭据不存在"}) return } plain, err := h.crypt.Decrypt(cred.Content) if err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "凭据解密失败"}) return } applyCredential(cred, plain, &server) } if err := h.db.Create(&server).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{ Code: 500, Message: "创建服务器失败", }) return } h.recordServerAudit(c, "create", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "") c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "创建成功", Data: server, }) } // UpdateServer 更新服务器 // @Summary 更新服务器 // @Description 更新指定服务器信息 // @Tags 服务器 // @Accept json // @Produce json // @Security Bearer // @Param id path int true "服务器ID" // @Param request body model.UpdateServerRequest true "更新请求" // @Success 200 {object} model.Response{data=model.Server} // @Router /servers/{id} [put] func (h *ServerHandler) UpdateServer(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var req model.UpdateServerRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Response{ Code: 400, Message: "请求参数错误: " + err.Error(), }) return } var server model.Server if err := h.db.First(&server, id).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{ Code: 404, Message: "服务器不存在", }) return } updates := map[string]interface{}{} if req.Hostname != "" { updates["hostname"] = req.Hostname } if req.IP != "" { updates["ip"] = req.IP } if req.PublicIP != "" { updates["public_ip"] = req.PublicIP } if req.Port > 0 { updates["port"] = req.Port } if req.Username != "" { updates["username"] = req.Username } if req.Password != "" { updates["password"] = req.Password } if req.SSHKey != "" { updates["ssh_key"] = req.SSHKey } if req.AuthType != "" { updates["auth_type"] = req.AuthType } // 关联 SSH 凭证:解密后覆盖认证字段(applyCredential 填充已 load 的 server,再同步到 updates) // Logan.Li if req.CredentialID != nil && *req.CredentialID > 0 { var cred model.Credential if err := h.db.First(&cred, *req.CredentialID).Error; err != nil { c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "凭据不存在"}) return } plain, err := h.crypt.Decrypt(cred.Content) if err != nil { c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "凭据解密失败"}) return } applyCredential(cred, plain, &server) updates["ssh_key"] = server.SSHKey updates["password"] = server.Password updates["auth_type"] = server.AuthType updates["username"] = server.Username updates["port"] = server.Port } if req.OS != "" { updates["os"] = req.OS } if req.CPU > 0 { updates["cpu"] = req.CPU } if req.Memory > 0 { updates["memory"] = req.Memory } if req.Disk > 0 { updates["disk"] = req.Disk } if req.Status != "" { updates["status"] = req.Status } if req.RegionID != nil { updates["region_id"] = *req.RegionID } if req.CloudID != nil { updates["cloud_id"] = *req.CloudID } if req.Tags != "" { updates["tags"] = req.Tags } if req.Description != "" { updates["description"] = req.Description } if err := h.db.Model(&server).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{ Code: 500, Message: "更新失败", }) return } h.db.Preload("Region").Preload("Cloud").First(&server, id) h.recordServerAudit(c, "update", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "") c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "更新成功", Data: server, }) } // DeleteServer 删除服务器 // @Summary 删除服务器 // @Description 删除指定服务器 // @Tags 服务器 // @Produce json // @Security Bearer // @Param id path int true "服务器ID" // @Success 200 {object} model.Response // @Router /servers/{id} [delete] func (h *ServerHandler) DeleteServer(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var server model.Server if err := h.db.First(&server, id).Error; err != nil { c.JSON(http.StatusNotFound, model.Response{ Code: 404, Message: "服务器不存在", }) return } // 硬删除:先清理关联数据,再物理删除服务器记录 h.db.Unscoped().Where("hostname = ?", server.Hostname).Delete(&model.AgentReport{}) h.db.Unscoped().Where("hostname = ?", server.Hostname).Delete(&model.MetricsAggregate{}) if err := h.db.Unscoped().Delete(&model.Server{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, model.Response{ Code: 500, Message: "删除失败", }) return } h.recordServerAudit(c, "delete", server.Hostname+" ("+server.IP+")", strconv.Itoa(int(server.ID)), 1, "") c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "服务器已彻底删除", }) } // GetServerStats 获取服务器统计 // @Summary 获取服务器统计 // @Description 获取服务器统计数据 // @Tags 服务器 // @Produce json // @Security Bearer // @Success 200 {object} model.Response{data=model.ServerStats} // @Router /servers/stats [get] func (h *ServerHandler) GetServerStats(c *gin.Context) { var stats model.ServerStats h.db.Model(&model.Server{}).Count(&stats.Total) h.db.Model(&model.Server{}).Where("status = ?", "running").Count(&stats.Running) h.db.Model(&model.Server{}).Where("status = ?", "stopped").Count(&stats.Stopped) h.db.Model(&model.Server{}).Where("status = ?", "maintenance").Count(&stats.Maintenance) h.db.Model(&model.Server{}).Where("status = ?", "error").Count(&stats.Error) var avgCPU, avgMemory, avgDisk float64 h.db.Model(&model.Server{}).Select("AVG(cpu_usage)").Scan(&avgCPU) h.db.Model(&model.Server{}).Select("AVG(memory_usage)").Scan(&avgMemory) h.db.Model(&model.Server{}).Select("AVG(disk_usage)").Scan(&avgDisk) stats.AvgCPU = avgCPU stats.AvgMemory = avgMemory stats.AvgDisk = avgDisk c.JSON(http.StatusOK, model.Response{ Code: 200, Message: "success", Data: stats, }) }