优化router、main等文件。

This commit is contained in:
2026-08-14 17:59:20 +08:00
parent 0f0850357d
commit f1780de632
6 changed files with 303 additions and 180 deletions
+4 -2
View File
@@ -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")
+1
View File
@@ -2,6 +2,7 @@
server:
port: 8080
mode: debug # debug 或 release
log_level: info # info, warn, error
database:
driver: sqlite # sqlite 或 mysql
+7 -1
View File
@@ -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")
+105 -22
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
// 全局模板渲染器实例