feat: gh-ost真实集成验证通过 + 登录页全面中文化 + 外观设置跨浏览器同步修复
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)
This commit is contained in:
1 parent
62a2b383e1
commit
8aaf2271b8
8 files changed
+135
-61
No files matched your search
@@ -13,6 +13,7 @@ import (
|
||||
// GhOstTask defines a gh-ost execution task
|
||||
type GhOstTask struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port,omitempty"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Database string `json:"database"`
|
||||
@@ -60,12 +61,18 @@ func RunGhOst(ctx context.Context, task GhOstTask, ghOstPath string) (*GhOstResu
|
||||
|
||||
args := []string{
|
||||
"--host=" + task.Host,
|
||||
fmt.Sprintf("--port=%d", task.Port),
|
||||
"--user=" + task.User,
|
||||
"--password=" + task.Password,
|
||||
"--database=" + task.Database,
|
||||
"--table=" + task.Table,
|
||||
"--alter=" + task.Alter,
|
||||
"--verbose",
|
||||
"--skip-port-validation",
|
||||
"--allow-on-master",
|
||||
"--initially-drop-ghost-table",
|
||||
"--initially-drop-old-table",
|
||||
"--initially-drop-socket-file",
|
||||
}
|
||||
|
||||
if task.ChunkSize > 0 {
|
||||
@@ -91,11 +98,11 @@ func RunGhOst(ctx context.Context, task GhOstTask, ghOstPath string) (*GhOstResu
|
||||
}
|
||||
|
||||
// Dry-Run vs Execute
|
||||
if task.DryRun || !task.Execute {
|
||||
args = append(args, "--dry-run")
|
||||
} else {
|
||||
// gh-ost defaults to noop (dry-run), only --execute actually applies changes
|
||||
if task.Execute {
|
||||
args = append(args, "--execute")
|
||||
}
|
||||
// always verbose for output
|
||||
|
||||
cmd := exec.CommandContext(ctx, ghOstPath, args...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
@@ -193,12 +200,18 @@ func (m *TaskManager) ListTasks() map[string]string {
|
||||
func buildArgs(task GhOstTask, ghOstPath string) []string {
|
||||
args := []string{
|
||||
"--host=" + task.Host,
|
||||
fmt.Sprintf("--port=%d", task.Port),
|
||||
"--user=" + task.User,
|
||||
"--password=" + task.Password,
|
||||
"--database=" + task.Database,
|
||||
"--table=" + task.Table,
|
||||
"--alter=" + task.Alter,
|
||||
"--verbose",
|
||||
"--skip-port-validation",
|
||||
"--allow-on-master",
|
||||
"--initially-drop-ghost-table",
|
||||
"--initially-drop-old-table",
|
||||
"--initially-drop-socket-file",
|
||||
}
|
||||
|
||||
if task.ChunkSize > 0 {
|
||||
@@ -223,9 +236,8 @@ func buildArgs(task GhOstTask, ghOstPath string) []string {
|
||||
args = append(args, "--postpone-cut-over-flag-file=/tmp/gh-ost-cutover.flag")
|
||||
}
|
||||
|
||||
if task.DryRun || !task.Execute {
|
||||
args = append(args, "--dry-run")
|
||||
} else {
|
||||
// gh-ost defaults to noop, only --execute actually applies changes
|
||||
if task.Execute {
|
||||
args = append(args, "--execute")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -13,15 +14,27 @@ import (
|
||||
|
||||
// POST /api/v1/ghost/dry-run
|
||||
func GhOstDryRun(c *gin.Context) {
|
||||
var task ghost.GhOstTask
|
||||
if err := c.ShouldBindJSON(&task); err != nil {
|
||||
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)
|
||||
result, err := ghost.RunGhOst(c.Request.Context(), *task, config.C.GhOstPath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, result)
|
||||
return
|
||||
@@ -29,17 +42,51 @@ func GhOstDryRun(c *gin.Context) {
|
||||
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 task ghost.GhOstTask
|
||||
if err := c.ShouldBindJSON(&task); err != nil {
|
||||
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)
|
||||
taskID := ghost.RunGhOstAsync(*task, config.C.GhOstPath, nil)
|
||||
c.JSON(http.StatusAccepted, gin.H{
|
||||
"task_id": taskID,
|
||||
"message": "gh-ost execution started",
|
||||
|
||||
@@ -43,26 +43,41 @@ func UpdateUserPreferences(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": result.Error.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Sync login_style and theme to SystemSetting so public endpoint picks them up
|
||||
syncToSystemSetting := func(key, value string) {
|
||||
var s model.SystemSetting
|
||||
if model.DB.Where("`key` = ?", key).First(&s).Error == nil {
|
||||
model.DB.Model(&s).Update("value", value)
|
||||
} else {
|
||||
model.DB.Create(&model.SystemSetting{
|
||||
Key: key, Value: value, ValueType: "string",
|
||||
Group: "appearance", Label: key, SortOrder: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
if v, ok := prefs["login_style"]; ok {
|
||||
syncToSystemSetting("login_style", v)
|
||||
}
|
||||
if v, ok := prefs["theme"]; ok {
|
||||
syncToSystemSetting("default_theme", v)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Preferences updated", "preferences": prefs})
|
||||
}
|
||||
|
||||
// GET /api/v1/public/login-style (no auth required)
|
||||
func GetPublicLoginStyle(c *gin.Context) {
|
||||
// Check system settings first
|
||||
var setting model.SystemSetting
|
||||
style := "split" // default
|
||||
if model.DB.Where("`key` = ?", "login_style").First(&setting).Error == nil {
|
||||
if setting.Value != "" {
|
||||
style = setting.Value
|
||||
}
|
||||
}
|
||||
|
||||
// Also return theme default
|
||||
style := "split"
|
||||
theme := "dark"
|
||||
if model.DB.Where("`key` = ?", "default_theme").First(&setting).Error == nil {
|
||||
if setting.Value != "" {
|
||||
theme = setting.Value
|
||||
}
|
||||
|
||||
var s model.SystemSetting
|
||||
if model.DB.Where("`key` = ?", "login_style").Take(&s).Error == nil && s.Value != "" {
|
||||
style = s.Value
|
||||
}
|
||||
s = model.SystemSetting{} // reset to avoid GORM reusing primary key
|
||||
if model.DB.Where("`key` = ?", "default_theme").Take(&s).Error == nil && s.Value != "" {
|
||||
theme = s.Value
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"login_style": style, "theme": theme})
|
||||
|
||||
Reference in new issue
Block a user