diff --git a/config/config.go b/config/config.go index 21563a8..0aa9bbd 100644 --- a/config/config.go +++ b/config/config.go @@ -132,3 +132,9 @@ func GetCurrentTheme() string { } func SetCurrentTheme(theme string) { } + +// GetJWTSecret 获取 JWT 密钥 +func (c *Config) GetJWTSecret() string { + //return c.JWT.Secret + return c.Security.JWTSecret +} diff --git a/controllers/home.go b/controllers/home.go index 8bf536d..ee26021 100644 --- a/controllers/home.go +++ b/controllers/home.go @@ -2,16 +2,54 @@ package controllers import ( "go_blog/models" + "go_blog/themes" // <-- 确保导入 themes 包 "net/http" "github.com/gin-gonic/gin" + "gorm.io/gorm" // <-- 确保导入 gorm ) func Home(c *gin.Context) { + tm, exists := c.Get("ThemeManager") + if !exists { + c.String(http.StatusInternalServerError, "Theme manager not found in context") + return + } + themeManager, ok := tm.(*themes.ThemeManager) + if !ok { + c.String(http.StatusInternalServerError, "Invalid theme manager type in context") + return + } var items []models.Content - models.DB.Select("*").Limit(5).Find(&items, "type = ?", "post") - c.HTML(http.StatusOK, "index.tmpl", gin.H{ + // 从 Gin 上下文中获取 DB 实例 + dbInterface, exists := c.Get("DB") + if !exists { + c.String(http.StatusInternalServerError, "DB not found in context") + return + } + db, ok := dbInterface.(*gorm.DB) + if !ok { + c.String(http.StatusInternalServerError, "Invalid DB type in context") + return + } + + db.Select("*").Limit(5).Find(&items, "type = ?", "post") + + tpl := themeManager.GetTemplate("index") // "index" 是模板的基本名 (例如 index.tmpl -> index) + if tpl == nil { + c.String(http.StatusInternalServerError, "Template 'index' not found in current theme: "+themeManager.CurrentTheme()) + return + } + + c.Status(http.StatusOK) + c.Header("Content-Type", "text/html; charset=utf-8") + err := tpl.Execute(c.Writer, gin.H{ "Items": items, + "Title": "首页", // 你可以根据需要传递更多数据 }) + if err != nil { + // 实际项目中应记录错误 + c.String(http.StatusInternalServerError, "Error rendering template: "+err.Error()) + } } diff --git a/controllers/showpost.go b/controllers/showpost.go index 36cc1dd..27a4bd1 100644 --- a/controllers/showpost.go +++ b/controllers/showpost.go @@ -2,19 +2,69 @@ package controllers import ( "go_blog/models" + "go_blog/themes" // <-- 确保导入 themes 包 "net/http" "strconv" "github.com/gin-gonic/gin" + "gorm.io/gorm" // <-- 确保导入 gorm ) func ShowPost(c *gin.Context) { - - id, err := strconv.ParseInt(c.Param("id"), 10, 32) - if err != nil { + tm, exists := c.Get("ThemeManager") + if !exists { + c.String(http.StatusInternalServerError, "Theme manager not found") return } + themeManager, ok := tm.(*themes.ThemeManager) + if !ok { + c.String(http.StatusInternalServerError, "Invalid theme manager type") + return + } + + idParam := c.Param("id") + id, err := strconv.ParseInt(idParam, 10, 32) + if err != nil { + c.String(http.StatusBadRequest, "Invalid post ID format") + return + } + var content = models.Content{Cid: int32(id)} - models.DB.First(&content) - c.HTML(http.StatusOK, "post.tmpl", content) + // 从 Gin 上下文中获取 DB 实例 + dbInterface, exists := c.Get("DB") + if !exists { + c.String(http.StatusInternalServerError, "DB not found in context") + return + } + db, ok := dbInterface.(*gorm.DB) + if !ok { + c.String(http.StatusInternalServerError, "Invalid DB type in context") + return + } + + result := db.First(&content) + if result.Error != nil { + if result.Error == gorm.ErrRecordNotFound { + c.String(http.StatusNotFound, "Post not found") + } else { + c.String(http.StatusInternalServerError, "Error fetching post: "+result.Error.Error()) + } + return + } + + // 假设你的主题中有一个名为 "post" 的模板 (post.html 或 post.tmpl) + // 注意:当前的 default 主题 (web/themes/default/templates/) 并没有 post.tmpl,你需要添加它。 + tpl := themeManager.GetTemplate("post") + if tpl == nil { + c.String(http.StatusInternalServerError, "Template 'post' not found in current theme: "+themeManager.CurrentTheme()+". Make sure 'post.html' or 'post.tmpl' exists in the theme's templates directory.") + return + } + + c.Status(http.StatusOK) + c.Header("Content-Type", "text/html; charset=utf-8") + // 将整个 content 对象传递给模板。模板内部通过 {{ .Title }} {{ .Text }} 等访问。 + err = tpl.Execute(c.Writer, content) + if err != nil { + c.String(http.StatusInternalServerError, "Error rendering template: "+err.Error()) + } } diff --git a/controllers/themes.go b/controllers/themes.go new file mode 100644 index 0000000..8db135e --- /dev/null +++ b/controllers/themes.go @@ -0,0 +1,39 @@ +package controllers + +import ( + "go_blog/themes" + "os" + + "github.com/gin-gonic/gin" +) + +// ListThemes 获取可用主题列表 +func ListThemes(c *gin.Context) { + // 从主题管理器获取当前主题 + tm, exists := c.Get("ThemeManager") + if !exists { + c.JSON(500, gin.H{"error": "主题管理器未找到"}) + return + } + themeManager := tm.(*themes.ThemeManager) + + // 读取 web/themes 目录下的所有主题文件夹 + themesDir := "web/themes" + entries, err := os.ReadDir(themesDir) + if err != nil { + c.JSON(500, gin.H{"error": "读取主题目录失败: " + err.Error()}) + return + } + + var themeList []string + for _, entry := range entries { + if entry.IsDir() { + themeList = append(themeList, entry.Name()) + } + } + + c.JSON(200, gin.H{ + "current": themeManager.CurrentTheme(), + "themes": themeList, + }) +} diff --git a/controllers/users.go b/controllers/users.go index 9692d4a..53225d8 100644 --- a/controllers/users.go +++ b/controllers/users.go @@ -21,7 +21,8 @@ func UsersLoginHandler(ctx *gin.Context) { response := Response{Ctx: ctx} var loginUser serializers.Login if err := ctx.ShouldBind(&loginUser); err != nil { - panic(err) + response.BadRequest("请求参数错误: " + err.Error()) // 替换 panic 为错误响应 + return } user := loginUser.GetUser() isLoginUser := user.CheckPassword() @@ -29,7 +30,7 @@ func UsersLoginHandler(ctx *gin.Context) { response.BadRequest("密码错误") return } - token, err := jwt.GenToken(user.ID, user.Username) + token, err := jwt.GenerateToken(user) if err != nil { panic(err) } @@ -44,7 +45,8 @@ func UsersRegisterHandler(ctx *gin.Context) { response := Response{Ctx: ctx} var registerUser serializers.Login if err := ctx.ShouldBind(®isterUser); err != nil { - panic(err) + response.BadRequest("请求参数错误: " + err.Error()) // 替换 panic 为错误响应 + return } user := registerUser.GetUser() status := user.CheckDuplicateUsername() @@ -73,12 +75,20 @@ func UsersSetInfoHandler(ctx *gin.Context) { response.BadRequest("获取不到参数") return } - currentUser := jwt.AssertUser(ctx) - if currentUser != nil { - models.DB.Model(¤tUser).Updates(jsonData) - response.Response(currentUser, nil) + // 从上下文中获取用户(假设 JWT 中间件已将用户存入 "user" 键) + user, exists := ctx.Get("user") + if !exists { + response.Unauthenticated("未登录") return } + currentUser, ok := user.(*models.Account) // 明确类型为 models.Account + if !ok { + response.ServerError("用户类型错误") + return + } + + models.DB.Model(currentUser).Updates(jsonData) + response.Response(currentUser, nil) } // 修改密码 @@ -119,10 +129,14 @@ func UsersListHandler(ctx *gin.Context) { var pager serializers.Pager pager.InitPager(ctx) var users []models.Account - db := models.DB.Model(&users) - total := int64(pager.Total) - db.Count(&total) - db.Offset(pager.OffSet).Limit(pager.PageSize).Find(&users) + + // 先查询总记录数 + var totalCount int64 + models.DB.Model(&models.Account{}).Count(&totalCount) + pager.Total = int(totalCount) // 正确设置总数 + + // 分页查询 + models.DB.Offset(pager.OffSet()).Limit(pager.PageSize).Find(&users) pager.GetPager() response.Response(users, pager) } diff --git a/go.mod b/go.mod index 650503a..fa26a61 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.23.8 require ( github.com/fsnotify/fsnotify v1.7.0 github.com/gin-gonic/gin v1.9.1 - github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/gorilla/websocket v1.5.1 golang.org/x/crypto v0.33.0 gorm.io/driver/mysql v1.5.6 diff --git a/go.sum b/go.sum index 402e64b..97a3842 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/main.go b/main.go index 7f76d70..0369e08 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,6 @@ import ( // @title 个人博客系统API // @version 1.0 // @description 基于Go语言的可定制主题博客系统 -const templatePath = "./templates/*" func main() { @@ -60,7 +59,6 @@ func main() { gin.SetMode(gin.ReleaseMode) } router := gin.Default() - router.LoadHTMLGlob(templatePath) themeManager.RegisterStaticRoutes(router) // 6. 注册中间件 diff --git a/models/account.go b/models/account.go index f996677..005e3be 100644 --- a/models/account.go +++ b/models/account.go @@ -69,3 +69,12 @@ func (a *Account) CheckDuplicateUsername() bool { return true } } + +// GetAccountByID 根据 ID 查询用户 +func GetAccountByID(id uint) (*Account, error) { + var account Account + if err := DB.First(&account, id).Error; err != nil { + return nil, err + } + return &account, nil +} diff --git a/models/user.go b/models/user.go index 5810f1f..e086fb9 100644 --- a/models/user.go +++ b/models/user.go @@ -7,3 +7,12 @@ type User struct { Password string PasswordHash []byte } + +// GetUserByID 根据 ID 查询用户 +func GetUserByID(id uint) (*User, error) { + var user User + if err := DB.First(&user, id).Error; err != nil { + return nil, err + } + return &user, nil +} diff --git a/pkg/jwt/auth.go b/pkg/jwt/auth.go index 43e3766..d58bfb9 100644 --- a/pkg/jwt/auth.go +++ b/pkg/jwt/auth.go @@ -1,77 +1,90 @@ -/* -@Time : 2020/6/29 9:05 -@Author : xuyiqing -@File : auth.py -*/ - package jwt import ( - "go_blog/models" + "errors" + "fmt" + "net/http" + "strings" "time" + "go_blog/config" + "go_blog/models" + "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt" - "github.com/spf13/viper" + "github.com/golang-jwt/jwt/v5" ) -// 定义jwt载荷 -type UserClaims struct { - jwt.StandardClaims - ID uint64 `json:"id"` - Username string `json:"username"` +// 定义 JWT 密钥(从配置文件读取) +var jwtSecret = []byte(config.GetJWTSecret()) + +// CustomClaims 自定义 JWT 载荷(包含用户 ID) +type CustomClaims struct { + UserID uint `json:"user_id"` + jwt.RegisteredClaims } -// 根据payload查询user返回 -func (c *UserClaims) GetUserByID() *models.Account { - var user models.Account - models.DB.Model(&models.Account{}).First(&user, c.ID) - if user.ID > 0 { - return &user - } else { - return nil - } -} - -// 生成jwt token字符串 -func GenToken(id uint64, username string) (string, error) { - expiredTime := time.Now().Add(time.Hour * time.Duration(24)).Unix() - claims := UserClaims{ - jwt.StandardClaims{ - ExpiresAt: expiredTime, +// GenerateToken 生成 JWT 令牌 +func GenerateToken(user *models.Account) (string, error) { + expiresAt := jwt.NewNumericDate(time.Now().Add(24 * time.Hour)) // 令牌有效期 24 小时 + claims := CustomClaims{ + UserID: uint(user.ID), + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: expiresAt, + Issuer: "go_blog", }, - id, - username, } - tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - token, err := tokenClaims.SignedString([]byte(viper.GetString("config.JwtSecretKey.secretKey"))) - return token, err + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(jwtSecret) } -// 验证token合法性 -func ValidateJwtToken(token string) (*UserClaims, error) { - tokenClaims, err := jwt.ParseWithClaims(token, &UserClaims{}, func(token *jwt.Token) (interface{}, error) { - return []byte(viper.GetString("config.JwtSecretKey.secretKey")), nil - }) - - if tokenClaims != nil { - if claims, ok := tokenClaims.Claims.(*UserClaims); ok && tokenClaims.Valid { - return claims, nil +// VerifyToken 验证 JWT 令牌并返回用户信息 +func VerifyToken(tokenStr string) (*models.Account, error) { // 修改返回类型为 *models.Account + token, err := jwt.ParseWithClaims(tokenStr, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } + return jwtSecret, nil + }) + if err != nil { + return nil, err } - return nil, err + + if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { + return models.GetAccountByID(claims.UserID) // 假设 models 包新增 GetAccountByID 方法 + } + return nil, errors.New("invalid token") } -// 断言设定ctx的当前用户 -func AssertUser(ctx *gin.Context) *models.Account { - currentUser, isExists := ctx.Get("CurrentUser") - if !isExists { - return nil - } - user, ok := currentUser.(*models.Account) - if ok { - return user - } else { - return nil +// AuthRequired Gin 中间件:验证 JWT 令牌 +func AuthRequired() gin.HandlerFunc { + return func(c *gin.Context) { + // 从请求头获取 Authorization 字段(格式:Bearer ) + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "请提供认证令牌"}) + c.Abort() + return + } + + // 解析令牌 + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "令牌格式错误(应为 Bearer )"}) + c.Abort() + return + } + + // 验证令牌并获取用户 + user, err := VerifyToken(parts[1]) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "无效或过期的令牌: " + err.Error()}) + c.Abort() + return + } + + // 将用户信息存入上下文(后续路由可通过 c.Get("user") 获取) + c.Set("user", user) + c.Next() } } diff --git a/routers/router.go b/routers/router.go index a457ff0..cdb16f9 100644 --- a/routers/router.go +++ b/routers/router.go @@ -5,8 +5,10 @@ import ( "go_blog/controllers" "go_blog/models" "go_blog/serializers" + "go_blog/themes" // <-- 确保导入 themes 包 "log" "net/http" + "strconv" "time" "github.com/gin-gonic/gin" @@ -19,32 +21,87 @@ func RegisterRoutes(r *gin.Engine) { r.GET("/events", esSSE) r.GET("/page", func(c *gin.Context) { + tm, exists := c.Get("ThemeManager") + if !exists { + c.String(http.StatusInternalServerError, "Theme manager not found") + return + } + themeManager, ok := tm.(*themes.ThemeManager) + if !ok { + c.String(http.StatusInternalServerError, "Invalid theme manager type") + return + } + var items []models.Content var pager serializers.Pager - pager.InitPager(c) + pager.InitPager(c) // 这会从查询参数中读取 page 和 pageSize offset := (pager.Page - 1) * pager.PageSize - 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 { + + dbInterface, dbOk := c.Get("DB") + if !dbOk { log.Println("未找到键 'DB' 的上下文值") c.JSON(http.StatusInternalServerError, gin.H{"error": "内部服务器错误"}) return } + db, typeOk := dbInterface.(*gorm.DB) + if !typeOk { + log.Println("无法将 DB 转换为 *gorm.DB 类型") + c.JSON(http.StatusInternalServerError, gin.H{"error": "内部服务器错误"}) + return + } - c.HTML(http.StatusOK, "index.tmpl", gin.H{ + db.Select("*").Offset(offset).Limit(pager.PageSize).Find(&items, "type = ?", "post") + var totalCount int64 + db.Model(&models.Content{}).Where("type = ?", "post").Count(&totalCount) + pager.Total = int(totalCount) // 确保 Pager 结构体中的 Total 类型匹配 + + tpl := themeManager.GetTemplate("index") // 假设分页列表也使用 index 模板 + if tpl == nil { + c.String(http.StatusInternalServerError, "Template 'index' not found in current theme: "+themeManager.CurrentTheme()) + return + } + + c.Status(http.StatusOK) + c.Header("Content-Type", "text/html; charset=utf-8") + err := tpl.Execute(c.Writer, gin.H{ "Items": items, - "Pager": pager, - "Title": "文章列表", + "Pager": pager, // 确保你的 index.tmpl 支持 Pager 结构 + "Title": "文章列表 - 第 " + strconv.Itoa(pager.Page) + " 页", }) + if err != nil { + c.String(http.StatusInternalServerError, "Error rendering template: "+err.Error()) + } }) + r.GET("/createcontent", func(c *gin.Context) { - c.HTML(http.StatusOK, "content.tmpl", nil) + tm, exists := c.Get("ThemeManager") + if !exists { + c.String(http.StatusInternalServerError, "Theme manager not found") + return + } + themeManager, ok := tm.(*themes.ThemeManager) + if !ok { + c.String(http.StatusInternalServerError, "Invalid theme manager type") + return + } + + // 假设你的主题中有一个名为 "content" 的模板 (content.html 或 content.tmpl) + // 注意:当前的 default 主题并没有 content.tmpl,你需要添加它。 + // 如果这个页面是后台页面,它可能不需要主题化,或者使用一个固定的后台模板。 + tpl := themeManager.GetTemplate("content") + if tpl == nil { + c.String(http.StatusInternalServerError, "Template 'content' not found in current theme: "+themeManager.CurrentTheme()+". Make sure 'content.html' or 'content.tmpl' exists.") + return + } + + c.Status(http.StatusOK) + c.Header("Content-Type", "text/html; charset=utf-8") + err := tpl.Execute(c.Writer, gin.H{ + "Title": "创建新内容", + }) + if err != nil { + c.String(http.StatusInternalServerError, "Error rendering template: "+err.Error()) + } }) r.GET("/ws", controllers.WebSocketHandler) @@ -57,12 +114,12 @@ func RegisterRoutes(r *gin.Engine) { r.GET("/", controllers.Home) r.GET("/post/:id", controllers.ShowPost) - // // Admin panel - // admin := r.Group("/admin", middleware.AuthRequired()) - // { - // admin.GET("/themes", controllers.ListThemes) - // admin.POST("/themes/switch", controllers.SwitchTheme) - // } + // Admin panel(取消注释并完善) + admin := r.Group("/admin", jwt.AuthRequired()) // 新增中间件 + { + admin.GET("/themes", controllers.ListThemes) + admin.POST("/themes/switch", controllers.SwitchTheme) + } // Static files for themes r.StaticFS("/themes", http.Dir("web/themes")) diff --git a/templates/admin/themes.tmpl b/templates/admin/themes.tmpl new file mode 100644 index 0000000..036865d --- /dev/null +++ b/templates/admin/themes.tmpl @@ -0,0 +1,29 @@ + + + + 主题管理 + + + +
+

