博客系统实战 #
本节将使用Gin框架构建一个完整的博客系统,包括文章管理、分类标签、评论系统等核心功能。
项目结构 #
text
blog-system/
├── main.go
├── config/
│ └── config.go
├── models/
│ ├── user.go
│ ├── article.go
│ ├── category.go
│ ├── tag.go
│ └── comment.go
├── handlers/
│ ├── article_handler.go
│ ├── category_handler.go
│ ├── tag_handler.go
│ └── comment_handler.go
├── services/
│ ├── article_service.go
│ ├── category_service.go
│ ├── tag_service.go
│ └── comment_service.go
├── middleware/
│ ├── auth.go
│ └── response.go
├── utils/
│ ├── pagination.go
│ └── slug.go
└── router/
└── router.go
数据模型 #
用户模型 #
go
// models/user.go
package models
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"uniqueIndex;size:50;not null"`
Email string `json:"email" gorm:"uniqueIndex;size:100;not null"`
Password string `json:"-" gorm:"size:255;not null"`
Nickname string `json:"nickname" gorm:"size:50"`
Avatar string `json:"avatar" gorm:"size:255"`
Bio string `json:"bio" gorm:"size:500"`
Role string `json:"role" gorm:"size:20;default:'user'"`
Status int `json:"status" gorm:"default:1"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
Articles []Article `json:"articles,omitempty" gorm:"foreignKey:AuthorID"`
Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:UserID"`
}
文章模型 #
go
// models/article.go
package models
import (
"time"
"gorm.io/gorm"
)
type Article struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title" gorm:"size:200;not null;index"`
Slug string `json:"slug" gorm:"uniqueIndex;size:200;not null"`
Summary string `json:"summary" gorm:"size:500"`
Content string `json:"content" gorm:"type:text;not null"`
Cover string `json:"cover" gorm:"size:255"`
AuthorID uint `json:"author_id" gorm:"index;not null"`
CategoryID uint `json:"category_id" gorm:"index"`
Status int `json:"status" gorm:"default:0"` // 0: draft, 1: published
ViewCount int `json:"view_count" gorm:"default:0"`
LikeCount int `json:"like_count" gorm:"default:0"`
CommentCount int `json:"comment_count" gorm:"default:0"`
IsTop bool `json:"is_top" gorm:"default:false"`
PublishedAt *time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
Author *User `json:"author,omitempty" gorm:"foreignKey:AuthorID"`
Category *Category `json:"category,omitempty" gorm:"foreignKey:CategoryID"`
Tags []Tag `json:"tags,omitempty" gorm:"many2many:article_tags;"`
Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:ArticleID"`
}
func (Article) TableName() string {
return "articles"
}
type ArticleListResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Summary string `json:"summary"`
Cover string `json:"cover"`
Author *UserBrief `json:"author"`
Category *CategoryBrief `json:"category"`
Tags []TagBrief `json:"tags"`
ViewCount int `json:"view_count"`
LikeCount int `json:"like_count"`
CommentCount int `json:"comment_count"`
IsTop bool `json:"is_top"`
PublishedAt *time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"`
}
type UserBrief struct {
ID uint `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
}
type CategoryBrief struct {
ID uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
}
type TagBrief struct {
ID uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
}
func (a *Article) ToListResponse() *ArticleListResponse {
resp := &ArticleListResponse{
ID: a.ID,
Title: a.Title,
Slug: a.Slug,
Summary: a.Summary,
Cover: a.Cover,
ViewCount: a.ViewCount,
LikeCount: a.LikeCount,
CommentCount: a.CommentCount,
IsTop: a.IsTop,
PublishedAt: a.PublishedAt,
CreatedAt: a.CreatedAt,
}
if a.Author != nil {
resp.Author = &UserBrief{
ID: a.Author.ID,
Username: a.Author.Username,
Nickname: a.Author.Nickname,
Avatar: a.Author.Avatar,
}
}
if a.Category != nil {
resp.Category = &CategoryBrief{
ID: a.Category.ID,
Name: a.Category.Name,
Slug: a.Category.Slug,
}
}
if len(a.Tags) > 0 {
resp.Tags = make([]TagBrief, len(a.Tags))
for i, tag := range a.Tags {
resp.Tags[i] = TagBrief{
ID: tag.ID,
Name: tag.Name,
Slug: tag.Slug,
}
}
}
return resp
}
分类模型 #
go
// models/category.go
package models
import (
"time"
"gorm.io/gorm"
)
type Category struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"uniqueIndex;size:50;not null"`
Slug string `json:"slug" gorm:"uniqueIndex;size:50;not null"`
Description string `json:"description" gorm:"size:200"`
ParentID uint `json:"parent_id" gorm:"default:0;index"`
SortOrder int `json:"sort_order" gorm:"default:0"`
ArticleCount int `json:"article_count" gorm:"default:0"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
Articles []Article `json:"articles,omitempty" gorm:"foreignKey:CategoryID"`
Children []Category `json:"children,omitempty" gorm:"foreignKey:ParentID"`
}
func (Category) TableName() string {
return "categories"
}
标签模型 #
go
// models/tag.go
package models
import (
"time"
"gorm.io/gorm"
)
type Tag struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"uniqueIndex;size:50;not null"`
Slug string `json:"slug" gorm:"uniqueIndex;size:50;not null"`
Description string `json:"description" gorm:"size:200"`
Color string `json:"color" gorm:"size:20"`
ArticleCount int `json:"article_count" gorm:"default:0"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
Articles []Article `json:"articles,omitempty" gorm:"many2many:article_tags;"`
}
func (Tag) TableName() string {
return "tags"
}
评论模型 #
go
// models/comment.go
package models
import (
"time"
"gorm.io/gorm"
)
type Comment struct {
ID uint `json:"id" gorm:"primaryKey"`
Content string `json:"content" gorm:"type:text;not null"`
ArticleID uint `json:"article_id" gorm:"index;not null"`
UserID uint `json:"user_id" gorm:"index;not null"`
ParentID uint `json:"parent_id" gorm:"default:0;index"`
ReplyToID uint `json:"reply_to_id" gorm:"default:0"`
Status int `json:"status" gorm:"default:1"` // 0: hidden, 1: visible
LikeCount int `json:"like_count" gorm:"default:0"`
IP string `json:"-" gorm:"size:50"`
UserAgent string `json:"-" gorm:"size:255"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
Article *Article `json:"article,omitempty" gorm:"foreignKey:ArticleID"`
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Replies []Comment `json:"replies,omitempty" gorm:"foreignKey:ParentID"`
}
func (Comment) TableName() string {
return "comments"
}
type CommentTree struct {
ID uint `json:"id"`
Content string `json:"content"`
User *UserBrief `json:"user"`
LikeCount int `json:"like_count"`
CreatedAt time.Time `json:"created_at"`
Replies []CommentTree `json:"replies"`
}
func BuildCommentTree(comments []Comment) []CommentTree {
var tree []CommentTree
commentMap := make(map[uint]*CommentTree)
for _, c := range comments {
node := &CommentTree{
ID: c.ID,
Content: c.Content,
LikeCount: c.LikeCount,
CreatedAt: c.CreatedAt,
Replies: []CommentTree{},
}
if c.User != nil {
node.User = &UserBrief{
ID: c.User.ID,
Username: c.User.Username,
Nickname: c.User.Nickname,
Avatar: c.User.Avatar,
}
}
commentMap[c.ID] = node
}
for _, c := range comments {
if c.ParentID == 0 {
tree = append(tree, *commentMap[c.ID])
} else {
if parent, ok := commentMap[c.ParentID]; ok {
parent.Replies = append(parent.Replies, *commentMap[c.ID])
}
}
}
return tree
}
文章服务 #
go
// services/article_service.go
package services
import (
"blog-system/models"
"blog-system/utils"
"errors"
"time"
"gorm.io/gorm"
)
type ArticleService struct {
db *gorm.DB
}
func NewArticleService(db *gorm.DB) *ArticleService {
return &ArticleService{db: db}
}
type CreateArticleRequest struct {
Title string `json:"title" binding:"required,max=200"`
Summary string `json:"summary" binding:"max=500"`
Content string `json:"content" binding:"required"`
Cover string `json:"cover"`
CategoryID uint `json:"category_id"`
TagIDs []uint `json:"tag_ids"`
Status int `json:"status"`
IsTop bool `json:"is_top"`
}
type UpdateArticleRequest struct {
Title string `json:"title" binding:"max=200"`
Summary string `json:"summary" binding:"max=500"`
Content string `json:"content"`
Cover string `json:"cover"`
CategoryID uint `json:"category_id"`
TagIDs []uint `json:"tag_ids"`
Status *int `json:"status"`
IsTop *bool `json:"is_top"`
}
type ArticleListQuery struct {
utils.Pagination
Keyword string `form:"keyword"`
CategoryID uint `form:"category_id"`
TagID uint `form:"tag_id"`
Status *int `form:"status"`
AuthorID uint `form:"author_id"`
}
func (s *ArticleService) Create(authorID uint, req *CreateArticleRequest) (*models.Article, error) {
slug := utils.GenerateSlug(req.Title)
var count int64
s.db.Model(&models.Article{}).Where("slug = ?", slug).Count(&count)
if count > 0 {
slug = slug + "-" + time.Now().Format("20060102")
}
article := &models.Article{
Title: req.Title,
Slug: slug,
Summary: req.Summary,
Content: req.Content,
Cover: req.Cover,
AuthorID: authorID,
CategoryID: req.CategoryID,
Status: req.Status,
IsTop: req.IsTop,
}
if req.Status == 1 {
now := time.Now()
article.PublishedAt = &now
}
err := s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(article).Error; err != nil {
return err
}
if len(req.TagIDs) > 0 {
var tags []models.Tag
if err := tx.Find(&tags, req.TagIDs).Error; err != nil {
return err
}
if err := tx.Model(article).Association("Tags").Replace(tags); err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
return s.GetByID(article.ID)
}
func (s *ArticleService) GetByID(id uint) (*models.Article, error) {
var article models.Article
err := s.db.Preload("Author").
Preload("Category").
Preload("Tags").
First(&article, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("文章不存在")
}
return nil, err
}
return &article, nil
}
func (s *ArticleService) GetBySlug(slug string) (*models.Article, error) {
var article models.Article
err := s.db.Preload("Author").
Preload("Category").
Preload("Tags").
Where("slug = ?", slug).
First(&article).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("文章不存在")
}
return nil, err
}
return &article, nil
}
func (s *ArticleService) List(query *ArticleListQuery) ([]models.Article, int64, error) {
var articles []models.Article
var total int64
q := s.db.Model(&models.Article{}).Preload("Author").Preload("Category").Preload("Tags")
if query.Keyword != "" {
q = q.Where("title LIKE ?", "%"+query.Keyword+"%")
}
if query.CategoryID > 0 {
q = q.Where("category_id = ?", query.CategoryID)
}
if query.TagID > 0 {
q = q.Joins("JOIN article_tags ON article_tags.article_id = articles.id").
Where("article_tags.tag_id = ?", query.TagID)
}
if query.Status != nil {
q = q.Where("status = ?", *query.Status)
}
if query.AuthorID > 0 {
q = q.Where("author_id = ?", query.AuthorID)
}
q.Count(&total)
q = q.Order("is_top DESC, published_at DESC")
q = q.Offset(query.GetOffset()).Limit(query.GetLimit())
if err := q.Find(&articles).Error; err != nil {
return nil, 0, err
}
return articles, total, nil
}
func (s *ArticleService) Update(id uint, req *UpdateArticleRequest) (*models.Article, error) {
article, err := s.GetByID(id)
if err != nil {
return nil, err
}
updates := make(map[string]interface{})
if req.Title != "" {
updates["title"] = req.Title
updates["slug"] = utils.GenerateSlug(req.Title)
}
if req.Summary != "" {
updates["summary"] = req.Summary
}
if req.Content != "" {
updates["content"] = req.Content
}
if req.Cover != "" {
updates["cover"] = req.Cover
}
if req.CategoryID > 0 {
updates["category_id"] = req.CategoryID
}
if req.Status != nil {
updates["status"] = *req.Status
if *req.Status == 1 && article.PublishedAt == nil {
now := time.Now()
updates["published_at"] = &now
}
}
if req.IsTop != nil {
updates["is_top"] = *req.IsTop
}
err = s.db.Transaction(func(tx *gorm.DB) error {
if len(updates) > 0 {
if err := tx.Model(article).Updates(updates).Error; err != nil {
return err
}
}
if req.TagIDs != nil {
var tags []models.Tag
if len(req.TagIDs) > 0 {
if err := tx.Find(&tags, req.TagIDs).Error; err != nil {
return err
}
}
if err := tx.Model(article).Association("Tags").Replace(tags); err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
return s.GetByID(id)
}
func (s *ArticleService) Delete(id uint) error {
result := s.db.Delete(&models.Article{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("文章不存在")
}
return nil
}
func (s *ArticleService) IncrementViewCount(id uint) error {
return s.db.Model(&models.Article{}).Where("id = ?", id).
UpdateColumn("view_count", gorm.Expr("view_count + ?", 1)).Error
}
func (s *ArticleService) IncrementLikeCount(id uint) error {
return s.db.Model(&models.Article{}).Where("id = ?", id).
UpdateColumn("like_count", gorm.Expr("like_count + ?", 1)).Error
}
分类服务 #
go
// services/category_service.go
package services
import (
"blog-system/models"
"blog-system/utils"
"errors"
"gorm.io/gorm"
)
type CategoryService struct {
db *gorm.DB
}
func NewCategoryService(db *gorm.DB) *CategoryService {
return &CategoryService{db: db}
}
type CreateCategoryRequest struct {
Name string `json:"name" binding:"required,max=50"`
Description string `json:"description" binding:"max=200"`
ParentID uint `json:"parent_id"`
SortOrder int `json:"sort_order"`
}
func (s *CategoryService) Create(req *CreateCategoryRequest) (*models.Category, error) {
var count int64
s.db.Model(&models.Category{}).Where("name = ?", req.Name).Count(&count)
if count > 0 {
return nil, errors.New("分类名称已存在")
}
category := &models.Category{
Name: req.Name,
Slug: utils.GenerateSlug(req.Name),
Description: req.Description,
ParentID: req.ParentID,
SortOrder: req.SortOrder,
}
if err := s.db.Create(category).Error; err != nil {
return nil, err
}
return category, nil
}
func (s *CategoryService) List() ([]models.Category, error) {
var categories []models.Category
err := s.db.Order("sort_order ASC, id ASC").Find(&categories).Error
return categories, err
}
func (s *CategoryService) GetTree() ([]models.Category, error) {
var categories []models.Category
if err := s.db.Order("sort_order ASC, id ASC").Find(&categories).Error; err != nil {
return nil, err
}
return buildCategoryTree(categories, 0), nil
}
func buildCategoryTree(categories []models.Category, parentID uint) []models.Category {
var tree []models.Category
for _, cat := range categories {
if cat.ParentID == parentID {
cat.Children = buildCategoryTree(categories, cat.ID)
tree = append(tree, cat)
}
}
return tree
}
func (s *CategoryService) Update(id uint, req *CreateCategoryRequest) (*models.Category, error) {
var category models.Category
if err := s.db.First(&category, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("分类不存在")
}
return nil, err
}
updates := map[string]interface{}{
"name": req.Name,
"slug": utils.GenerateSlug(req.Name),
"description": req.Description,
"parent_id": req.ParentID,
"sort_order": req.SortOrder,
}
if err := s.db.Model(&category).Updates(updates).Error; err != nil {
return nil, err
}
return &category, nil
}
func (s *CategoryService) Delete(id uint) error {
var count int64
s.db.Model(&models.Article{}).Where("category_id = ?", id).Count(&count)
if count > 0 {
return errors.New("该分类下还有文章,无法删除")
}
result := s.db.Delete(&models.Category{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("分类不存在")
}
return nil
}
评论服务 #
go
// services/comment_service.go
package services
import (
"blog-system/models"
"errors"
"gorm.io/gorm"
)
type CommentService struct {
db *gorm.DB
}
func NewCommentService(db *gorm.DB) *CommentService {
return &CommentService{db: db}
}
type CreateCommentRequest struct {
ArticleID uint `json:"article_id" binding:"required"`
Content string `json:"content" binding:"required,min=1,max=1000"`
ParentID uint `json:"parent_id"`
ReplyToID uint `json:"reply_to_id"`
}
func (s *CommentService) Create(userID uint, req *CreateCommentRequest, ip, userAgent string) (*models.Comment, error) {
var article models.Article
if err := s.db.First(&article, req.ArticleID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("文章不存在")
}
return nil, err
}
if req.ParentID > 0 {
var parent models.Comment
if err := s.db.First(&parent, req.ParentID).Error; err != nil {
return nil, errors.New("父评论不存在")
}
if parent.ArticleID != req.ArticleID {
return nil, errors.New("父评论不属于该文章")
}
}
comment := &models.Comment{
Content: req.Content,
ArticleID: req.ArticleID,
UserID: userID,
ParentID: req.ParentID,
ReplyToID: req.ReplyToID,
Status: 1,
IP: ip,
UserAgent: userAgent,
}
if err := s.db.Create(comment).Error; err != nil {
return nil, err
}
s.db.Model(&models.Article{}).Where("id = ?", req.ArticleID).
UpdateColumn("comment_count", gorm.Expr("comment_count + ?", 1))
return s.GetByID(comment.ID)
}
func (s *CommentService) GetByID(id uint) (*models.Comment, error) {
var comment models.Comment
err := s.db.Preload("User").First(&comment, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("评论不存在")
}
return nil, err
}
return &comment, nil
}
func (s *CommentService) GetByArticleID(articleID uint) ([]models.Comment, error) {
var comments []models.Comment
err := s.db.Preload("User").
Where("article_id = ? AND status = 1", articleID).
Order("created_at ASC").
Find(&comments).Error
return comments, err
}
func (s *CommentService) GetCommentTree(articleID uint) ([]models.CommentTree, error) {
comments, err := s.GetByArticleID(articleID)
if err != nil {
return nil, err
}
return models.BuildCommentTree(comments), nil
}
func (s *CommentService) Delete(id uint, userID uint, isAdmin bool) error {
var comment models.Comment
if err := s.db.First(&comment, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("评论不存在")
}
return err
}
if !isAdmin && comment.UserID != userID {
return errors.New("无权删除此评论")
}
if err := s.db.Delete(&comment).Error; err != nil {
return err
}
s.db.Model(&models.Article{}).Where("id = ?", comment.ArticleID).
UpdateColumn("comment_count", gorm.Expr("comment_count - ?", 1))
return nil
}
文章处理器 #
go
// handlers/article_handler.go
package handlers
import (
"blog-system/middleware"
"blog-system/models"
"blog-system/services"
"blog-system/utils"
"strconv"
"github.com/gin-gonic/gin"
)
type ArticleHandler struct {
service *services.ArticleService
}
func NewArticleHandler(service *services.ArticleService) *ArticleHandler {
return &ArticleHandler{service: service}
}
func (h *ArticleHandler) Create(c *gin.Context) {
var req services.CreateArticleRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "请求参数错误")
return
}
userID := middleware.GetUserID(c)
article, err := h.service.Create(userID, &req)
if err != nil {
utils.BadRequest(c, err.Error())
return
}
utils.Created(c, article)
}
func (h *ArticleHandler) GetByID(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
utils.BadRequest(c, "无效的文章ID")
return
}
article, err := h.service.GetByID(uint(id))
if err != nil {
utils.NotFound(c, err.Error())
return
}
h.service.IncrementViewCount(uint(id))
utils.Success(c, article)
}
func (h *ArticleHandler) GetBySlug(c *gin.Context) {
slug := c.Param("slug")
article, err := h.service.GetBySlug(slug)
if err != nil {
utils.NotFound(c, err.Error())
return
}
h.service.IncrementViewCount(article.ID)
utils.Success(c, article)
}
func (h *ArticleHandler) List(c *gin.Context) {
var query services.ArticleListQuery
if err := c.ShouldBindQuery(&query); err != nil {
utils.BadRequest(c, "请求参数错误")
return
}
if query.Page == 0 {
query.Page = 1
}
if query.PageSize == 0 {
query.PageSize = 10
}
articles, total, err := h.service.List(&query)
if err != nil {
utils.InternalError(c, "获取文章列表失败")
return
}
responses := make([]*models.ArticleListResponse, len(articles))
for i, article := range articles {
responses[i] = article.ToListResponse()
}
utils.SuccessWithPage(c, responses, total, query.Page, query.PageSize)
}
func (h *ArticleHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
utils.BadRequest(c, "无效的文章ID")
return
}
var req services.UpdateArticleRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "请求参数错误")
return
}
article, err := h.service.Update(uint(id), &req)
if err != nil {
utils.BadRequest(c, err.Error())
return
}
utils.Success(c, article)
}
func (h *ArticleHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
utils.BadRequest(c, "无效的文章ID")
return
}
if err := h.service.Delete(uint(id)); err != nil {
utils.BadRequest(c, err.Error())
return
}
utils.Success(c, gin.H{"message": "删除成功"})
}
func (h *ArticleHandler) Like(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
utils.BadRequest(c, "无效的文章ID")
return
}
if err := h.service.IncrementLikeCount(uint(id)); err != nil {
utils.BadRequest(c, err.Error())
return
}
utils.Success(c, gin.H{"message": "点赞成功"})
}
路由配置 #
go
// router/router.go
package router
import (
"blog-system/handlers"
"blog-system/middleware"
"blog-system/models"
"blog-system/services"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func Setup(db *gorm.DB) *gin.Engine {
r := gin.Default()
db.AutoMigrate(
&models.User{},
&models.Article{},
&models.Category{},
&models.Tag{},
&models.Comment{},
)
articleService := services.NewArticleService(db)
categoryService := services.NewCategoryService(db)
tagService := services.NewTagService(db)
commentService := services.NewCommentService(db)
articleHandler := handlers.NewArticleHandler(articleService)
categoryHandler := handlers.NewCategoryHandler(categoryService)
tagHandler := handlers.NewTagHandler(tagService)
commentHandler := handlers.NewCommentHandler(commentService)
api := r.Group("/api/v1")
{
articles := api.Group("/articles")
{
articles.GET("", articleHandler.List)
articles.GET("/:id", articleHandler.GetByID)
articles.GET("/slug/:slug", articleHandler.GetBySlug)
articles.POST("/:id/like", articleHandler.Like)
articles.GET("/:id/comments", commentHandler.GetByArticle)
auth := articles.Use(middleware.JWTAuth())
{
auth.POST("", articleHandler.Create)
auth.PUT("/:id", articleHandler.Update)
auth.DELETE("/:id", articleHandler.Delete)
}
}
categories := api.Group("/categories")
{
categories.GET("", categoryHandler.List)
categories.GET("/tree", categoryHandler.GetTree)
admin := categories.Use(middleware.JWTAuth(), middleware.RequireAdmin())
{
admin.POST("", categoryHandler.Create)
admin.PUT("/:id", categoryHandler.Update)
admin.DELETE("/:id", categoryHandler.Delete)
}
}
tags := api.Group("/tags")
{
tags.GET("", tagHandler.List)
admin := tags.Use(middleware.JWTAuth(), middleware.RequireAdmin())
{
admin.POST("", tagHandler.Create)
admin.PUT("/:id", tagHandler.Update)
admin.DELETE("/:id", tagHandler.Delete)
}
}
comments := api.Group("/comments")
comments.Use(middleware.JWTAuth())
{
comments.POST("", commentHandler.Create)
comments.DELETE("/:id", commentHandler.Delete)
}
}
return r
}
小结 #
本节构建了一个完整的博客系统:
- 文章管理:创建、编辑、发布、删除
- 分类系统:支持多级分类
- 标签系统:文章多标签关联
- 评论系统:支持嵌套回复
- 统计功能:浏览量、点赞数、评论数
这个博客系统具备完整的CMS功能,可以根据需求继续扩展。
最后更新:2026-03-28