diff --git a/main.go b/main.go index ab0f885..7f76d70 100644 --- a/main.go +++ b/main.go @@ -41,9 +41,14 @@ func main() { // 3. 初始化数据库 models.InitDatabase(conf) - + sqlDB, _ := models.DB.DB() + defer func() { + if err := sqlDB.Close(); err != nil { + slog.Error("数据库关闭异常", "error", err) + } + }() // 4. 初始化主题系统 - themeManager := themes.NewManager(conf.Theme.Current) + themeManager := themes.NewManager("web\\themes") if err := themeManager.LoadTheme(conf.Theme.Current); err != nil { slog.Error("主题系统初始化失败", "error", err) os.Exit(1) diff --git a/models/init.go b/models/init.go index 9b7b171..5f8bcf9 100644 --- a/models/init.go +++ b/models/init.go @@ -41,12 +41,7 @@ func InitDatabase(conf *config.Config) { 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{}) diff --git a/routers/router.go b/routers/router.go index 98b3765..a457ff0 100644 --- a/routers/router.go +++ b/routers/router.go @@ -7,6 +7,7 @@ import ( "go_blog/serializers" "log" "net/http" + "time" "github.com/gin-gonic/gin" "gorm.io/gorm" @@ -91,6 +92,13 @@ func esSSE(c *gin.Context) { log.Panic("server not support") //浏览器不兼容 } + for { + // Simulate sending events every second + fmt.Fprintf(w, "data: %s\n\n", time.Now().Format(time.Stamp)) + w.(http.Flusher).Flush() + time.Sleep(1 * time.Second) + } + _, 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 { diff --git a/templates/index.tmpl b/templates/index.tmpl index a3e6e98..7ab57e6 100644 --- a/templates/index.tmpl +++ b/templates/index.tmpl @@ -44,10 +44,39 @@ .header {} + + - +

SSE test

+
+ +

diff --git a/themes/manager.go b/themes/manager.go index adba2a1..eed5f4f 100644 --- a/themes/manager.go +++ b/themes/manager.go @@ -1,7 +1,6 @@ package themes import ( - "embed" "errors" "fmt" "html/template" @@ -10,154 +9,228 @@ import ( "path/filepath" "strings" "sync" + "time" "github.com/gin-gonic/gin" + "golang.org/x/exp/slog" + "gopkg.in/yaml.v3" ) -// 主题元数据结构 +const ( + defaultLayout = "base.html" + themeConfigFile = "theme.yaml" + requiredTemplates = "home.html,post.html" +) + +// ThemeMeta 主题元数据 type ThemeMeta struct { Name string `yaml:"name"` - Author string `yaml:"author"` Version string `yaml:"version"` + Author string `yaml:"author"` Description string `yaml:"description"` + License string `yaml:"license"` } -// 主题管理器 +// ThemeManager 主题管理器 type ThemeManager struct { - mu sync.RWMutex - currentTheme string - templates map[string]*template.Template - baseTemplates embed.FS // 可选:嵌入基础模板 - themesDir string + mu sync.RWMutex + themesDir string + currentTheme string + templates *template.Template + staticFS gin.StaticFS + meta ThemeMeta + funcMap template.FuncMap } -// 创建新主题管理器 -func NewManager(themesDir string) *ThemeManager { - return &ThemeManager{ - templates: make(map[string]*template.Template), +// NewManager 创建主题管理器实例 +func NewManager(themesDir, defaultTheme string) (*ThemeManager, error) { + m := &ThemeManager{ themesDir: themesDir, + funcMap: defaultFuncMap(), + } + + if err := m.LoadTheme(defaultTheme); err != nil { + return nil, fmt.Errorf("初始化默认主题失败: %w", err) + } + return m, nil +} + +// 默认模板函数 +func defaultFuncMap() template.FuncMap { + return template.FuncMap{ + "safeHTML": func(s string) template.HTML { return template.HTML(s) }, + "dateFmt": func(t time.Time) string { return t.Format("2006-01-02") }, + "assetPath": func(name string) string { + return fmt.Sprintf("/theme/static/%s?t=%d", name, time.Now().Unix()) + }, } } -// 核心方法:加载主题 -func (m *ThemeManager) LoadTheme(themeName string) error { +// LoadTheme 加载指定主题 +func (m *ThemeManager) LoadTheme(name string) error { + start := time.Now() + themePath := filepath.Join(m.themesDir, name) + + // 验证主题完整性 + if err := validateTheme(themePath); err != nil { + return fmt.Errorf("主题验证失败: %w", err) + } + + // 加载元数据 + meta, err := loadThemeMeta(themePath) + if err != nil { + return fmt.Errorf("元数据加载失败: %w", err) + } + + // 解析模板 + tpls, err := parseTemplates(themePath, m.funcMap) + if err != nil { + return fmt.Errorf("模板解析失败: %w", err) + } + + // 准备静态文件系统 + staticPath := filepath.Join(themePath, "static") + staticFS := gin.Dir(staticPath, false) + + // 原子化更新状态 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) - } - - // 2. 加载模板文件 - tpls, err := m.parseTemplates(themePath) - if err != nil { - return fmt.Errorf("template parsing failed: %w", err) - } - - // 3. 更新当前主题 - m.currentTheme = themeName + m.currentTheme = name m.templates = tpls + m.staticFS = staticFS + m.meta = meta + slog.Info("主题加载成功", + "theme", name, + "templates", len(tpls.Templates()), + "duration", time.Since(start), + ) return nil } -// 获取当前主题名称 -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 { +// 验证主题完整性 +func validateTheme(themePath string) error { + // 检查必须文件 requiredFiles := []string{ - "theme.yaml", - "templates/home.html", - "templates/post.html", + themeConfigFile, + filepath.Join("templates", "base.html"), } + for _, f := range strings.Split(requiredTemplates, ",") { + requiredFiles = append(requiredFiles, + filepath.Join("templates", f)) + } + + var missing []string for _, f := range requiredFiles { - if _, err := os.Stat(filepath.Join(themePath, f)); os.IsNotExist(err) { - return false + if _, err := os.Stat(filepath.Join(themePath, f)); err != nil { + if os.IsNotExist(err) { + missing = append(missing, f) + } } } - return true + + if len(missing) > 0 { + return fmt.Errorf("缺失必要文件: %v", missing) + } + return nil } -// 解析模板文件(支持布局继承) -func (m *ThemeManager) parseTemplates(themePath string) (map[string]*template.Template, error) { - templates := make(map[string]*template.Template) +// 加载主题元数据 +func loadThemeMeta(themePath string) (ThemeMeta, error) { + configPath := filepath.Join(themePath, themeConfigFile) + data, err := os.ReadFile(configPath) + if err != nil { + return ThemeMeta{}, err + } - // 加载公共基础模板(可选) - baseTpl := template.New("base").Funcs(template.FuncMap{ - "safeHTML": func(s string) template.HTML { return template.HTML(s) }, - }) + var meta ThemeMeta + if err := yaml.Unmarshal(data, &meta); err != nil { + return ThemeMeta{}, err + } - // 从主题目录加载模板 + if meta.Name == "" { + return ThemeMeta{}, errors.New("主题名称不能为空") + } + return meta, nil +} + +// 解析模板文件 +func parseTemplates(themePath string, funcMap template.FuncMap) (*template.Template, error) { 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") { + baseFile := filepath.Join(tplDir, defaultLayout) + + // 先解析基础模板 + baseTpl, err := template.New("base"). + Funcs(funcMap). + ParseFiles(baseFile) + if err != nil { + return nil, fmt.Errorf("基础模板解析失败: %w", err) + } + + // 遍历模板目录 + return filepath.WalkDir(tplDir, func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { return nil } - // 读取模板内容 - content, err := os.ReadFile(path) - if err != nil { - return err + if path == baseFile { // 已处理基础模板 + return nil } - // 生成模板名称(相对路径) - name := strings.TrimPrefix(path, tplDir+string(filepath.Separator)) - name = strings.TrimSuffix(name, filepath.Ext(name)) + if filepath.Ext(path) != ".html" { + return nil + } // 克隆基础模板并解析 - tpl := template.Must(baseTpl.Clone()) - tpl, err = tpl.Parse(string(content)) + _, err = baseTpl.Clone(). + ParseFiles(path) if err != nil { - return fmt.Errorf("parse error in %s: %w", name, err) + return fmt.Errorf("解析 %s 失败: %w", filepath.Base(path), 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 } -// 获取可用主题列表 +// GetTemplate 获取指定模板 +func (m *ThemeManager) GetTemplate(name string) *template.Template { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.templates.Lookup(name + ".html") +} + +// RegisterStatic 注册静态资源路由 +func (m *ThemeManager) RegisterStatic(r *gin.Engine) { + m.mu.RLock() + defer m.mu.RUnlock() + + r.StaticFS("/theme/static", m.staticFS) +} + +// CurrentThemeInfo 返回当前主题信息 +func (m *ThemeManager) CurrentThemeInfo() ThemeMeta { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.meta +} + +// ListThemes 获取可用主题列表 func (m *ThemeManager) ListThemes() ([]string, error) { - dirs, err := os.ReadDir(m.themesDir) + entries, 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()) + for _, entry := range entries { + if entry.IsDir() { + if validateTheme(filepath.Join(m.themesDir, entry.Name())) == nil { + themes = append(themes, entry.Name()) + } } } return themes, nil