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:
logan committed 2026-05-27 00:06:13 +08:00
1 parent 62a2b383e1
commit 8aaf2271b8
8 files changed
+135 -61

No files matched your search

+18 -6
View File
@@ -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")
}
+53 -6
View File
@@ -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",
+29 -14
View File
@@ -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})
@@ -23,9 +23,9 @@ export default function CenteredCard() {
<div style={{ position: 'relative', zIndex: 10, width: '100%', maxWidth: 400, background: 'var(--dg-bg-primary)', borderRadius: 14, padding: '40px 32px', boxShadow: '0 8px 32px rgba(0,0,0,0.3)' }}>
<div style={{ textAlign: 'center', marginBottom: 32 }}>
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 6 }}>
<span className="dg-gradient-text">SQL Audit Platform</span>
<span className="dg-gradient-text">SQL审核平台</span>
</h1>
<p style={{ color: 'var(--dg-text-muted)', fontSize: 14 }}>Sign in to your account</p>
<p style={{ color: 'var(--dg-text-muted)', fontSize: 14 }}>登录您的账号</p>
</div>
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
@@ -33,14 +33,14 @@ export default function CenteredCard() {
<input
type="text"
placeholder="Username"
placeholder="用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={{ padding: '11px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
/>
<input
type="password"
placeholder="Password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ padding: '11px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
@@ -52,7 +52,7 @@ export default function CenteredCard() {
className="dg-gradient-bg"
style={{ padding: '11px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 4 }}
>
{loading ? 'Signing in…' : 'Sign in'}
{loading ? '登录中…' : '登录'}
</button>
</form>
</div>
+5 -5
View File
@@ -40,9 +40,9 @@ export default function FullImage() {
>
<div style={{ textAlign: 'center', marginBottom: 32 }}>
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 6 }}>
<span className="dg-gradient-text">SQL Audit Platform</span>
<span className="dg-gradient-text">SQL审核平台</span>
</h1>
<p style={{ color: 'rgba(255,255,255,0.6)', fontSize: 14 }}>Sign in to your workspace</p>
<p style={{ color: 'rgba(255,255,255,0.6)', fontSize: 14 }}>登录您的工作区</p>
</div>
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
@@ -50,14 +50,14 @@ export default function FullImage() {
<input
type="text"
placeholder="Username"
placeholder="用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={{ padding: '11px 14px', borderRadius: 8, background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)', color: '#fff', fontSize: 14, outline: 'none' }}
/>
<input
type="password"
placeholder="Password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ padding: '11px 14px', borderRadius: 8, background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.12)', color: '#fff', fontSize: 14, outline: 'none' }}
@@ -69,7 +69,7 @@ export default function FullImage() {
className="dg-gradient-bg"
style={{ padding: '11px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 4 }}
>
{loading ? 'Signing in…' : 'Sign in'}
{loading ? '登录中…' : '登录'}
</button>
</form>
</div>
@@ -18,23 +18,23 @@ export default function MinimalDark() {
<div style={{ width: '100%', maxWidth: 320, textAlign: 'center' }}>
<Database size={36} className="dg-gradient-text" style={{ marginBottom: 16, display: 'inline-block' }} />
<h1 style={{ fontSize: 22, fontWeight: 700, marginBottom: 4 }}>
<span className="dg-gradient-text">SQL Audit Platform</span>
<span className="dg-gradient-text">SQL审核平台</span>
</h1>
<p style={{ color: 'var(--dg-text-muted)', fontSize: 13, marginBottom: 36 }}>Secure database auditing & compliance</p>
<p style={{ color: 'var(--dg-text-muted)', fontSize: 13, marginBottom: 36 }}>安全的数据库审核与合规管理</p>
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
{error && <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div>}
<input
type="text"
placeholder="Username"
placeholder="用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: 'none', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
/>
<input
type="password"
placeholder="Password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-secondary)', border: 'none', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
@@ -46,7 +46,7 @@ export default function MinimalDark() {
className="dg-gradient-bg"
style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1, marginTop: 8 }}
>
{loading ? 'Signing in…' : 'Sign in'}
{loading ? '登录中…' : '登录'}
</button>
</form>
</div>
+7 -7
View File
@@ -29,9 +29,9 @@ export default function SidePanel() {
<div style={{ position: 'relative', zIndex: 2, textAlign: 'center', maxWidth: 320 }}>
<Database size={40} style={{ color: '#fff', marginBottom: 20 }} />
<h1 style={{ fontSize: 26, fontWeight: 700, color: '#fff', marginBottom: 12 }}>SQL Audit Platform</h1>
<h1 style={{ fontSize: 26, fontWeight: 700, color: '#fff', marginBottom: 12 }}>SQL审核平台</h1>
<p style={{ color: 'rgba(255,255,255,0.7)', fontSize: 14, lineHeight: 1.6 }}>
Empowering teams with intelligent SQL auditing, real-time compliance monitoring, and actionable insights for database security.
为团队提供智能 SQL 审核、实时合规监控与数据库安全保障。
</p>
</div>
</div>
@@ -39,21 +39,21 @@ export default function SidePanel() {
{/* Right Panel — Login Form */}
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 48 }}>
<form onSubmit={handleLogin} style={{ width: '100%', maxWidth: 360, display: 'flex', flexDirection: 'column', gap: 20 }}>
<h2 style={{ fontSize: 22, fontWeight: 600, marginBottom: 4 }}>Welcome back</h2>
<p style={{ color: 'var(--dg-text-muted)', fontSize: 14, marginBottom: 8 }}>Sign in to access your dashboard</p>
<h2 style={{ fontSize: 22, fontWeight: 600, marginBottom: 4 }}>欢迎回来</h2>
<p style={{ color: 'var(--dg-text-muted)', fontSize: 14, marginBottom: 8 }}>登录以访问控制台</p>
{error && <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div>}
<input
type="text"
placeholder="Username"
placeholder="用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
/>
<input
type="password"
placeholder="Password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
@@ -65,7 +65,7 @@ export default function SidePanel() {
className="dg-gradient-bg"
style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1 }}
>
{loading ? 'Signing in…' : 'Sign in'}
{loading ? '登录中…' : '登录'}
</button>
</form>
</div>
+13 -13
View File
@@ -3,12 +3,12 @@ import { useTheme } from '@/hooks/useTheme';
import { Sun, Moon, Database, Code, GitBranch, Bell, History, UserCog } from 'lucide-react';
const features = [
{ icon: Database, label: 'SQL Auditing', desc: 'Real-time query analysis' },
{ icon: Code, label: 'Smart Rules', desc: 'Custom compliance rules' },
{ icon: GitBranch, label: 'Version Control', desc: 'Track schema changes' },
{ icon: Bell, label: 'Alerts', desc: 'Instant notifications' },
{ icon: History, label: 'History', desc: 'Full audit trail' },
{ icon: UserCog, label: 'Access Control', desc: 'Role-based permissions' },
{ icon: Database, label: 'SQL审核', desc: '实时查询分析与合规检测' },
{ icon: Code, label: '智能规则', desc: '自定义审核规则引擎' },
{ icon: GitBranch, label: '变更管理', desc: 'DDL/DML 变更审批流' },
{ icon: Bell, label: '即时通知', desc: '多渠道消息推送' },
{ icon: History, label: '审计追踪', desc: '完整操作历史记录' },
{ icon: UserCog, label: '权限管理', desc: '基于角色的访问控制' },
];
export default function SplitView() {
@@ -28,10 +28,10 @@ export default function SplitView() {
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: '48px 40px', background: 'var(--dg-bg-secondary)' }}>
<div style={{ maxWidth: 440 }}>
<h1 style={{ fontSize: 28, fontWeight: 700, marginBottom: 8 }}>
<span className="dg-gradient-text">SQL Audit Platform</span>
<span className="dg-gradient-text">SQL审核平台</span>
</h1>
<p style={{ color: 'var(--dg-text-muted)', fontSize: 15, marginBottom: 36, lineHeight: 1.5 }}>
Enterprise-grade SQL auditing and compliance management.
企业级 SQL 审核与合规管理平台。
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
{features.map((f) => (
@@ -51,21 +51,21 @@ export default function SplitView() {
onSubmit={handleLogin}
style={{ width: '100%', maxWidth: 360, display: 'flex', flexDirection: 'column', gap: 20 }}
>
<h2 style={{ fontSize: 22, fontWeight: 600, marginBottom: 4 }}>Sign in</h2>
<p style={{ color: 'var(--dg-text-muted)', fontSize: 14, marginBottom: 8 }}>Enter your credentials to continue</p>
<h2 style={{ fontSize: 22, fontWeight: 600, marginBottom: 4 }}>登录</h2>
<p style={{ color: 'var(--dg-text-muted)', fontSize: 14, marginBottom: 8 }}>请输入您的账号信息</p>
{error && <div style={{ color: '#f87171', fontSize: 13 }}>{error}</div>}
<input
type="text"
placeholder="Username"
placeholder="用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
/>
<input
type="password"
placeholder="Password"
placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ padding: '10px 14px', borderRadius: 8, background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)', fontSize: 14, outline: 'none' }}
@@ -77,7 +77,7 @@ export default function SplitView() {
className="dg-gradient-bg"
style={{ padding: '10px 14px', borderRadius: 8, border: 'none', color: '#fff', fontWeight: 600, fontSize: 14, cursor: loading || !username || !password ? 'not-allowed' : 'pointer', opacity: loading || !username || !password ? 0.6 : 1 }}
>
{loading ? 'Signing in…' : 'Sign in'}
{loading ? '登录中…' : '登录'}
</button>
</form>
</div>