优化router、main等文件。
This commit is contained in:
+4
-2
@@ -19,8 +19,9 @@ type DatabaseConfig struct {
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int
|
||||
Mode string
|
||||
Port int
|
||||
Mode string
|
||||
LogLevel string // 日志级别:debug/info/warn/error,默认 info
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
@@ -70,6 +71,7 @@ func Load() *Config {
|
||||
func setDefaults() {
|
||||
viper.SetDefault("server.port", 8080)
|
||||
viper.SetDefault("server.mode", "debug")
|
||||
viper.SetDefault("server.log_level", "info")
|
||||
viper.SetDefault("database.driver", "sqlite")
|
||||
viper.SetDefault("database.dsn", "goblog.db")
|
||||
viper.SetDefault("app.name", "GoBlog")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
server:
|
||||
port: 8080
|
||||
mode: debug # debug 或 release
|
||||
log_level: info # info, warn, error
|
||||
|
||||
database:
|
||||
driver: sqlite # sqlite 或 mysql
|
||||
|
||||
@@ -62,7 +62,7 @@ func loginInfo(c *gin.Context) (isLoggedIn bool, loginName string) {
|
||||
return true, loginName
|
||||
}
|
||||
|
||||
// 首页
|
||||
// 首页数据结构
|
||||
type IndexData struct {
|
||||
Title string
|
||||
SiteName string
|
||||
@@ -84,6 +84,7 @@ type IndexData struct {
|
||||
LoginName string
|
||||
}
|
||||
|
||||
// 首页(供模板使用)
|
||||
func IndexView(c *gin.Context) {
|
||||
cfg := getConfig()
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
@@ -94,6 +95,7 @@ func IndexView(c *gin.Context) {
|
||||
|
||||
categoryID, _ := strconv.Atoi(c.Query("category"))
|
||||
tagID, _ := strconv.Atoi(c.Query("tag"))
|
||||
authorID, _ := strconv.Atoi(c.Query("author"))
|
||||
|
||||
db := database.DB.Model(&models.Post{}).Preload("Category").Preload("Tags").Preload("Author").
|
||||
Where("status = ?", "published")
|
||||
@@ -105,6 +107,9 @@ func IndexView(c *gin.Context) {
|
||||
db = db.Joins("JOIN post_tags ON post_tags.post_id = posts.id").
|
||||
Where("post_tags.tag_id = ?", tagID)
|
||||
}
|
||||
if authorID > 0 {
|
||||
db = db.Where("author_id = ?", authorID)
|
||||
}
|
||||
|
||||
var total int64
|
||||
db.Count(&total)
|
||||
@@ -169,6 +174,7 @@ type PostDetailData struct {
|
||||
LoginName string
|
||||
}
|
||||
|
||||
// 文章详情页(供模板使用)
|
||||
func PostView(c *gin.Context) {
|
||||
cfg := getConfig()
|
||||
slug := c.Param("slug")
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"goblog/config"
|
||||
"goblog/database"
|
||||
"goblog/models"
|
||||
"goblog/routers"
|
||||
"goblog/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -19,10 +28,8 @@ func main() {
|
||||
// 设置 Gin 模式
|
||||
gin.SetMode(cfg.Server.Mode)
|
||||
|
||||
// 初始化日志系统
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
// 初始化日志系统(使用 Server.LogLevel)
|
||||
logger := newLogger(cfg.Server.LogLevel)
|
||||
slog.SetDefault(logger)
|
||||
|
||||
slog.Info("启动博客系统",
|
||||
@@ -30,6 +37,8 @@ func main() {
|
||||
"theme", cfg.App.Theme,
|
||||
"db_driver", cfg.Database.Driver,
|
||||
"port", cfg.Server.Port,
|
||||
"mode", cfg.Server.Mode,
|
||||
"log_level", cfg.Server.LogLevel,
|
||||
)
|
||||
|
||||
// 初始化数据库
|
||||
@@ -37,31 +46,105 @@ func main() {
|
||||
slog.Error("数据库初始化失败", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// defer database.Close() // 如果提供
|
||||
|
||||
// 从数据库读取主题设置,如果不存在则使用配置文件中的值
|
||||
theme := cfg.App.Theme
|
||||
var option models.Option
|
||||
if err := database.DB.Where("name = ? AND user_id = ?", "theme", 0).First(&option).Error; err == nil {
|
||||
theme = option.Value
|
||||
slog.Info("从数据库加载主题", "theme", theme)
|
||||
} else {
|
||||
slog.Info("使用配置文件中的主题", "theme", theme)
|
||||
// 从数据库加载主题(若不存在则使用配置值)
|
||||
theme, err := loadTheme(cfg.App.Theme)
|
||||
if err != nil {
|
||||
slog.Error("加载主题失败", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 创建路由引擎
|
||||
r := gin.Default()
|
||||
// 创建路由引擎(使用自定义 slog 中间件)
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(slogMiddleware(logger))
|
||||
|
||||
// 配置模板渲染器
|
||||
routers.SetupTemplates(r, theme)
|
||||
// 配置模板渲染器(现在 LoadTemplates 返回 error)
|
||||
htmlRenderer, err := utils.LoadTemplates(theme)
|
||||
if err != nil {
|
||||
slog.Error("加载模板失败", "error", err, "theme", theme)
|
||||
os.Exit(1)
|
||||
}
|
||||
utils.SetGlobalRenderer(htmlRenderer)
|
||||
r.HTMLRender = htmlRenderer
|
||||
|
||||
// 配置所有路由
|
||||
routers.SetupRoutes(r)
|
||||
|
||||
// 启动服务器
|
||||
port := routers.GetServerPort(cfg.Server.Port)
|
||||
slog.Info("服务器启动成功", "port", port, "mode", cfg.Server.Mode)
|
||||
if err := r.Run(port); err != nil {
|
||||
slog.Error("服务器启动失败", "error", err)
|
||||
os.Exit(1)
|
||||
// 启动服务器(优雅关闭)
|
||||
runServer(r, cfg.Server.Port)
|
||||
}
|
||||
|
||||
// newLogger 根据配置字符串创建 slog.Logger
|
||||
func newLogger(level string) *slog.Logger {
|
||||
var lvl slog.Level
|
||||
if err := lvl.UnmarshalText([]byte(level)); err != nil {
|
||||
lvl = slog.LevelInfo // 默认 info
|
||||
}
|
||||
opts := &slog.HandlerOptions{Level: lvl}
|
||||
return slog.New(slog.NewTextHandler(os.Stdout, opts))
|
||||
}
|
||||
|
||||
// loadTheme 从数据库读取系统主题,若不存在则使用 fallback
|
||||
func loadTheme(fallback string) (string, error) {
|
||||
var option models.Option
|
||||
err := database.DB.Where("name = ? AND user_id = ?", "theme", 0).First(&option).Error
|
||||
if err == nil {
|
||||
slog.Info("从数据库加载主题", "theme", option.Value)
|
||||
return option.Value, nil
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
slog.Info("使用配置文件中的主题", "theme", fallback)
|
||||
return fallback, nil
|
||||
}
|
||||
return "", err // 真实错误必须返回
|
||||
}
|
||||
|
||||
// slogMiddleware 自定义请求日志中间件
|
||||
func slogMiddleware(logger *slog.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
|
||||
c.Next()
|
||||
|
||||
logger.Info("HTTP请求",
|
||||
"method", c.Request.Method,
|
||||
"path", path,
|
||||
"status", c.Writer.Status(),
|
||||
"duration", time.Since(start),
|
||||
"client_ip", c.ClientIP(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// runServer 启动 HTTP 服务并支持优雅关闭
|
||||
func runServer(r *gin.Engine, port int) {
|
||||
addr := ":" + strconv.Itoa(port)
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
go func() {
|
||||
slog.Info("服务器启动成功", "addr", addr)
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
slog.Error("服务器启动失败", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
slog.Info("正在关闭服务器...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
slog.Error("服务器强制关闭", "error", err)
|
||||
} else {
|
||||
slog.Info("服务器已优雅关闭")
|
||||
}
|
||||
}
|
||||
|
||||
+156
-126
@@ -1,151 +1,181 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"goblog/handlers/backend"
|
||||
"goblog/handlers/frontend"
|
||||
"goblog/middleware"
|
||||
"goblog/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 常量定义
|
||||
const (
|
||||
staticPath = "/static"
|
||||
adminStaticPath = "/admin/static"
|
||||
adminPath = "/admin"
|
||||
apiPrefix = "/api"
|
||||
)
|
||||
|
||||
// SetupRoutes 配置所有路由
|
||||
func SetupRoutes(r *gin.Engine) {
|
||||
// 静态文件
|
||||
r.Static("/static", "./static")
|
||||
setupStaticRoutes(r)
|
||||
setupAdminPageRoutes(r)
|
||||
setupFrontendRoutes(r)
|
||||
setupAPIRoutes(r)
|
||||
}
|
||||
|
||||
// 后台管理(放在前面避免冲突)
|
||||
r.Static("/admin/static", "./static/admin")
|
||||
// 独立登录页
|
||||
r.GET("/admin/login", backend.AdminLoginView)
|
||||
// 后台首页:可选认证中间件注入用户信息,服务端按角色渲染菜单与页面框架
|
||||
r.GET("/admin", middleware.OptionalAuth(), backend.AdminView)
|
||||
// setupStaticRoutes 配置静态资源路由
|
||||
func setupStaticRoutes(r *gin.Engine) {
|
||||
r.Static(staticPath, "./static")
|
||||
r.Static(adminStaticPath, "./static/admin")
|
||||
}
|
||||
|
||||
// 前端页面路由(可选认证:侧边栏根据登录态显示系统菜单)
|
||||
// setupAdminPageRoutes 配置后台管理页面路由(服务端渲染)
|
||||
func setupAdminPageRoutes(r *gin.Engine) {
|
||||
r.GET(adminPath+"/login", backend.AdminLoginView)
|
||||
r.GET(adminPath, middleware.OptionalAuth(), backend.AdminView)
|
||||
}
|
||||
|
||||
// setupFrontendRoutes 配置前台页面路由
|
||||
func setupFrontendRoutes(r *gin.Engine) {
|
||||
r.GET("/", middleware.OptionalAuth(), frontend.IndexView)
|
||||
r.GET("/post/:slug", middleware.OptionalAuth(), frontend.PostView)
|
||||
r.GET("/page/:slug", middleware.OptionalAuth(), frontend.PageView)
|
||||
r.GET("/search", middleware.OptionalAuth(), frontend.SearchView)
|
||||
r.POST("/comment", frontend.SubmitComment)
|
||||
// 前台退出登录:清除 Cookie 后回首页
|
||||
r.GET("/logout", backend.WebLogout)
|
||||
|
||||
// API 路由组
|
||||
api := r.Group("/api")
|
||||
r.POST("/comment", frontend.SubmitComment) // 前台提交评论(公开)
|
||||
r.GET("/logout", backend.WebLogout) // 前台退出登录
|
||||
}
|
||||
|
||||
// setupAPIRoutes 配置所有 /api 下的接口路由
|
||||
func setupAPIRoutes(r *gin.Engine) {
|
||||
api := r.Group(apiPrefix)
|
||||
|
||||
// 公开接口:无需认证或可选认证
|
||||
setupPublicAPIRoutes(api)
|
||||
|
||||
// 需要认证的接口(JWT)
|
||||
setupAuthenticatedAPIRoutes(api)
|
||||
|
||||
// 管理员专属接口(JWT + 管理员角色)
|
||||
setupAdminAPIRoutes(api)
|
||||
}
|
||||
|
||||
// setupPublicAPIRoutes 配置公开 API(无需认证或可选认证)
|
||||
func setupPublicAPIRoutes(api *gin.RouterGroup) {
|
||||
// 文章
|
||||
api.GET("/posts", middleware.OptionalAuth(), backend.GetPosts)
|
||||
api.GET("/posts/:id", middleware.OptionalAuth(), backend.GetPost)
|
||||
|
||||
// 分类
|
||||
api.GET("/categories", backend.GetCategories)
|
||||
api.GET("/categories/:id", backend.GetCategory)
|
||||
|
||||
// 标签
|
||||
api.GET("/tags", backend.GetTags)
|
||||
api.GET("/tags/:id", backend.GetTag)
|
||||
|
||||
// 页面
|
||||
api.GET("/pages", backend.GetPages)
|
||||
api.GET("/pages/:id", backend.GetPage)
|
||||
|
||||
// 评论列表(公开,仅显示已批准评论)
|
||||
api.GET("/comments", backend.GetComments)
|
||||
|
||||
// 创建评论(可选认证,若登录则关联用户)
|
||||
api.POST("/comments", middleware.OptionalAuth(), backend.CreateComment)
|
||||
|
||||
// 登录(公开)
|
||||
api.POST("/auth/login", backend.Login)
|
||||
|
||||
// 仪表盘统计(可选认证,未登录返回公开统计,登录返回更多数据)
|
||||
api.GET("/dashboard/stats", middleware.OptionalAuth(), backend.GetDashboardStats)
|
||||
|
||||
// 主题公开接口
|
||||
api.GET("/themes", backend.GetThemes)
|
||||
api.GET("/theme", backend.GetCurrentTheme)
|
||||
}
|
||||
|
||||
// setupAuthenticatedAPIRoutes 配置需要登录的 API(JWT 认证)
|
||||
func setupAuthenticatedAPIRoutes(api *gin.RouterGroup) {
|
||||
auth := api.Group("/")
|
||||
auth.Use(middleware.JWTAuth())
|
||||
|
||||
// 当前用户信息与修改
|
||||
auth.GET("/auth/me", backend.GetCurrentUser)
|
||||
auth.PUT("/auth/me", backend.UpdateUser)
|
||||
auth.PUT("/auth/password", backend.ChangePassword)
|
||||
|
||||
// 登出
|
||||
auth.POST("/auth/logout", backend.Logout)
|
||||
|
||||
// 文章管理
|
||||
auth.POST("/posts", backend.CreatePost)
|
||||
auth.PUT("/posts/:id", backend.UpdatePost)
|
||||
auth.DELETE("/posts/:id", backend.DeletePost)
|
||||
|
||||
// Markdown 预览
|
||||
auth.POST("/markdown/preview", backend.PreviewMarkdown)
|
||||
|
||||
// 文件上传
|
||||
auth.POST("/upload", backend.UploadImage)
|
||||
|
||||
// 我的评论管理(管理自己文章下的评论)
|
||||
auth.GET("/my/comments", backend.GetComments)
|
||||
auth.PUT("/my/comments/:id/approve", backend.ApproveComment)
|
||||
auth.PUT("/my/comments/:id/spam", backend.MarkSpamComment)
|
||||
auth.DELETE("/my/comments/:id", backend.DeleteComment)
|
||||
|
||||
// 主题管理(认证用户可切换主题、创建主题、编辑主题文件)
|
||||
themeAuth := auth.Group("/")
|
||||
{
|
||||
// 公开 API(支持可选认证,以便管理员查看所有文章)
|
||||
api.GET("/posts", middleware.OptionalAuth(), backend.GetPosts)
|
||||
api.GET("/dashboard/stats", middleware.OptionalAuth(), backend.GetDashboardStats)
|
||||
api.GET("/posts/:id", middleware.OptionalAuth(), backend.GetPost)
|
||||
api.GET("/categories", backend.GetCategories)
|
||||
api.GET("/categories/:id", backend.GetCategory)
|
||||
api.GET("/tags", backend.GetTags)
|
||||
api.GET("/tags/:id", backend.GetTag)
|
||||
api.GET("/pages", backend.GetPages)
|
||||
api.GET("/pages/:id", backend.GetPage)
|
||||
api.GET("/comments", backend.GetComments)
|
||||
|
||||
// 需要认证的 API
|
||||
api.POST("/comments", middleware.OptionalAuth(), backend.CreateComment)
|
||||
|
||||
// 用户认证
|
||||
api.POST("/auth/login", backend.Login)
|
||||
api.POST("/auth/logout", backend.Logout)
|
||||
|
||||
// 需要登录的 API
|
||||
auth := api.Group("/")
|
||||
auth.Use(middleware.JWTAuth())
|
||||
{
|
||||
auth.GET("/auth/me", backend.GetCurrentUser)
|
||||
|
||||
// 仪表盘统计(所有登录用户可访问,handler 内部按角色过滤数据)
|
||||
auth.GET("/dashboard", middleware.OptionalAuth(), backend.GetDashboardStats)
|
||||
auth.PUT("/auth/me", backend.UpdateUser)
|
||||
auth.PUT("/auth/password", backend.ChangePassword)
|
||||
|
||||
// 文章管理(所有登录用户可新建文章,修改/删除自己的文章)
|
||||
auth.POST("/posts", backend.CreatePost)
|
||||
auth.PUT("/posts/:id", backend.UpdatePost)
|
||||
auth.DELETE("/posts/:id", backend.DeletePost)
|
||||
|
||||
// Markdown 预览(文章编辑器用)
|
||||
auth.POST("/markdown/preview", backend.PreviewMarkdown)
|
||||
|
||||
// 文件上传(文章编辑器封面图等)
|
||||
auth.POST("/upload", backend.UploadImage)
|
||||
|
||||
// 评论管理(登录用户管理自己文章的评论)
|
||||
auth.GET("/my/comments", backend.GetComments)
|
||||
auth.PUT("/my/comments/:id/approve", backend.ApproveComment)
|
||||
auth.PUT("/my/comments/:id/spam", backend.MarkSpamComment)
|
||||
auth.DELETE("/my/comments/:id", backend.DeleteComment)
|
||||
}
|
||||
|
||||
// 主题管理(可选认证)
|
||||
api.GET("/themes", backend.GetThemes)
|
||||
api.GET("/theme", backend.GetCurrentTheme)
|
||||
api.POST("/theme", middleware.JWTAuth(), backend.SwitchTheme)
|
||||
api.POST("/themes/create", middleware.JWTAuth(), backend.CreateTheme)
|
||||
api.GET("/themes/:theme/files", middleware.JWTAuth(), backend.GetThemeFiles)
|
||||
api.GET("/themes/:theme/files/:file", middleware.JWTAuth(), backend.GetThemeFile)
|
||||
api.PUT("/themes/:theme/files/:file", middleware.JWTAuth(), backend.SaveThemeFile)
|
||||
|
||||
// 管理员 API
|
||||
admin := api.Group("/admin")
|
||||
admin.Use(middleware.JWTAuth(), middleware.AdminRequired())
|
||||
{
|
||||
// Markdown 预览(文章编辑器用)已移至登录组
|
||||
|
||||
// 分类管理
|
||||
admin.POST("/categories", backend.CreateCategory)
|
||||
admin.PUT("/categories/:id", backend.UpdateCategory)
|
||||
admin.DELETE("/categories/:id", backend.DeleteCategory)
|
||||
|
||||
// 标签管理
|
||||
admin.POST("/tags", backend.CreateTag)
|
||||
admin.PUT("/tags/:id", backend.UpdateTag)
|
||||
admin.DELETE("/tags/:id", backend.DeleteTag)
|
||||
|
||||
// 页面管理
|
||||
admin.POST("/pages", backend.CreatePage)
|
||||
admin.PUT("/pages/:id", backend.UpdatePage)
|
||||
admin.DELETE("/pages/:id", backend.DeletePage)
|
||||
|
||||
// 评论管理
|
||||
admin.GET("/comments", backend.GetComments)
|
||||
admin.PUT("/comments/:id/approve", backend.ApproveComment)
|
||||
admin.PUT("/comments/:id/spam", backend.MarkSpamComment)
|
||||
admin.DELETE("/comments/:id", backend.DeleteComment)
|
||||
|
||||
// 用户管理
|
||||
admin.GET("/users", backend.GetUsers)
|
||||
admin.POST("/users", backend.CreateUser)
|
||||
admin.PUT("/users/:id", backend.AdminUpdateUser)
|
||||
admin.PUT("/users/:id/password", backend.AdminResetPassword)
|
||||
admin.DELETE("/users/:id", backend.DeleteUser)
|
||||
|
||||
// 基础设置
|
||||
admin.GET("/settings", backend.GetSettings)
|
||||
admin.PUT("/settings", backend.UpdateSettings)
|
||||
|
||||
// 文件管理(图片上传)
|
||||
admin.POST("/upload", backend.UploadImage)
|
||||
admin.GET("/uploads", backend.ListUploads)
|
||||
admin.DELETE("/uploads/:name", backend.DeleteUpload)
|
||||
}
|
||||
themeAuth.POST("/theme", backend.SwitchTheme)
|
||||
themeAuth.POST("/themes/create", backend.CreateTheme)
|
||||
themeAuth.GET("/themes/:theme/files", backend.GetThemeFiles)
|
||||
themeAuth.GET("/themes/:theme/files/:file", backend.GetThemeFile)
|
||||
themeAuth.PUT("/themes/:theme/files/:file", backend.SaveThemeFile)
|
||||
}
|
||||
}
|
||||
|
||||
// SetupTemplates 配置模板渲染器
|
||||
func SetupTemplates(r *gin.Engine, theme string) {
|
||||
htmlRenderer := utils.LoadTemplates(theme)
|
||||
utils.SetGlobalRenderer(htmlRenderer)
|
||||
r.HTMLRender = htmlRenderer
|
||||
}
|
||||
// setupAdminAPIRoutes 配置管理员专属 API(需要登录 + 管理员角色)
|
||||
func setupAdminAPIRoutes(api *gin.RouterGroup) {
|
||||
admin := api.Group("/admin")
|
||||
admin.Use(middleware.JWTAuth(), middleware.AdminRequired())
|
||||
|
||||
// GetServerPort 获取服务器端口
|
||||
func GetServerPort(port int) string {
|
||||
return ":" + strconv.Itoa(port)
|
||||
// 分类管理
|
||||
admin.POST("/categories", backend.CreateCategory)
|
||||
admin.PUT("/categories/:id", backend.UpdateCategory)
|
||||
admin.DELETE("/categories/:id", backend.DeleteCategory)
|
||||
|
||||
// 标签管理
|
||||
admin.POST("/tags", backend.CreateTag)
|
||||
admin.PUT("/tags/:id", backend.UpdateTag)
|
||||
admin.DELETE("/tags/:id", backend.DeleteTag)
|
||||
|
||||
// 页面管理
|
||||
admin.POST("/pages", backend.CreatePage)
|
||||
admin.PUT("/pages/:id", backend.UpdatePage)
|
||||
admin.DELETE("/pages/:id", backend.DeletePage)
|
||||
|
||||
// 评论管理(所有评论)
|
||||
admin.GET("/comments", backend.GetComments)
|
||||
admin.PUT("/comments/:id/approve", backend.ApproveComment)
|
||||
admin.PUT("/comments/:id/spam", backend.MarkSpamComment)
|
||||
admin.DELETE("/comments/:id", backend.DeleteComment)
|
||||
|
||||
// 用户管理
|
||||
admin.GET("/users", backend.GetUsers)
|
||||
admin.POST("/users", backend.CreateUser)
|
||||
admin.PUT("/users/:id", backend.AdminUpdateUser)
|
||||
admin.PUT("/users/:id/password", backend.AdminResetPassword)
|
||||
admin.DELETE("/users/:id", backend.DeleteUser)
|
||||
|
||||
// 基础设置
|
||||
admin.GET("/settings", backend.GetSettings)
|
||||
admin.PUT("/settings", backend.UpdateSettings)
|
||||
|
||||
// 文件管理
|
||||
admin.POST("/upload", backend.UploadImage)
|
||||
admin.GET("/uploads", backend.ListUploads)
|
||||
admin.DELETE("/uploads/:name", backend.DeleteUpload)
|
||||
}
|
||||
|
||||
+30
-29
@@ -67,12 +67,14 @@ func (r *htmlRender) WriteContentType(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
// LoadTemplates 加载指定主题的模板
|
||||
func LoadTemplates(theme string) *TemplateRenderer {
|
||||
func LoadTemplates(theme string) (*TemplateRenderer, error) {
|
||||
r := &TemplateRenderer{
|
||||
templates: make(map[string]*template.Template),
|
||||
}
|
||||
r.reload(theme)
|
||||
return r
|
||||
if err := r.reload(theme); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Reload 重新加载主题模板(线程安全)
|
||||
@@ -83,34 +85,35 @@ func (r *TemplateRenderer) Reload(theme string) {
|
||||
}
|
||||
|
||||
// reload 内部重载方法(需要持有锁)
|
||||
func (r *TemplateRenderer) reload(theme string) {
|
||||
func (r *TemplateRenderer) reload(theme string) error {
|
||||
templateDir := "templates/" + theme
|
||||
|
||||
// 需要独立渲染的页面模板
|
||||
pages := []string{"index", "post", "page"}
|
||||
|
||||
// 收集主题目录下的公共局部模板(例如 sidebar/header/footer),
|
||||
// 这些模板通过 {{template "xxx" .}} 被 base.html 引用,
|
||||
// 必须与页面模板一起解析,否则渲染时会因模板未定义而失败(白屏)。
|
||||
// 收集主题目录下的公共局部模板
|
||||
partials := []string{}
|
||||
if matches, err := filepath.Glob(templateDir + "/*.html"); err == nil {
|
||||
for _, f := range matches {
|
||||
base := filepath.Base(f)
|
||||
if base == "base.html" {
|
||||
continue
|
||||
}
|
||||
name := base[:len(base)-len(".html")]
|
||||
isPage := false
|
||||
for _, p := range pages {
|
||||
if p == name {
|
||||
isPage = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isPage {
|
||||
partials = append(partials, f)
|
||||
matches, err := filepath.Glob(templateDir + "/*.html")
|
||||
if err != nil {
|
||||
return fmt.Errorf("扫描主题目录失败: %w", err)
|
||||
}
|
||||
|
||||
for _, f := range matches {
|
||||
base := filepath.Base(f)
|
||||
if base == "base.html" {
|
||||
continue
|
||||
}
|
||||
name := base[:len(base)-len(".html")]
|
||||
isPage := false
|
||||
for _, p := range pages {
|
||||
if p == name {
|
||||
isPage = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isPage {
|
||||
partials = append(partials, f)
|
||||
}
|
||||
}
|
||||
|
||||
// 为每个页面创建独立的模板组合
|
||||
@@ -125,19 +128,17 @@ func (r *TemplateRenderer) reload(theme string) {
|
||||
)
|
||||
allFiles = append(allFiles, partials...)
|
||||
|
||||
// 解析模板文件
|
||||
// 解析模板文件,遇到错误立即返回
|
||||
tmpl, err := template.New("").Funcs(templateFuncs()).ParseFiles(allFiles...)
|
||||
if err != nil {
|
||||
// 如果解析失败,记录错误但不中断
|
||||
fmt.Printf("警告: 加载主题 %s 的 %s 页面模板失败: %v\n", theme, pageName, err)
|
||||
continue
|
||||
return fmt.Errorf("解析页面模板 %s 失败: %w", pageName, err)
|
||||
}
|
||||
newTemplates[pageName] = tmpl
|
||||
}
|
||||
|
||||
// 更新模板映射
|
||||
// 所有页面模板解析成功后才更新映射
|
||||
r.templates = newTemplates
|
||||
fmt.Printf("主题 %s 加载完成,共加载 %d 个页面模板\n", theme, len(newTemplates))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 全局模板渲染器实例
|
||||
|
||||
Reference in New Issue
Block a user