feat: 添加安装页面,支持 SQLite 和 Redis 配置
Build and Deploy / build-and-push (push) Failing after 42s

- 新增安装控制器 (install_controller.go)
- 新增安装服务 (install_service.go)
- 添加 Redis 配置结构到配置服务
- 更新路由注册,添加安装 API
- 创建前端安装页面 (Install.tsx)
- 支持 SQLite 和 MySQL 数据库选择
- Redis 为可选配置
- 安装完成后创建管理员账号

API 端点:
- GET /api/v1/install/status - 获取安装状态
- POST /api/v1/install - 执行安装
This commit is contained in:
2026-07-21 17:07:41 +00:00
parent c55f184cfa
commit f7a7d44ebe
12 changed files with 1134 additions and 9 deletions
+110
View File
@@ -0,0 +1,110 @@
package controllers
import (
"net/http"
"github.com/engigu/baihu-panel/internal/services"
"github.com/gin-gonic/gin"
)
type InstallController struct {
installService *services.InstallService
}
func NewInstallController() *InstallController {
return &InstallController{
installService: services.NewInstallService(),
}
}
// GetInstallStatus 获取安装状态
// @Summary 获取安装状态
// @Description 检查系统是否已完成安装
// @Tags 安装
// @Produce json
// @Success 200 {object} services.InstallStatus
// @Router /api/v1/install/status [get]
func (c *InstallController) GetInstallStatus(ctx *gin.Context) {
status, err := c.installService.CheckInstallStatus()
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, status)
}
// Install 执行安装
// @Summary 执行安装
// @Description 初始化系统配置和管理员账号
// @Tags 安装
// @Accept json
// @Produce json
// @Param request body services.InstallRequest true "安装请求"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/v1/install [post]
func (c *InstallController) Install(ctx *gin.Context) {
// 先检查是否已安装
status, err := c.installService.CheckInstallStatus()
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "检查安装状态失败"})
return
}
if status.Installed {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "系统已安装,无法重复安装"})
return
}
var req services.InstallRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()})
return
}
// 验证必填字段
if req.AdminUsername == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "管理员用户名不能为空"})
return
}
if req.AdminPassword == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "管理员密码不能为空"})
return
}
if len(req.AdminPassword) < 6 {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "管理员密码至少6位"})
return
}
// MySQL 必填验证
if req.DBType == "mysql" {
if req.DBHost == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "MySQL 主机不能为空"})
return
}
if req.DBName == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "MySQL 数据库名不能为空"})
return
}
}
// Redis 启用时的验证
if req.RedisEnabled {
if req.RedisHost == "" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Redis 主机不能为空"})
return
}
}
// 执行安装
if err := c.installService.Install(&req); err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "安装失败: " + err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{
"message": "安装成功",
"admin_username": req.AdminUsername,
})
}
+7
View File
@@ -11,6 +11,13 @@ func initPublicAPIRoutes(api *gin.RouterGroup, c *Controllers) {
ctx.JSON(200, gin.H{"message": "pong"})
})
// Install routes (无需认证,仅在未安装时可用)
install := api.Group("/install")
{
install.GET("/status", c.Install.GetInstallStatus)
install.POST("", c.Install.Install)
}
// api.GET("/debug/goroutines", func(ctx *gin.Context) {
// buf := make([]byte, 1024*1024)
// n := runtime.Stack(buf, true)
+1
View File
@@ -71,6 +71,7 @@ func RegisterControllers() *Controllers {
Monitor: controllers.NewMonitorController(executorService),
Interconnect: controllers.NewInterconnectController(interconnectService),
Data: controllers.NewDataController(taskController, envController),
Install: controllers.NewInstallController(),
}
}
+1
View File
@@ -34,6 +34,7 @@ type Controllers struct {
Monitor *controllers.MonitorController
Interconnect *controllers.InterconnectController
Data *controllers.DataController
Install *controllers.InstallController
}
func Setup(c *Controllers) *gin.Engine {
+9
View File
@@ -36,10 +36,19 @@ type SecurityConfig struct {
Secret string `ini:"secret"`
}
type RedisConfig struct {
Enabled bool `ini:"enabled"`
Host string `ini:"host"`
Port int `ini:"port"`
Password string `ini:"password"`
DB int `ini:"db"`
}
type AppConfig struct {
Server ServerConfig `ini:"server"`
Database DatabaseConfig `ini:"database"`
Security SecurityConfig `ini:"security"`
Redis RedisConfig `ini:"redis"`
}
var Config *AppConfig
+263
View File
@@ -0,0 +1,263 @@
package services
import (
"os"
"path/filepath"
"github.com/engigu/baihu-panel/internal/constant"
"github.com/engigu/baihu-panel/internal/database"
"github.com/engigu/baihu-panel/internal/logger"
"github.com/engigu/baihu-panel/internal/utils"
"gopkg.in/ini.v1"
)
// InstallRequest 安装请求
type InstallRequest struct {
// 数据库配置
DBType string `json:"db_type" binding:"required"` // sqlite 或 mysql
DBHost string `json:"db_host"` // MySQL 主机
DBPort int `json:"db_port"` // MySQL 端口
DBUser string `json:"db_user"` // MySQL 用户名
DBPassword string `json:"db_password"` // MySQL 密码
DBName string `json:"db_name"` // MySQL 数据库名
DBPath string `json:"db_path"` // SQLite 数据库路径
DBSSLMode string `json:"db_ssl_mode"` // SSL 模式
// Redis 配置(可选)
RedisEnabled bool `json:"redis_enabled"` // 是否启用 Redis
RedisHost string `json:"redis_host"` // Redis 主机
RedisPort int `json:"redis_port"` // Redis 端口
RedisPassword string `json:"redis_password"` // Redis 密码
RedisDB int `json:"redis_db"` // Redis 数据库索引
// 管理员账号
AdminUsername string `json:"admin_username" binding:"required"` // 管理员用户名
AdminPassword string `json:"admin_password" binding:"required"` // 管理员密码
AdminEmail string `json:"admin_email"` // 管理员邮箱
// 站点设置
SiteTitle string `json:"site_title"` // 站点标题
SiteSubtitle string `json:"site_subtitle"` // 站点副标题
}
// InstallStatus 安装状态
type InstallStatus struct {
Installed bool `json:"installed"`
ConfigPath string `json:"config_path"`
DBType string `json:"db_type"`
}
// InstallService 安装服务
type InstallService struct {
settingsService *SettingsService
userService *UserService
}
// NewInstallService 创建安装服务
func NewInstallService() *InstallService {
return &InstallService{
settingsService: NewSettingsService(),
userService: NewUserService(),
}
}
// CheckInstallStatus 检查安装状态
func (s *InstallService) CheckInstallStatus() (*InstallStatus, error) {
status := &InstallStatus{
ConfigPath: constant.ConfigPath,
}
// 检查配置文件是否存在
configExists := false
if _, err := os.Stat(constant.ConfigPath); err == nil {
configExists = true
}
// 检查是否已初始化(数据库中有管理员用户)
initialized := false
if configExists {
// 尝试检查是否有管理员用户
admin := s.userService.GetUserByUsername("admin")
if admin != nil {
initialized = true
}
}
// 同时检查数据库中的初始化标志
dbInitialized := s.settingsService.GetSettingValue(constant.SectionSystem, constant.KeyInitialized)
if dbInitialized == "true" {
initialized = true
}
status.Installed = initialized
status.DBType = Config.Database.Type
return status, nil
}
// Install 执行安装
func (s *InstallService) Install(req *InstallRequest) error {
// 1. 创建配置文件
if err := s.createConfigFile(req); err != nil {
return err
}
// 2. 重新加载数据库配置
if err := s.reloadDatabase(req); err != nil {
return err
}
// 3. 初始化数据库
if err := database.Migrate(); err != nil {
return err
}
// 4. 初始化设置
if err := s.settingsService.InitSettings(); err != nil {
logger.Warnf("[Install] 初始化设置失败: %v", err)
}
// 5. 创建管理员账号
if err := s.createAdmin(req); err != nil {
return err
}
// 6. 保存站点设置
if req.SiteTitle != "" || req.SiteSubtitle != "" {
siteSettings := make(map[string]string)
if req.SiteTitle != "" {
siteSettings[constant.KeyTitle] = req.SiteTitle
}
if req.SiteSubtitle != "" {
siteSettings[constant.KeySubtitle] = req.SiteSubtitle
}
if err := s.settingsService.UpdateSectionSettings(constant.SectionSite, siteSettings); err != nil {
logger.Warnf("[Install] 保存站点设置失败: %v", err)
}
}
// 7. 标记已初始化
if err := s.settingsService.SaveSetting(constant.SectionSystem, constant.KeyInitialized, "true"); err != nil {
return err
}
logger.Info("[Install] 安装完成")
return nil
}
// createConfigFile 创建配置文件
func (s *InstallService) createConfigFile(req *InstallRequest) error {
// 确保配置目录存在
configDir := filepath.Dir(constant.ConfigPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
return err
}
// 创建配置文件
cfg := ini.Empty()
// [server] 配置
serverSection, _ := cfg.NewSection("server")
serverSection.NewKey("port", "8052")
serverSection.NewKey("host", "0.0.0.0")
serverSection.NewKey("cookie_name", "BHToken")
// [database] 配置
dbSection, _ := cfg.NewSection("database")
dbSection.NewKey("type", req.DBType)
if req.DBType == "sqlite" {
dbPath := req.DBPath
if dbPath == "" {
dbPath = constant.DefaultDBPath
}
dbSection.NewKey("path", dbPath)
} else if req.DBType == "mysql" {
dbSection.NewKey("host", req.DBHost)
dbSection.NewKey("port", intToStr(req.DBPort))
dbSection.NewKey("user", req.DBUser)
dbSection.NewKey("password", req.DBPassword)
dbSection.NewKey("dbname", req.DBName)
if req.DBSSLMode != "" {
dbSection.NewKey("ssl_mode", req.DBSSLMode)
}
}
dbSection.NewKey("table_prefix", "baihu_")
// [redis] 配置(如果启用)
if req.RedisEnabled {
redisSection, _ := cfg.NewSection("redis")
redisSection.NewKey("enabled", "true")
redisSection.NewKey("host", req.RedisHost)
redisSection.NewKey("port", intToStr(req.RedisPort))
if req.RedisPassword != "" {
redisSection.NewKey("password", req.RedisPassword)
}
redisSection.NewKey("db", intToStr(req.RedisDB))
}
// [security] 配置
securitySection, _ := cfg.NewSection("security")
secret := utils.RandomString(32)
securitySection.NewKey("secret", secret)
// 保存配置文件
if err := cfg.SaveTo(constant.ConfigPath); err != nil {
return err
}
logger.Infof("[Install] 配置文件已保存到: %s", constant.ConfigPath)
return nil
}
// reloadDatabase 重新加载数据库
func (s *InstallService) reloadDatabase(req *InstallRequest) error {
dbCfg := &database.Config{
Type: req.DBType,
Host: req.DBHost,
Port: req.DBPort,
User: req.DBUser,
Password: req.DBPassword,
DBName: req.DBName,
Path: req.DBPath,
SSLMode: req.DBSSLMode,
}
if req.DBType == "sqlite" && req.DBPath == "" {
dbCfg.Path = constant.DefaultDBPath
}
if err := database.Init(dbCfg); err != nil {
return err
}
return nil
}
// createAdmin 创建管理员账号
func (s *InstallService) createAdmin(req *InstallRequest) error {
// 检查用户是否已存在
existingUser := s.userService.GetUserByUsername(req.AdminUsername)
if existingUser != nil {
logger.Info("[Install] 管理员账号已存在,跳过创建")
return nil
}
email := req.AdminEmail
if email == "" {
email = "admin@local"
}
s.userService.CreateUser(req.AdminUsername, req.AdminPassword, email, "admin")
logger.Infof("[Install] 管理员账号创建成功: %s", req.AdminUsername)
return nil
}
// intToStr 整数转字符串
func intToStr(n int) string {
if n == 0 {
return ""
}
return utils.IntToStr(n)
}
+30
View File
@@ -60,3 +60,33 @@ func TrimLastRunes(s string, maxRunes int) string {
}
return s
}
// IntToStr 将整数转换为字符串
func IntToStr(n int) string {
if n == 0 {
return "0"
}
var negative bool
if n < 0 {
negative = true
n = -n
}
var digits []byte
for n > 0 {
digits = append(digits, byte('0'+n%10))
n /= 10
}
if negative {
digits = append(digits, '-')
}
// Reverse
for i, j := 0, len(digits)-1; i < j; i, j = i+1, j-1 {
digits[i], digits[j] = digits[j], digits[i]
}
return string(digits)
}