diff --git a/config/conf.ini b/config/conf.ini deleted file mode 100644 index ab5f686..0000000 --- a/config/conf.ini +++ /dev/null @@ -1,23 +0,0 @@ -[mysql] -Type = mysql -Host = 47.93.160.42 -Port = 3630 -User = blog -Password = qI7=bL4@iJ -DBName = blog -Charset = utf8mb4 -Prefix = gin_ -;Prefix = gin_ - -[jwt] -SecretKey = \x13\xbf\xd2 1\xce\x8b\xc1\t\xc1=\xec\x07\x93\xd4\x9e\xbco\xb0Z - -[project] -StaticUrlMapPath = {"assets/static/": "static/", "assets/docs/": "docs/", "media/": "media/"} -TemplateGlob = templates/**/* -MediaFilePath = "media/upload/" - -[server] -Port = 7890 -ReadTimeout = 60 -WriteTimeout = 60 diff --git a/config/config.go b/config/config.go index e36a8c6..b805b42 100644 --- a/config/config.go +++ b/config/config.go @@ -8,16 +8,13 @@ package config import ( "fmt" - "log" "time" "github.com/fsnotify/fsnotify" - "github.com/go-ini/ini" "github.com/spf13/viper" ) -type SqlDataBase struct { - Type string +type DataBaseConfig struct { Host string Port string User string @@ -25,6 +22,9 @@ type SqlDataBase struct { DBName string Charset string Prefix string + Driver string + DSN string + MaxConns int `mapstructure:"max_conns"` } type Jwt struct { @@ -32,75 +32,95 @@ type Jwt struct { } type Project struct { - StaticUrlMapPath string + StaticUrlMapPath []interface{} TemplateGlob string MediaFilePath string CurrentTheme string } -type Server struct { +type ServerConfig struct { Port string + EnableGzip bool `mapstructure:"enable_gzip"` + CSRFSecret string `mapstructure:"csrf_secret"` ReadTimeout time.Duration WriteTimeout time.Duration } -var ( - DataBase = &SqlDataBase{} - JwtSecretKey = &Jwt{} - ProjectCfg = &Project{} - HttpServer = &Server{} -) - -func SetUp() { - cfg, err := ini.Load("config/conf.ini") - if err != nil { - panic(err) - } - if err := cfg.Section("mysql").MapTo(DataBase); err != nil { - panic(err) - } - if err := cfg.Section("jwt").MapTo(JwtSecretKey); err != nil { - panic(err) - } - if err := cfg.Section("project").MapTo(ProjectCfg); err != nil { - panic(err) - } - if err := cfg.Section("server").MapTo(HttpServer); err != nil { - panic(err) - } +type ThemeConfig struct { + Current string + AllowUpload bool `mapstructure:"allow_upload"` +} +type SecurityConfig struct { + JWTSecret string `mapstructure:"jwt_secret"` + CORSAllowedOrigins []string `mapstructure:"cors_allowed_origins"` +} +type Config struct { + Env string + Server ServerConfig + DataBase DataBaseConfig + Theme ThemeConfig + JwtSecretKey Jwt + Project Project + Security SecurityConfig } -func SetUp1() { +func LoadConfig(configPath string) (*Config, error) { + // 1. 基础配置 + viper.SetConfigFile(configPath) // 设置配置文件的名称(不包括扩展名) - viper.SetConfigName("config.yml") + viper.SetConfigName("config") // 设置配置文件所在的目录 - viper.AddConfigPath("./conf") + viper.AddConfigPath("./config") // 设置配置文件的类型为YAML viper.SetConfigType("yaml") + // 3. 自动读取环境变量(自动转换大写和下划线) + viper.AutomaticEnv() + viper.SetEnvPrefix("BLOG") // 环境变量前缀 BLOG_xxx + // 2. 设置默认值 + viper.SetDefault("server.port", 3000) + viper.SetDefault("database.max_conns", 10) + viper.SetDefault("theme.allow_upload", false) // 读取配置文件 if err := viper.ReadInConfig(); err != nil { - log.Fatalf("Error reading config file, %s", err) + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + return nil, fmt.Errorf("配置文件未找到: %s", configPath) + } + return nil, fmt.Errorf("读取配置文件失败: %w", err) } + //监控并重新读取配置文件 viper.WatchConfig() viper.OnConfigChange(func(e fsnotify.Event) { // 配置文件发生变更之后会调用的回调函数 fmt.Println("Config file changed:", e.Name) }) + // 5. 反序列化到结构体 + var cfg Config + if err := viper.Unmarshal(&cfg); err != nil { + return nil, fmt.Errorf("配置解析失败: %w", err) + } + + // 6. 校验必要配置项 + // if cfg.Security.JWTSecret == "" { + // return nil, fmt.Errorf("安全配置错误: jwt_secret 必须设置") + // } + // 获取配置值 - mysqlHost := viper.GetString("mysql.Host") + mysqlHost := viper.GetString("database.Host") jwtSecretKey := viper.GetString("jwt.SecretKey") serverPort := viper.GetInt("server.Port") - templateGlob := viper.GetString("project.TemplateGlob") // 打印获取到的配置值 fmt.Printf("MySQL Host: %s\n", mysqlHost) fmt.Printf("JWT Secret Key: %s\n", jwtSecretKey) fmt.Printf("Server Port: %d\n", serverPort) fmt.Printf("TemplateGlob: %s\n", templateGlob) + + return &cfg, nil } + func GetCurrentTheme() string { return "default" diff --git a/config/config.yml b/config/config.yml index 52e8fa6..df0dc6e 100644 --- a/config/config.yml +++ b/config/config.yml @@ -1,5 +1,5 @@ database: - Type: mysql + Driver: mysql Host: 47.93.160.42 Port: 3630 User: blog @@ -7,10 +7,13 @@ database: DBName: blog Charset: utf8mb4 Prefix: gin_ + DSN: "mysql:mysql@tcp(47.93.160.42:3630)/gin_blog?charset=utf8mb4&parseTime=True&loc=Local" # Prefix: gin_ # This line is commented out in the original .ini file jwt: SecretKey: "\x13\xbf\xd2 1\xce\x8b\xc1\t\xc1=\xec\x07\x93\xd4\x9e\xbco\xb0Z" +security: + JWTSecret: "jwt_secret" project: StaticUrlMapPath: @@ -21,7 +24,7 @@ project: MediaFilePath: "media/upload/" server: - Port: 7890 + Port: 8090 ReadTimeout: 60 WriteTimeout: 60 diff --git a/controllers/home.go b/controllers/home.go index a1e4397..8bf536d 100644 --- a/controllers/home.go +++ b/controllers/home.go @@ -1,13 +1,17 @@ package controllers import ( + "go_blog/models" "net/http" "github.com/gin-gonic/gin" ) func Home(c *gin.Context) { - c.HTML(http.StatusOK, "home.html", gin.H{ - "title": "首页", + + var items []models.Content + models.DB.Select("*").Limit(5).Find(&items, "type = ?", "post") + c.HTML(http.StatusOK, "index.tmpl", gin.H{ + "Items": items, }) } diff --git a/controllers/showpost.go b/controllers/showpost.go index e34d175..36cc1dd 100644 --- a/controllers/showpost.go +++ b/controllers/showpost.go @@ -1,13 +1,20 @@ package controllers import ( + "go_blog/models" "net/http" + "strconv" "github.com/gin-gonic/gin" ) func ShowPost(c *gin.Context) { - c.HTML(http.StatusOK, "home.html", gin.H{ - "title": "首页", - }) + + id, err := strconv.ParseInt(c.Param("id"), 10, 32) + if err != nil { + return + } + var content = models.Content{Cid: int32(id)} + models.DB.First(&content) + c.HTML(http.StatusOK, "post.tmpl", content) } diff --git a/main.go b/main.go index 6ed41f5..1461069 100644 --- a/main.go +++ b/main.go @@ -1,47 +1,34 @@ package main import ( - "fmt" - conf "go_blog/config" - "go_blog/controllers" + "go_blog/config" "go_blog/models" "go_blog/routers" - "go_blog/serializers" - "log" - "net/http" - "strconv" "go_blog/themes" - "gorm.io/driver/mysql" "gorm.io/gorm" - "gorm.io/gorm/schema" - "honnef.co/go/tools/config" "github.com/gin-gonic/gin" ) const templatePath = "./templates/*" -func init() { - conf.SetUp() - models.SetUp() -} - func main() { - + conf, err := config.LoadConfig("config.yml") + if err != nil { + panic("配置文件加载失败: " + err.Error()) + } + models.InitDatabase(conf) // 4. 初始化主题管理器 themeManager := themes.NewManager() - if err := themeManager.LoadTheme(conf.GetCurrentTheme()); err != nil { - panic("主题加载失败: " + err.Error()) - } + // if err := themeManager.LoadTheme(conf.GetCurrentTheme()); err != nil { + // panic("主题加载失败: " + err.Error()) + // } // 5. 创建Gin实例 router := gin.Default() router.LoadHTMLGlob(templatePath) - // 注册WebSocket路由 - registerRoutes(router) - router.GET("/events", esSSE) // 6. 注册中间件 router.Use( @@ -58,120 +45,7 @@ func main() { // 8. 注册路由 routers.RegisterRoutes(router) // 9. 启动服务 - router.Run(":8910") //router.Run(":" + cfg.Server.Port) -} -func esSSE(c *gin.Context) { - w := c.Writer - - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.Header().Set("Access-Control-Allow-Origin", "*") - - _, ok := w.(http.Flusher) - - if !ok { - log.Panic("server not support") //浏览器不兼容 - } - - _, err := fmt.Fprintf(w, "id: aaa\ndata: %s\n\n", "dsdf") - _, er1r := fmt.Fprintf(w, "event: connecttime\ndata: %s\n\n", "connecttime") - if err != nil || er1r != nil { - print("error", err) - return - } -} - -func registerRoutes(r *gin.Engine) { - - r.GET("/", func(c *gin.Context) { - var items []models.Content - models.DB.Select("*").Limit(5).Find(&items, "type = ?", "post") - c.HTML(http.StatusOK, "index.tmpl", gin.H{ - "Items": items, - }) - }) - - r.GET("/page", func(c *gin.Context) { - var items []models.Content - var pager serializers.Pager - pager.InitPager(c) - offset := (pager.Page - 1) * pager.PageSize - models.DB.Select("*").Offset(offset).Limit(pager.PageSize).Find(&items, "type = ?", "post") - c.HTML(http.StatusOK, "index.tmpl", gin.H{ - "Items": items, - "Pager": pager, - "Title": "文章列表", - }) - }) - r.GET("/createcontent", func(c *gin.Context) { - c.HTML(http.StatusOK, "content.tmpl", nil) - }) - - r.GET("/post/:id", func(c *gin.Context) { - id, err := strconv.ParseInt(c.Param("id"), 10, 32) - if err != nil { - return - } - var content = models.Content{Cid: int32(id)} - models.DB.First(&content) - c.HTML(http.StatusOK, "post.tmpl", content) - }) - user := getUserInfo() - r.GET("/login", func(c *gin.Context) { - c.HTML(200, "login.tmpl", map[string]interface{}{ - "title": "这个是titile,传入templates中的", - "user": user, - }) - }) - r.GET("/register", func(c *gin.Context) { - c.HTML(200, "register.tmpl", map[string]interface{}{ - "title": "这个是titile,传入templates中的", - "user": user, - }) - }) - - r.GET("/ws", controllers.WebSocketHandler) - r.POST("/content", controllers.CreateContentHandler) - r.POST("/login", controllers.UsersLoginHandler) - r.POST("/register", controllers.UsersRegisterHandler) - r.POST("/setinfo", controllers.UsersSetInfoHandler) - r.POST("/setpwd", controllers.UsersSetPwdHandler) - -} - -func getUserInfo() models.User { - user := models.User{ - Name: "user", - Gender: "male", - Age: 18, - Password: "nothings", - PasswordHash: []byte("nothings"), - } - return user -} - -func initDatabase() { - // 1. 初始化配置 - cfg, err := config.Load("config.yaml") - if err != nil { - panic("加载配置失败: " + err.Error()) - } - - // 2. 初始化数据库 - db, err := gorm.Open(mysql.Open(cfg.String()), &gorm.Config{ - NamingStrategy: schema.NamingStrategy{ - TablePrefix: conf.DataBase.Prefix, - }, - }) - if err != nil { - panic("数据库连接失败: " + err.Error()) - } - - // 3. 自动迁移数据模型 - if err := db.AutoMigrate(&models.Article{}, &models.User{}); err != nil { - panic("数据库迁移失败: " + err.Error()) - } + router.Run(":" + conf.Server.Port) //router.Run() } // 自定义模板渲染器 diff --git a/models/init.go b/models/init.go index d09f208..5e84605 100644 --- a/models/init.go +++ b/models/init.go @@ -8,7 +8,7 @@ package models import ( "fmt" - conf "go_blog/config" + "go_blog/config" "go_blog/pkg/util" "time" @@ -19,7 +19,8 @@ import ( var DB *gorm.DB -func SetUp() { +func InitDatabase(conf *config.Config) { + conUri := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local", conf.DataBase.User, conf.DataBase.Password, @@ -27,20 +28,23 @@ func SetUp() { conf.DataBase.Port, conf.DataBase.DBName, conf.DataBase.Charset) - + // 2. 初始化数据库 db, err := gorm.Open(mysql.Open(conUri), &gorm.Config{ NamingStrategy: schema.NamingStrategy{ TablePrefix: conf.DataBase.Prefix, }, }) if err != nil { - panic(err) + panic("数据库连接失败: " + err.Error()) } - DB = db + DB = db + // 3. 自动迁移数据模型 DB.AutoMigrate(&Account{}) DB.AutoMigrate(&Content{}) - + // if err := db.AutoMigrate(&models.Article{}, &models.User{}); err != nil { + // panic("数据库迁移失败: " + err.Error()) + // } } type BaseModel struct { diff --git a/pkg/jwt/auth.go b/pkg/jwt/auth.go index aa2d16f..43e3766 100644 --- a/pkg/jwt/auth.go +++ b/pkg/jwt/auth.go @@ -7,12 +7,12 @@ package jwt import ( - conf "go_blog/config" "go_blog/models" "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" + "github.com/spf13/viper" ) // 定义jwt载荷 @@ -44,14 +44,14 @@ func GenToken(id uint64, username string) (string, error) { username, } tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - token, err := tokenClaims.SignedString([]byte(conf.JwtSecretKey.SecretKey)) + token, err := tokenClaims.SignedString([]byte(viper.GetString("config.JwtSecretKey.secretKey"))) return token, err } // 验证token合法性 func ValidateJwtToken(token string) (*UserClaims, error) { tokenClaims, err := jwt.ParseWithClaims(token, &UserClaims{}, func(token *jwt.Token) (interface{}, error) { - return []byte(conf.JwtSecretKey.SecretKey), nil + return []byte(viper.GetString("config.JwtSecretKey.secretKey")), nil }) if tokenClaims != nil { diff --git a/routers/router.go b/routers/router.go index 0908d31..4e4d418 100644 --- a/routers/router.go +++ b/routers/router.go @@ -1,13 +1,43 @@ package routers // Add the package declaration at the top import ( + "fmt" "go_blog/controllers" + "go_blog/models" + "go_blog/serializers" + "log" "net/http" "github.com/gin-gonic/gin" ) func RegisterRoutes(r *gin.Engine) { + + // 注册WebSocket路由 + r.GET("/events", esSSE) + + r.GET("/page", func(c *gin.Context) { + var items []models.Content + var pager serializers.Pager + pager.InitPager(c) + offset := (pager.Page - 1) * pager.PageSize + models.DB.Select("*").Offset(offset).Limit(pager.PageSize).Find(&items, "type = ?", "post") + c.HTML(http.StatusOK, "index.tmpl", gin.H{ + "Items": items, + "Pager": pager, + "Title": "文章列表", + }) + }) + r.GET("/createcontent", func(c *gin.Context) { + c.HTML(http.StatusOK, "content.tmpl", nil) + }) + + r.GET("/ws", controllers.WebSocketHandler) + r.POST("/content", controllers.CreateContentHandler) + r.POST("/login", controllers.UsersLoginHandler) + r.POST("/register", controllers.UsersRegisterHandler) + r.POST("/setinfo", controllers.UsersSetInfoHandler) + r.POST("/setpwd", controllers.UsersSetPwdHandler) // Frontend routes (dynamic themes) r.GET("/", controllers.Home) r.GET("/post/:id", controllers.ShowPost) @@ -22,3 +52,35 @@ func RegisterRoutes(r *gin.Engine) { // Static files for themes r.StaticFS("/themes", http.Dir("web/themes")) } +func getUserInfo() models.User { + user := models.User{ + Name: "user", + Gender: "male", + Age: 18, + Password: "nothings", + PasswordHash: []byte("nothings"), + } + return user +} + +func esSSE(c *gin.Context) { + w := c.Writer + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + + _, ok := w.(http.Flusher) + + if !ok { + log.Panic("server not support") //浏览器不兼容 + } + + _, err := fmt.Fprintf(w, "id: aaa\ndata: %s\n\n", "dsdf") + _, er1r := fmt.Fprintf(w, "event: connecttime\ndata: %s\n\n", "connecttime") + if err != nil || er1r != nil { + print("error", err) + return + } +} diff --git a/themes/manager.go b/themes/manager.go index f9043fb..3046cb1 100644 --- a/themes/manager.go +++ b/themes/manager.go @@ -22,19 +22,19 @@ func (tm *ThemeManager) LoadTheme(themeName string) error { tm.CurrentTheme = themeName // Step 1: Validate the theme by reading theme.yaml - themeConfigPath := fmt.Sprintf("h:/code/go_blog/themes/%s/theme.yaml", themeName) + themeConfigPath := fmt.Sprintf("h:/code/go_blog/web/themes/%s/theme.yaml", themeName) if _, err := os.Stat(themeConfigPath); os.IsNotExist(err) { return fmt.Errorf("theme %s does not exist or is invalid", themeName) } // Step 2: Precompile all HTML templates and cache them tm.Templates = make(map[string]*template.Template) - templateDir := fmt.Sprintf("h:/code/go_blog/themes/%s/templates", themeName) + templateDir := fmt.Sprintf("h:/code/go_blog/web/themes/%s/tmplates/", themeName) err := filepath.Walk(templateDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } - if !info.IsDir() && strings.HasSuffix(info.Name(), ".html") { + if !info.IsDir() && strings.HasSuffix(info.Name(), ".tmpl") { tmpl, err := template.ParseFiles(path) if err != nil { return err diff --git a/web/themes/default/templates/index.tmpl b/web/themes/default/templates/index.tmpl new file mode 100644 index 0000000..a3e6e98 --- /dev/null +++ b/web/themes/default/templates/index.tmpl @@ -0,0 +1,164 @@ + + +
+