package install import ( "crypto/rand" "encoding/hex" "fmt" "log" "os" "path/filepath" "strings" "time" "verification-platform-backend/internal/config" "verification-platform-backend/internal/database" "verification-platform-backend/internal/model" "verification-platform-backend/pkg/response" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "github.com/spf13/viper" "golang.org/x/crypto/bcrypt" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" ) func SetupRoutes(r *gin.Engine) { install := r.Group("/api/install") { install.GET("/status", handleGetInstallStatus) install.POST("/setup", handleSetup) install.POST("/test-db", handleTestDatabase) } } type InstallStatus struct { Installed bool `json:"installed"` DbType string `json:"db_type"` HasAdmin bool `json:"has_admin"` ConfigExist bool `json:"config_exist"` } func handleGetInstallStatus(c *gin.Context) { configExist := checkConfigFile() hasAdmin := checkAdminExists() installed := configExist && hasAdmin var dbType string if configExist { dbType = config.GetString("database.type") if dbType == "" { if strings.Contains(config.GetString("database.host"), "") && config.GetString("database.host") != "localhost" { dbType = "mysql" } else { dbType = "sqlite" } } } response.Success(c, InstallStatus{ Installed: installed, DbType: dbType, HasAdmin: hasAdmin, ConfigExist: configExist, }) } type TestDBRequest struct { DbType string `json:"db_type"` Host string `json:"host"` Port string `json:"port"` Name string `json:"name"` Username string `json:"username"` Password string `json:"password"` } func handleTestDatabase(c *gin.Context) { var req TestDBRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, 400, "参数错误") return } if req.DbType == "sqlite" { dataDir := config.GetDataDir() dbPath := filepath.Join(dataDir, "verification_platform.db") testDB, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { response.Error(c, 400, "无法创建SQLite数据库: "+err.Error()) return } sqlDB, _ := testDB.DB() sqlDB.Close() response.Success(c, gin.H{"message": "SQLite数据库连接成功"}) return } if req.DbType == "mysql" { if req.Host == "" || req.Port == "" || req.Name == "" || req.Username == "" { response.Error(c, 400, "MySQL配置不完整") return } dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", req.Username, req.Password, req.Host, req.Port, req.Name) testDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { response.Error(c, 400, "MySQL连接失败: "+err.Error()) return } sqlDB, _ := testDB.DB() sqlDB.Close() response.Success(c, gin.H{"message": "MySQL连接成功"}) return } response.Error(c, 400, "不支持的数据库类型") } type SetupRequest struct { DbType string `json:"db_type"` DbHost string `json:"db_host"` DbPort string `json:"db_port"` DbName string `json:"db_name"` DbUsername string `json:"db_username"` DbPassword string `json:"db_password"` JwtSecret string `json:"jwt_secret"` AdminUser string `json:"admin_user"` AdminPass string `json:"admin_pass"` AdminEmail string `json:"admin_email"` UseRedis bool `json:"use_redis"` RedisHost string `json:"redis_host"` RedisPort string `json:"redis_port"` RedisPass string `json:"redis_password"` RedisDb int `json:"redis_db"` } func handleSetup(c *gin.Context) { if checkAdminExists() { response.Error(c, 400, "系统已安装") return } var req SetupRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, 400, "参数错误") return } if req.AdminUser == "" || req.AdminPass == "" { response.Error(c, 400, "管理员账号和密码不能为空") return } if len(req.AdminPass) < 6 { response.Error(c, 400, "密码长度至少6位") return } jwtSecret := req.JwtSecret if jwtSecret == "" { jwtSecret = generateRandomSecret(32) } var newDB *gorm.DB var err error if req.DbType == "sqlite" { dataDir := config.GetDataDir() dbPath := filepath.Join(dataDir, "verification_platform.db") newDB, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), DisableForeignKeyConstraintWhenMigrating: true, }) if err != nil { response.Error(c, 500, "SQLite数据库创建失败: "+err.Error()) return } } else if req.DbType == "mysql" { if req.DbHost == "" || req.DbPort == "" || req.DbName == "" || req.DbUsername == "" { response.Error(c, 400, "MySQL配置不完整") return } dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", req.DbUsername, req.DbPassword, req.DbHost, req.DbPort, req.DbName) newDB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), DisableForeignKeyConstraintWhenMigrating: true, }) if err != nil { response.Error(c, 500, "MySQL连接失败: "+err.Error()) return } } else { response.Error(c, 400, "不支持的数据库类型") return } if sqlDB, err := newDB.DB(); err == nil { sqlDB.SetMaxIdleConns(10) sqlDB.SetMaxOpenConns(100) sqlDB.SetConnMaxLifetime(time.Hour) } if err := runMigrations(newDB); err != nil { response.Error(c, 500, "数据库迁移失败: "+err.Error()) return } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.AdminPass), bcrypt.DefaultCost) if err != nil { response.Error(c, 500, "密码加密失败") return } admin := model.User{ Username: req.AdminUser, Password: string(hashedPassword), Email: &req.AdminEmail, Role: "admin", Status: "active", } if err := newDB.Create(&admin).Error; err != nil { response.Error(c, 500, "创建管理员失败: "+err.Error()) return } configContent := generateConfigFile(req.DbType, req, jwtSecret) if err := saveConfigFile(configContent); err != nil { log.Printf("Warning: failed to save config file: %v", err) } viper.Set("app.jwt_secret", jwtSecret) viper.Set("database.type", req.DbType) if req.DbType == "mysql" { viper.Set("database.host", req.DbHost) viper.Set("database.port", req.DbPort) viper.Set("database.name", req.DbName) viper.Set("database.username", req.DbUsername) viper.Set("database.password", req.DbPassword) } database.DB = newDB response.Success(c, gin.H{ "message": "安装成功", "jwt_secret": jwtSecret, }) } func checkConfigFile() bool { dataDir := config.GetDataDir() configPath := filepath.Join(dataDir, "config.yaml") _, err := os.Stat(configPath) return err == nil } func checkAdminExists() bool { if database.DB == nil { return false } var count int64 database.DB.Model(&model.User{}).Where("role = ?", "admin").Count(&count) return count > 0 } func generateRandomSecret(length int) string { bytes := make([]byte, length) rand.Read(bytes) return hex.EncodeToString(bytes)[:length] } func runMigrations(db *gorm.DB) error { return db.AutoMigrate( &model.User{}, &model.AppUser{}, &model.Application{}, &model.CardType{}, &model.Card{}, &model.RechargeRecord{}, &model.UserDevice{}, &model.DeviceSession{}, &model.Setting{}, &model.ExtensionAPIKey{}, &model.AgentApplication{}, &model.Package{}, &model.Order{}, ) } func generateConfigFile(dbType string, req SetupRequest, jwtSecret string) string { var content strings.Builder content.WriteString("# 应用配置\n") content.WriteString("app:\n") content.WriteString(" name: verification-platform\n") content.WriteString(" env: production\n") content.WriteString(" port: \"8080\"\n") content.WriteString(fmt.Sprintf(" jwt_secret: \"%s\"\n", jwtSecret)) content.WriteString(" jwt_expire: 24h\n\n") content.WriteString("# 数据库配置\n") content.WriteString("database:\n") content.WriteString(fmt.Sprintf(" type: %s\n", dbType)) if dbType == "mysql" { content.WriteString(fmt.Sprintf(" host: %s\n", req.DbHost)) content.WriteString(fmt.Sprintf(" port: %s\n", req.DbPort)) content.WriteString(fmt.Sprintf(" name: %s\n", req.DbName)) content.WriteString(fmt.Sprintf(" username: %s\n", req.DbUsername)) content.WriteString(fmt.Sprintf(" password: %s\n", req.DbPassword)) } else { content.WriteString(" host: localhost\n") content.WriteString(" port: \"3306\"\n") content.WriteString(" name: verification_platform\n") content.WriteString(" username: root\n") content.WriteString(" password: \"\"\n") } content.WriteString(" charset: utf8mb4\n") content.WriteString(" max_idle_conns: 10\n") content.WriteString(" max_open_conns: 100\n\n") content.WriteString("# Redis配置\n") content.WriteString("redis:\n") content.WriteString(fmt.Sprintf(" enabled: %v\n", req.UseRedis)) if req.UseRedis { content.WriteString(fmt.Sprintf(" host: %s\n", req.RedisHost)) content.WriteString(fmt.Sprintf(" port: %s\n", req.RedisPort)) content.WriteString(fmt.Sprintf(" password: %s\n", req.RedisPass)) content.WriteString(fmt.Sprintf(" db: %d\n", req.RedisDb)) } else { content.WriteString(" host: localhost\n") content.WriteString(" port: \"6379\"\n") content.WriteString(" password: \"\"\n") content.WriteString(" db: 0\n") } content.WriteString("\n# 日志配置\n") content.WriteString("log:\n") content.WriteString(" level: info\n") content.WriteString(" format: json\n") return content.String() } func saveConfigFile(content string) error { dataDir := config.GetDataDir() configPath := filepath.Join(dataDir, "config.yaml") file, err := os.Create(configPath) if err != nil { return err } defer file.Close() _, err = file.WriteString(content) return err }