博客系统实战 #

本节将使用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
}

小结 #

本节构建了一个完整的博客系统:

  1. 文章管理:创建、编辑、发布、删除
  2. 分类系统:支持多级分类
  3. 标签系统:文章多标签关联
  4. 评论系统:支持嵌套回复
  5. 统计功能:浏览量、点赞数、评论数

这个博客系统具备完整的CMS功能,可以根据需求继续扩展。

最后更新:2026-03-28