gh-ost集成: - handler根据instance_id查DB获取连接信息(host/port/user/password) - 修复gh-ost参数: 去掉不存在的--dry-run(默认noop), 加--execute才真执行 - 加--skip-port-validation(端口映射环境必须) - 加--allow-on-master(单机MySQL直接执行) - 加--initially-drop-ghost-table/old-table/socket-file(清理残留) - GhOstTask增加Port字段, buildArgs同步更新 - dry-run真实验证通过: 连接1.94.55.133:9005, 3行表, 3.85s完成 登录页中文化: - 5个登录组件(SplitView/CenteredCard/SidePanel/FullImage/MinimalDark) - 46处英文文案全部替换为中文 - 平台标题统一为'SQL审核平台' 外观设置同步修复: - GetPublicLoginStyle: GORM First()复用主键导致第二次查询失败 - 改用Take()+重置结构体, 修复theme始终返回dark的bug - UpdateUserPreferences: 同步写入SystemSetting表(login_style+default_theme)
227 lines
6.0 KiB
Go
227 lines
6.0 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"sql-platform/config"
|
|
"sql-platform/internal/ghost"
|
|
"sql-platform/internal/model"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// POST /api/v1/ghost/dry-run
|
|
func GhOstDryRun(c *gin.Context) {
|
|
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
|
|
}
|
|
|
|
// Look up instance connection info
|
|
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
|
|
|
|
result, err := ghost.RunGhOst(c.Request.Context(), *task, config.C.GhOstPath)
|
|
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)
|
|
}
|
|
// gh-ost needs separate host and port
|
|
host := inst.Host
|
|
port := inst.Port
|
|
return &ghost.GhOstTask{
|
|
Host: host,
|
|
Port: port,
|
|
User: inst.Username,
|
|
Password: inst.Password,
|
|
Database: database,
|
|
Table: table,
|
|
Alter: alter,
|
|
}, nil
|
|
}
|
|
|
|
// POST /api/v1/ghost/execute
|
|
func GhOstExecute(c *gin.Context) {
|
|
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
|
|
|
|
taskID := ghost.RunGhOstAsync(*task, config.C.GhOstPath, 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)"})
|
|
}
|