Files
opsxsql/backend/internal/handler/instance.go
T
logan 31db229d70 feat: MySQL/PG 实时指标采集 + 版本检测修复 + 密码加密存储
- 新增 getMySQLMetrics(): SHOW GLOBAL STATUS 采集连接数/QPS/延迟/运行时间
- 新增 getPostgresMetrics(): pg_stat_activity 采集活跃连接+运行时间
- 修复 extractVersion(): 支持 Redis 'version: 7.4.9, mode: standalone' 格式(逗号截断)
- CreateInstance/UpdateInstance: 创建/更新时自动加密密码(enc: 前缀)
- InstanceMetrics handler: MySQL/PG 分别调用专属 metrics 函数
2026-05-31 00:15:18 +08:00

221 lines
5.9 KiB
Go

package handler
import (
"net/http"
"strconv"
"sql-platform/internal/middleware"
"sql-platform/internal/model"
"github.com/gin-gonic/gin"
)
// GET /api/v1/instances
func ListInstances(c *gin.Context) {
userID, _, role := middleware.GetCurrentUser(c)
var instances []model.Instance
query := model.DB.Order("id")
// developer only sees instances in their groups
if role == "developer" {
var groupIDs []int
model.DB.Model(&model.UserGroup{}).Where("user_id = ?", userID).Pluck("group_id", &groupIDs)
if len(groupIDs) > 0 {
var instanceIDs []int
model.DB.Model(&model.InstanceGroupMember{}).Where("group_id IN ?", groupIDs).Pluck("instance_id", &instanceIDs)
if len(instanceIDs) > 0 {
query = query.Where("id IN ?", instanceIDs)
} else {
c.JSON(http.StatusOK, []model.Instance{})
return
}
}
}
if err := query.Find(&instances).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
return
}
// mask passwords in response
for i := range instances {
instances[i].Password = ""
}
c.JSON(http.StatusOK, instances)
}
// GET /api/v1/instances/:id
func GetInstance(c *gin.Context) {
id := c.Param("id")
var instance model.Instance
if err := model.DB.First(&instance, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"})
return
}
instance.Password = "******"
c.JSON(http.StatusOK, instance)
}
// POST /api/v1/instances
func CreateInstance(c *gin.Context) {
var instance model.Instance
if err := c.ShouldBindJSON(&instance); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
// Encrypt password before storing
if instance.Password != "" {
encPw, err := encryptPassword(instance.Password)
if err == nil {
instance.Password = "enc:" + encPw
}
}
if err := model.DB.Create(&instance).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
return
}
// Auto-detect version and cluster_mode after creation
go func() {
result := testDBConnectionFull(&instance)
updates := map[string]interface{}{}
if result.Success && result.Version != "" {
updates["version"] = result.Version
}
if result.ClusterMode {
updates["cluster_mode"] = true
}
if len(updates) > 0 {
model.DB.Model(&model.Instance{}).Where("id = ?", instance.ID).Updates(updates)
}
}()
instance.Password = "******"
c.JSON(http.StatusCreated, instance)
}
// PUT /api/v1/instances/:id
func UpdateInstance(c *gin.Context) {
id := c.Param("id")
var instance model.Instance
if err := model.DB.First(&instance, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"})
return
}
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
// if password is masked, don't update it
if pw, ok := input["password"].(string); ok && pw == "******" {
delete(input, "password")
}
// Encrypt password if provided and not masked
if pw, ok := input["password"].(string); ok && pw != "" {
encPw, err := encryptPassword(pw)
if err == nil {
input["password"] = "enc:" + encPw
}
}
if err := model.DB.Model(&instance).Updates(input).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
return
}
model.DB.First(&instance, id)
instance.Password = "******"
c.JSON(http.StatusOK, instance)
}
// DELETE /api/v1/instances/:id
func DeleteInstance(c *gin.Context) {
id := c.Param("id")
if err := model.DB.Delete(&model.Instance{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Instance deleted"})
}
// POST /api/v1/instances/:id/test-connection
func TestConnection(c *gin.Context) {
id := c.Param("id")
var instance model.Instance
if err := model.DB.First(&instance, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"})
return
}
result := testDBConnectionFull(&instance)
// Update version & cluster_mode in DB if detected
updates := map[string]interface{}{}
if result.Success && result.Version != "" {
updates["version"] = result.Version
}
if result.ClusterMode {
updates["cluster_mode"] = true
}
if len(updates) > 0 {
model.DB.Model(&model.Instance{}).Where("id = ?", instance.ID).Updates(updates)
}
if result.Success {
c.JSON(http.StatusOK, gin.H{"success": true, "message": result.Message, "version": result.Version, "cluster_mode": result.ClusterMode})
} else {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": result.Message})
}
}
// GET /api/v1/instances/stats
func InstanceStats(c *gin.Context) {
var total int64
model.DB.Model(&model.Instance{}).Count(&total)
type typeCount struct {
DBType string `json:"db_type"`
Count int64 `json:"count"`
}
var byType []typeCount
model.DB.Model(&model.Instance{}).Select("db_type, count(*) as count").Group("db_type").Scan(&byType)
c.JSON(http.StatusOK, gin.H{
"total": total,
"by_type": byType,
})
}
// Helper: parse id param to int
func parseID(s string) (int, bool) {
id, err := strconv.Atoi(s)
return id, err == nil
}
// GET /api/v1/instances/:id/metrics — live metrics for an instance
func InstanceMetrics(c *gin.Context) {
id := c.Param("id")
var instance model.Instance
if err := model.DB.First(&instance, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"})
return
}
switch instance.DBType {
case "redis":
metrics := getRedisMetrics(&instance)
c.JSON(http.StatusOK, metrics)
case "mysql":
metrics := getMySQLMetrics(&instance)
c.JSON(http.StatusOK, metrics)
case "postgresql", "postgres", "pg":
metrics := getPostgresMetrics(&instance)
c.JSON(http.StatusOK, metrics)
default:
c.JSON(http.StatusOK, gin.H{
"status": "online",
"version": instance.Version,
})
}
}