diff --git a/admin/thememanager.go b/admin/thememanager.go new file mode 100644 index 0000000..81e09bf --- /dev/null +++ b/admin/thememanager.go @@ -0,0 +1,21 @@ +package admin + +import ( + "go_blog/themes" + + "github.com/gin-gonic/gin" +) + +var themeManager = themes.NewManager("themes") +var router = gin.Default() + +// 切换主题(管理后台) +func switchTheme(c *gin.Context) { + newTheme := c.PostForm("theme") + if err := themeManager.LoadTheme(newTheme); err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + themeManager.RegisterStaticRoutes(router) // 重新注册静态路由 + c.JSON(200, gin.H{"status": "success"}) +} diff --git a/config/config.go b/config/config.go index b805b42..21563a8 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,11 @@ import ( "github.com/spf13/viper" ) +const ( + EnvDevelopment = "development" + EnvProduction = "production" +) + type DataBaseConfig struct { Host string Port string diff --git a/main.go b/main.go index 1461069..ab0f885 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,9 @@ import ( "go_blog/config" "go_blog/models" "go_blog/routers" + "log/slog" + "os" + "time" "go_blog/themes" @@ -12,26 +15,52 @@ import ( "github.com/gin-gonic/gin" ) +// @title 个人博客系统API +// @version 1.0 +// @description 基于Go语言的可定制主题博客系统 const templatePath = "./templates/*" func main() { + + //读取配置文件 + // 1. 初始化配置 conf, err := config.LoadConfig("config.yml") if err != nil { - panic("配置文件加载失败: " + err.Error()) + slog.Error("配置加载失败", "error", err) + os.Exit(1) } - models.InitDatabase(conf) - // 4. 初始化主题管理器 - themeManager := themes.NewManager() - // if err := themeManager.LoadTheme(conf.GetCurrentTheme()); err != nil { - // panic("主题加载失败: " + err.Error()) + + // 2. 初始化日志系统 + // if err := logger.Initialize(cfg.Log); err != nil { + // slog.Error("日志初始化失败", "error", err) + // os.Exit(1) // } + // defer logger.Flush() + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + slog.SetDefault(logger) + + // 3. 初始化数据库 + models.InitDatabase(conf) + + // 4. 初始化主题系统 + themeManager := themes.NewManager(conf.Theme.Current) + if err := themeManager.LoadTheme(conf.Theme.Current); err != nil { + slog.Error("主题系统初始化失败", "error", err) + os.Exit(1) + } // 5. 创建Gin实例 + // 根据环境设置Gin模式 + if conf.Env == config.EnvProduction { + gin.SetMode(gin.ReleaseMode) + } router := gin.Default() router.LoadHTMLGlob(templatePath) + themeManager.RegisterStaticRoutes(router) // 6. 注册中间件 router.Use( + loggerMiddleware(), gin.Logger(), gin.Recovery(), databaseMiddleware(models.DB), @@ -45,7 +74,7 @@ func main() { // 8. 注册路由 routers.RegisterRoutes(router) // 9. 启动服务 - router.Run(":" + conf.Server.Port) //router.Run() + router.Run(":" + conf.Server.Port) } // 自定义模板渲染器 @@ -83,3 +112,19 @@ func themeMiddleware(manager *themes.ThemeManager) gin.HandlerFunc { c.Next() } } + +func loggerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + + c.Next() + + duration := time.Since(start) + slog.Info("请求处理完成", + "status", c.Writer.Status(), + "method", c.Request.Method, + "path", c.Request.URL.Path, + "duration", duration, + ) + } +} diff --git a/models/init.go b/models/init.go index 5e84605..9b7b171 100644 --- a/models/init.go +++ b/models/init.go @@ -10,6 +10,8 @@ import ( "fmt" "go_blog/config" "go_blog/pkg/util" + "log/slog" + "os" "time" "gorm.io/driver/mysql" @@ -34,14 +36,21 @@ func InitDatabase(conf *config.Config) { TablePrefix: conf.DataBase.Prefix, }, }) - if err != nil { - panic("数据库连接失败: " + err.Error()) - } + if err != nil { + slog.Error("数据库连接失败", "error", err) + os.Exit(1) + } + sqlDB, _ := db.DB() + defer func() { + if err := sqlDB.Close(); err != nil { + slog.Error("数据库关闭异常", "error", err) + } + }() DB = db // 3. 自动迁移数据模型 - DB.AutoMigrate(&Account{}) - DB.AutoMigrate(&Content{}) + db.AutoMigrate(&Account{}) + db.AutoMigrate(&Content{}) // if err := db.AutoMigrate(&models.Article{}, &models.User{}); err != nil { // panic("数据库迁移失败: " + err.Error()) // } diff --git a/routers/router.go b/routers/router.go index 4e4d418..98b3765 100644 --- a/routers/router.go +++ b/routers/router.go @@ -9,6 +9,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "gorm.io/gorm" ) func RegisterRoutes(r *gin.Engine) { @@ -21,7 +22,20 @@ func RegisterRoutes(r *gin.Engine) { 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") + if dbInterface, ok := c.Get("DB"); ok { + if db, ok := dbInterface.(*gorm.DB); ok { + db.Select("*").Offset(offset).Limit(pager.PageSize).Find(&items, "type = ?", "post") + } else { + log.Println("无法将 DB 转换为 *gorm.DB 类型") + c.JSON(http.StatusInternalServerError, gin.H{"error": "内部服务器错误"}) + return + } + } else { + log.Println("未找到键 'DB' 的上下文值") + c.JSON(http.StatusInternalServerError, gin.H{"error": "内部服务器错误"}) + return + } + c.HTML(http.StatusOK, "index.tmpl", gin.H{ "Items": items, "Pager": pager, diff --git a/themes/manager.go b/themes/manager.go index 3046cb1..adba2a1 100644 --- a/themes/manager.go +++ b/themes/manager.go @@ -1,57 +1,164 @@ -package themes // Declare the package at the top +package themes + import ( + "embed" + "errors" "fmt" "html/template" - "net/http" + "io/fs" "os" "path/filepath" "strings" + "sync" + + "github.com/gin-gonic/gin" ) -// ThemeManager manages themes for the blog system -type ThemeManager struct { - CurrentTheme string // Name of the currently active theme - Templates map[string]*template.Template // Cached compiled HTML templates +// 主题元数据结构 +type ThemeMeta struct { + Name string `yaml:"name"` + Author string `yaml:"author"` + Version string `yaml:"version"` + Description string `yaml:"description"` } -// LoadTheme loads a specified theme by its name -func (tm *ThemeManager) LoadTheme(themeName string) error { - // 1. 读取theme.yaml验证主题有效性 - // 2. 预编译所有HTML模板,缓存到Templates - // 3. 注册静态资源路由:/themes/[name]/static/*filepath - tm.CurrentTheme = themeName +// 主题管理器 +type ThemeManager struct { + mu sync.RWMutex + currentTheme string + templates map[string]*template.Template + baseTemplates embed.FS // 可选:嵌入基础模板 + themesDir string +} - // Step 1: Validate the theme by reading theme.yaml - 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) +// 创建新主题管理器 +func NewManager(themesDir string) *ThemeManager { + return &ThemeManager{ + templates: make(map[string]*template.Template), + themesDir: themesDir, + } +} + +// 核心方法:加载主题 +func (m *ThemeManager) LoadTheme(themeName string) error { + m.mu.Lock() + defer m.mu.Unlock() + + // 1. 验证主题目录结构 + themePath := filepath.Join(m.themesDir, themeName) + if !isValidTheme(themePath) { + return fmt.Errorf("invalid theme structure: %s", themeName) } - // Step 2: Precompile all HTML templates and cache them - tm.Templates = make(map[string]*template.Template) - 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(), ".tmpl") { - tmpl, err := template.ParseFiles(path) - if err != nil { - return err - } - tm.Templates[info.Name()] = tmpl - } - return nil - }) + // 2. 加载模板文件 + tpls, err := m.parseTemplates(themePath) if err != nil { - return fmt.Errorf("failed to load templates: %v", err) + return fmt.Errorf("template parsing failed: %w", err) } - // Step 3: Register static resource routes - http.Handle(fmt.Sprintf("/themes/%s/static/", themeName), http.StripPrefix(fmt.Sprintf("/themes/%s/static/", themeName), http.FileServer(http.Dir(fmt.Sprintf("h:/code/go_blog/themes/%s/static", themeName))))) + // 3. 更新当前主题 + m.currentTheme = themeName + m.templates = tpls return nil } -func NewManager() *ThemeManager { - return &ThemeManager{} + +// 获取当前主题名称 +func (m *ThemeManager) CurrentTheme() string { + m.mu.RLock() + defer m.mu.RUnlock() + return m.currentTheme +} + +// 获取编译后的模板 +func (m *ThemeManager) GetTemplate(name string) *template.Template { + m.mu.RLock() + defer m.mu.RUnlock() + return m.templates[name] +} + +// 注册静态文件路由(Gin框架) +func (m *ThemeManager) RegisterStaticRoutes(router *gin.Engine) { + staticPath := filepath.Join(m.themesDir, m.currentTheme, "static") + router.StaticFS("/theme/static", gin.Dir(staticPath, false)) +} + +// 校验主题完整性 +func isValidTheme(themePath string) bool { + requiredFiles := []string{ + "theme.yaml", + "templates/home.html", + "templates/post.html", + } + + for _, f := range requiredFiles { + if _, err := os.Stat(filepath.Join(themePath, f)); os.IsNotExist(err) { + return false + } + } + return true +} + +// 解析模板文件(支持布局继承) +func (m *ThemeManager) parseTemplates(themePath string) (map[string]*template.Template, error) { + templates := make(map[string]*template.Template) + + // 加载公共基础模板(可选) + baseTpl := template.New("base").Funcs(template.FuncMap{ + "safeHTML": func(s string) template.HTML { return template.HTML(s) }, + }) + + // 从主题目录加载模板 + tplDir := filepath.Join(themePath, "templates") + err := filepath.WalkDir(tplDir, func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() || !strings.HasSuffix(path, ".html") { + return nil + } + + // 读取模板内容 + content, err := os.ReadFile(path) + if err != nil { + return err + } + + // 生成模板名称(相对路径) + name := strings.TrimPrefix(path, tplDir+string(filepath.Separator)) + name = strings.TrimSuffix(name, filepath.Ext(name)) + + // 克隆基础模板并解析 + tpl := template.Must(baseTpl.Clone()) + tpl, err = tpl.Parse(string(content)) + if err != nil { + return fmt.Errorf("parse error in %s: %w", name, err) + } + + templates[name] = tpl + return nil + }) + + if err != nil { + return nil, err + } + + if len(templates) == 0 { + return nil, errors.New("no valid templates found") + } + + return templates, nil +} + +// 获取可用主题列表 +func (m *ThemeManager) ListThemes() ([]string, error) { + dirs, err := os.ReadDir(m.themesDir) + if err != nil { + return nil, err + } + + var themes []string + for _, d := range dirs { + if d.IsDir() && isValidTheme(filepath.Join(m.themesDir, d.Name())) { + themes = append(themes, d.Name()) + } + } + return themes, nil } diff --git a/web/themes/default/theme.yaml b/web/themes/default/theme.yaml index e69de29..801515a 100644 --- a/web/themes/default/theme.yaml +++ b/web/themes/default/theme.yaml @@ -0,0 +1,4 @@ +name: "Default Theme" +author: "Blog System" +version: "1.0.0" +description: "Default clean theme for the blog" \ No newline at end of file