主题管理

+

当前主题:{{.CurrentTheme}}

+ +
+

可选主题

+
+ + +
+
+
+ + \ No newline at end of file diff --git a/themes/manager.go b/themes/manager.go index 46fbd49..b98a9f8 100644 --- a/themes/manager.go +++ b/themes/manager.go @@ -39,6 +39,12 @@ func NewManager(themesDir string) *ThemeManager { } } +// 自定义错误类型 +var ( + ErrInvalidThemeStructure = errors.New("invalid theme structure") + ErrNoValidTemplates = errors.New("no valid templates found") +) + // 核心方法:加载主题 func (m *ThemeManager) LoadTheme(themeName string) error { m.mu.Lock() @@ -47,7 +53,7 @@ func (m *ThemeManager) LoadTheme(themeName string) error { // 1. 验证主题目录结构 themePath := filepath.Join(m.themesDir, themeName) if !isValidTheme(themePath) { - return fmt.Errorf("invalid theme structure: %s", themeName) + return fmt.Errorf("%w: %s", ErrInvalidThemeStructure, themeName) } // 2. 加载模板文件 @@ -85,21 +91,50 @@ func (m *ThemeManager) RegisterStaticRoutes(router *gin.Engine) { // 校验主题完整性 func isValidTheme(themePath string) bool { - requiredFiles := []string{ + requiredBaseFiles := []string{ // 不带扩展名的基本文件名 "theme.yaml", - // "templates/home.html", - // "templates/post.html", - "templates/index.html", + "templates/index", // 例如: templates/index.html 或 templates/index.tmpl + // "templates/post", // 如果 post 模板是必须的,也加入这里 } + supportedTemplateExtensions := []string{".html", ".tmpl"} - for _, f := range requiredFiles { - if _, err := os.Stat(filepath.Join(themePath, f)); os.IsNotExist(err) { + for _, baseFile := range requiredBaseFiles { + found := false + if strings.HasPrefix(baseFile, "templates/") { + // 对于模板文件,尝试所有支持的扩展名 + for _, ext := range supportedTemplateExtensions { + if exists, _ := fileExists(filepath.Join(themePath, baseFile+ext)); exists { + found = true + break + } + } + } else { + // 对于非模板文件 (如 theme.yaml),直接检查 + if exists, _ := fileExists(filepath.Join(themePath, baseFile)); exists { + found = true + } + } + + if !found { + // log.Printf("Required file/template %s not found in theme %s", baseFile, themePath) // 可选的调试日志 return false } } return true } +// 检查文件是否存在 +func fileExists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + // 解析模板文件(支持布局继承) func (m *ThemeManager) parseTemplates(themePath string) (map[string]*template.Template, error) { templates := make(map[string]*template.Template) @@ -112,25 +147,37 @@ func (m *ThemeManager) parseTemplates(themePath string) (map[string]*template.Te // 从主题目录加载模板 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 + if err != nil { + return err // 返回错误以停止 WalkDir + } + if d.IsDir() { + return nil // 跳过目录 + } + + // 支持 .html 和 .tmpl 扩展名 + lowerPath := strings.ToLower(path) + if !strings.HasSuffix(lowerPath, ".html") && !strings.HasSuffix(lowerPath, ".tmpl") { + return nil // 非模板文件,跳过 } // 读取模板内容 - content, err := os.ReadFile(path) + content, err := readFile(path) if err != nil { - return err + return fmt.Errorf("failed to read template file %s: %w", path, err) } - // 生成模板名称(相对路径) + // 生成模板名称(相对路径,不含扩展名) name := strings.TrimPrefix(path, tplDir+string(filepath.Separator)) name = strings.TrimSuffix(name, filepath.Ext(name)) // 克隆基础模板并解析 - tpl := template.Must(baseTpl.Clone()) + tpl, err := baseTpl.Clone() + if err != nil { + return fmt.Errorf("failed to clone base template for %s: %w", name, err) + } tpl, err = tpl.Parse(string(content)) if err != nil { - return fmt.Errorf("parse error in %s: %w", name, err) + return fmt.Errorf("parse error in %s (file: %s): %w", name, path, err) } templates[name] = tpl @@ -142,12 +189,17 @@ func (m *ThemeManager) parseTemplates(themePath string) (map[string]*template.Te } if len(templates) == 0 { - return nil, errors.New("no valid templates found") + return nil, ErrNoValidTemplates } return templates, nil } +// 读取文件内容 +func readFile(path string) ([]byte, error) { + return os.ReadFile(path) +} + // 获取可用主题列表 func (m *ThemeManager) ListThemes() ([]string, error) { dirs, err := os.ReadDir(m.themesDir) diff --git a/themes/parser.go b/themes/parser.go index 4e9279a..647198f 100644 --- a/themes/parser.go +++ b/themes/parser.go @@ -21,14 +21,25 @@ func RenderTemplate(c *gin.Context, tmpl string, data gin.H) { } // SwitchTheme handles POST requests to switch the current theme +// SwitchTheme 处理主题切换请求(修正后) func SwitchTheme(c *gin.Context) { - newTheme := c.PostForm("theme") - manager := &ThemeManager{} //themes.GetManager() + // 从上下文中获取主题管理器(通过 main.go 的 themeMiddleware 注入) + tm, exists := c.Get("ThemeManager") + if !exists { + c.JSON(http.StatusInternalServerError, gin.H{"error": "主题管理器未找到"}) + return + } + themeManager, ok := tm.(*themes.ThemeManager) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{"error": "主题管理器类型错误"}) + return + } - if err := manager.LoadTheme(newTheme); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - } else { - config.SetCurrentTheme(newTheme) // Update the configuration - c.JSON(http.StatusOK, gin.H{"status": "success"}) - } + newTheme := c.PostForm("theme") + if err := themeManager.LoadTheme(newTheme); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "加载主题失败: " + err.Error()}) + return + } + config.SetCurrentTheme(newTheme) // 持久化主题配置(需确保 config 包支持) + c.JSON(http.StatusOK, gin.H{"status": "主题切换成功"}) } diff --git a/web/themes/default/templates/post.tmpl b/web/themes/default/templates/post.tmpl new file mode 100644 index 0000000..9b53295 --- /dev/null +++ b/web/themes/default/templates/post.tmpl @@ -0,0 +1,64 @@ + + + + Page 1 + + + + + + +
+
+
+

{{.Title}}

+
+ {{.AuthorID}} + {{.Created}} + {{.CommentsNum}}条 +
+
+
+ {{.Text}} +
+
+ 标签:{{.Type}} +
+
+
+

非特殊说明,本博所有文章均为博主原创。

+ +
+
+
+
+ 上一篇 +
+
+ 下一篇 +
+
+
+
+
+
+ +