package handler import ( "fmt" "net/http" "strconv" "time" "sql-platform/internal/ghost" "sql-platform/internal/model" "github.com/gin-gonic/gin" ) // getSetting reads a system_setting value by key func getSetting(key, fallback string) string { var s model.SystemSetting if model.DB.Where("`key` = ?", key).Take(&s).Error == nil && s.Value != "" { return s.Value } return fallback } // isGhostEnabled checks if gh-ost is enabled in system settings func isGhostEnabled() bool { return getSetting("ghost_enabled", "true") == "true" } // getGhostPath returns the gh-ost binary path from settings func getGhostPath() string { return getSetting("ghost_path", "gh-ost") } // POST /api/v1/ghost/dry-run func GhOstDryRun(c *gin.Context) { if !isGhostEnabled() { c.JSON(http.StatusForbidden, gin.H{"detail": "gh-ost 功能已禁用,请在系统设置中开启"}) return } var req struct { InstanceID int `json:"instance_id"` Database string `json:"database"` Table string `json:"table"` Alter string `json:"alter"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } task, err := buildGhOstTask(req.InstanceID, req.Database, req.Table, req.Alter) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } task.DryRun = true task.Execute = false applyGhostSettings(task) result, err := ghost.RunGhOst(c.Request.Context(), *task, getGhostPath()) if err != nil { c.JSON(http.StatusInternalServerError, result) return } c.JSON(http.StatusOK, result) } // buildGhOstTask resolves instance_id to connection params func buildGhOstTask(instanceID int, database, table, alter string) (*ghost.GhOstTask, error) { var inst model.Instance if err := model.DB.First(&inst, instanceID).Error; err != nil { return nil, fmt.Errorf("instance %d not found", instanceID) } if inst.DBType != "mysql" { return nil, fmt.Errorf("gh-ost only supports MySQL instances, got %s", inst.DBType) } return &ghost.GhOstTask{ Host: inst.Host, Port: inst.Port, User: inst.Username, Password: inst.Password, Database: database, Table: table, Alter: alter, }, nil } // applyGhostSettings applies system_settings to the task (chunk-size, max-load, cloud flags) func applyGhostSettings(task *ghost.GhOstTask) { // Chunk size if cs, err := strconv.Atoi(getSetting("ghost_default_chunk_size", "1000")); err == nil && cs > 0 { task.ChunkSize = cs } // Max load if ml := getSetting("ghost_max_load", ""); ml != "" { task.MaxLoad = ml } // Allow on master task.AllowOnMaster = getSetting("ghost_allow_on_master", "true") == "true" } // POST /api/v1/ghost/execute func GhOstExecute(c *gin.Context) { if !isGhostEnabled() { c.JSON(http.StatusForbidden, gin.H{"detail": "gh-ost 功能已禁用,请在系统设置中开启"}) return } var req struct { InstanceID int `json:"instance_id"` Database string `json:"database"` Table string `json:"table"` Alter string `json:"alter"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } task, err := buildGhOstTask(req.InstanceID, req.Database, req.Table, req.Alter) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } task.DryRun = false task.Execute = true applyGhostSettings(task) taskID := ghost.RunGhOstAsync(*task, getGhostPath(), nil) c.JSON(http.StatusAccepted, gin.H{ "task_id": taskID, "message": "gh-ost execution started", }) } // GET /api/v1/ghost/status/:taskId func GhOstStatus(c *gin.Context) { taskID := c.Param("taskId") status, result := ghost.Manager.GetTaskStatus(taskID) if status == "" { c.JSON(http.StatusNotFound, gin.H{"detail": "Task not found"}) return } c.JSON(http.StatusOK, gin.H{ "task_id": taskID, "status": status, "result": result, }) } // GET /api/v1/ghost/tasks func GhOstListTasks(c *gin.Context) { tasks := ghost.Manager.ListTasks() c.JSON(http.StatusOK, tasks) } // GET /api/v1/audit-rules func ListAuditRules(c *gin.Context) { var rules []model.AuditRule model.DB.Order("id").Find(&rules) c.JSON(http.StatusOK, rules) } // GET /api/v1/announcements func ListAnnouncements(c *gin.Context) { var announcements []model.Announcement model.DB.Where("is_published = ?", true).Order("id DESC").Find(&announcements) c.JSON(http.StatusOK, announcements) } // GET /api/v1/query-histories func ListQueryHistories(c *gin.Context) { _, _, _ = c.GetInt("user_id"), c.GetString("username"), c.GetString("role") var histories []model.QueryHistory model.DB.Order("id DESC").Limit(100).Find(&histories) c.JSON(http.StatusOK, histories) } // GET /api/v1/settings — returns settings grouped by `group` field func ListSettings(c *gin.Context) { var settings []model.SystemSetting model.DB.Order("sort_order").Find(&settings) // group by `group` field grouped := make(map[string][]model.SystemSetting) for _, s := range settings { grouped[s.Group] = append(grouped[s.Group], s) } c.JSON(http.StatusOK, grouped) } // PUT /api/v1/settings — batch update settings func UpdateSettings(c *gin.Context) { var items []struct { ID int `json:"id"` Value string `json:"value"` } if err := c.ShouldBindJSON(&items); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } for _, item := range items { model.DB.Model(&model.SystemSetting{}).Where("id = ?", item.ID).Update("value", item.Value) } c.JSON(http.StatusOK, gin.H{"message": "Settings updated"}) } // GET /api/v1/settings/cleanup/preview func SettingsCleanupPreview(c *gin.Context) { c.JSON(http.StatusOK, map[string]interface{}{}) } // POST /api/v1/settings/cleanup/run func SettingsCleanupRun(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Cleanup done (placeholder)", "stats": map[string]interface{}{}}) } // GET /api/v1/notifications/channels func ListNotificationChannels(c *gin.Context) { var channels []model.NotificationChannel model.DB.Order("id").Find(&channels) c.JSON(http.StatusOK, channels) } // GET /api/v1/notifications/templates func ListNotificationTemplates(c *gin.Context) { var templates []model.NotificationTemplate model.DB.Order("id").Find(&templates) c.JSON(http.StatusOK, templates) } // GET /api/v1/workflows func ListWorkflows(c *gin.Context) { var workflows []model.WorkflowDefinition model.DB.Order("id").Find(&workflows) type WorkflowWithSteps struct { model.WorkflowDefinition Steps []model.WorkflowStep `json:"steps"` } result := make([]WorkflowWithSteps, len(workflows)) for i, wf := range workflows { result[i] = WorkflowWithSteps{WorkflowDefinition: wf, Steps: []model.WorkflowStep{}} model.DB.Where("workflow_id = ?", wf.ID).Order("step_number").Find(&result[i].Steps) } c.JSON(http.StatusOK, result) } // GET /api/v1/instance-groups func ListInstanceGroups(c *gin.Context) { var groups []model.InstanceGroup model.DB.Order("id").Find(&groups) result := make([]gin.H, len(groups)) for i, g := range groups { result[i] = enrichGroup(g) } c.JSON(http.StatusOK, result) } // POST /api/v1/notifications/test func TestNotification(c *gin.Context) { _ = time.Now() c.JSON(http.StatusOK, gin.H{"message": "Notification sent (placeholder)"}) }