4 Commits

Author SHA1 Message Date
admin d4d2d74811 fix: auto-create system_settings and users tables on first use
Docker Build / Build and Push Docker Image (push) Successful in 2m13s
Release / Build and Release (push) Successful in 1m0s
2026-06-22 22:31:19 +00:00
admin 530cfed686 fix: add install page HTML and handle missing table error
Docker Build / Build and Push Docker Image (push) Successful in 2m11s
Release / Build and Release (push) Successful in 1m0s
2026-06-22 22:04:31 +00:00
admin c7c10c7c17 fix: correct install handler initialization
Docker Build / Build and Push Docker Image (push) Successful in 2m10s
Release / Build and Release (push) Successful in 1m0s
2026-06-22 21:40:09 +00:00
admin ee54689fe9 fix: remove duplicate /install route registration
Docker Build / Build and Push Docker Image (push) Failing after 10s
Release / Build and Release (push) Failing after 9s
2026-06-22 21:28:15 +00:00
3 changed files with 149 additions and 26 deletions
+2 -15
View File
@@ -201,9 +201,8 @@ func setupRouter(app *Application) *gin.Engine {
})
})
// API v1
// 安装向导路由(未安装时)
if !app.Config.IsInstalled() {
// 安装向导(未安装时可用)
installRepo := install.NewRepository(app.DB)
installSvc := install.NewService(installRepo, "./config.yaml")
installHandler := install.NewHandler(installSvc)
@@ -215,17 +214,5 @@ func setupRouter(app *Application) *gin.Engine {
// TODO: 添加其他模块路由
}
// 安装页面
r.GET("/install", func(c *gin.Context) {
if app.Config.IsInstalled() {
c.Redirect(http.StatusFound, "/login")
return
}
// TODO: 返回前端安装页面
c.JSON(http.StatusOK, gin.H{
"message": "安装向导页面(前端待实现)",
})
})
return r
}
}
+102 -10
View File
@@ -242,14 +242,106 @@ func (h *Handler) Complete(c *gin.Context) {
// @Success 200
// @Router /install [get]
func (h *Handler) InstallPage(c *gin.Context) {
// 检查是否已安装
if installed, _ := h.svc.IsInstalled(c.Request.Context()); installed {
c.Redirect(http.StatusFound, "/login")
return
}
// 返回安装页面(前端静态页面)
c.HTML(http.StatusOK, "install.html", gin.H{
"title": "怒月 - 安装向导",
})
// 返回安装页面(简单 HTML,后续替换为前端)
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>怒月 - 安装向导</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; background: #0a0a0a; color: #e0e0e0; }
h1 { color: #fff; }
.step { margin: 20px 0; padding: 20px; background: #1a1a1a; border-radius: 8px; }
input, select { width: 100%; padding: 10px; margin: 10px 0; background: #2a2a2a; border: 1px solid #3a3a3a; color: #fff; border-radius: 4px; box-sizing: border-box; }
button { padding: 12px 24px; background: #4a9eff; color: #fff; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px; }
button:hover { background: #3a8eef; }
.success { color: #4ade80; }
.error { color: #f87171; }
#result { margin-top: 20px; padding: 15px; border-radius: 4px; }
</style>
</head>
<body>
<h1>🌙 怒月 - 安装向导</h1>
<div class="step">
<h3>步骤 1: 数据库配置</h3>
<label>数据库类型:</label>
<select id="dbType">
<option value="sqlite">SQLite (推荐)</option>
<option value="postgres">PostgreSQL</option>
</select>
<div id="sqliteConfig">
<label>数据库路径:</label>
<input type="text" id="dbPath" value="/data/nuyue.db">
</div>
<button onclick="testDB()">测试连接</button>
<span id="dbResult"></span>
</div>
<div class="step">
<h3>步骤 2: Redis 配置 (可选)</h3>
<label>启用 Redis:</label>
<select id="redisEnabled">
<option value="false">不启用</option>
<option value="true">启用</option>
</select>
<div id="redisConfig" style="display:none;">
<label>Redis 地址:</label>
<input type="text" id="redisHost" value="redis:6379">
</div>
<button onclick="testRedis()">测试连接</button>
<span id="redisResult"></span>
</div>
<div class="step">
<h3>步骤 3: 管理员账户</h3>
<label>用户名:</label>
<input type="text" id="adminUser" value="admin">
<label>密码:</label>
<input type="password" id="adminPass" value="">
<label>邮箱:</label>
<input type="email" id="adminEmail" value="">
</div>
<div class="step">
<button onclick="completeInstall()">完成安装</button>
</div>
<div id="result"></div>
<script>
document.getElementById('redisEnabled').onchange = function() {
document.getElementById('redisConfig').style.display = this.value === 'true' ? 'block' : 'none';
};
async function testDB() {
const res = await fetch('/api/v1/install/test-db', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({type: document.getElementById('dbType').value, path: document.getElementById('dbPath').value})
});
const data = await res.json();
document.getElementById('dbResult').textContent = data.code === 0 ? '✓ 连接成功' : '✗ ' + data.message;
document.getElementById('dbResult').className = data.code === 0 ? 'success' : 'error';
}
async function testRedis() {
const res = await fetch('/api/v1/install/test-redis', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({enabled: document.getElementById('redisEnabled').value === 'true', host: document.getElementById('redisHost').value})
});
const data = await res.json();
document.getElementById('redisResult').textContent = data.code === 0 ? '✓ 连接成功' : '✗ ' + data.message;
document.getElementById('redisResult').className = data.code === 0 ? 'success' : 'error';
}
async function completeInstall() {
const res = await fetch('/api/v1/install/complete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
database: {type: document.getElementById('dbType').value, path: document.getElementById('dbPath').value},
redis: {enabled: document.getElementById('redisEnabled').value === 'true', host: document.getElementById('redisHost').value},
admin: {username: document.getElementById('adminUser').value, password: document.getElementById('adminPass').value, email: document.getElementById('adminEmail').value}
})
});
const data = await res.json();
document.getElementById('result').innerHTML = data.code === 0 ? '<span class="success">✓ 安装完成!<a href="/login">点击登录</a></span>' : '<span class="error">✗ ' + data.message + '</span>';
}
</script>
</body>
</html>`)
}
+45 -1
View File
@@ -154,8 +154,9 @@ func (r *installRepository) IsInstalled(ctx context.Context) (bool, error) {
if err == gorm.ErrRecordNotFound {
return false, nil
}
// 表不存在或其他错误,返回 false(未安装状态)
if err != nil {
return false, err
return false, nil
}
return value == "true", nil
@@ -163,6 +164,11 @@ func (r *installRepository) IsInstalled(ctx context.Context) (bool, error) {
// upsertSetting 插入或更新设置
func (r *installRepository) upsertSetting(ctx context.Context, key, value string) error {
// 先确保表存在
if err := r.ensureTablesExist(ctx); err != nil {
return err
}
now := time.Now()
result := r.db.WithContext(ctx).
@@ -188,6 +194,44 @@ func (r *installRepository) upsertSetting(ctx context.Context, key, value string
return nil
}
// ensureTablesExist 确保必要的表存在
func (r *installRepository) ensureTablesExist(ctx context.Context) error {
// 创建 system_settings 表
if err := r.db.WithContext(ctx).Exec(`
CREATE TABLE IF NOT EXISTS system_settings (
id TEXT PRIMARY KEY,
category TEXT,
key TEXT UNIQUE,
value TEXT,
created_at DATETIME,
updated_at DATETIME
)
`).Error; err != nil {
return err
}
// 创建 users 表
if err := r.db.WithContext(ctx).Exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE,
password_hash TEXT,
email TEXT,
email_verified BOOLEAN,
role TEXT,
status TEXT,
encrypted_config_key TEXT,
config_key_nonce TEXT,
created_at DATETIME,
updated_at DATETIME
)
`).Error; err != nil {
return err
}
return nil
}
func boolToStr(b bool) string {
if b {
return "true"