实战案例 - 博客系统


第十三章:实战案例 - 博客系统

13.1 项目概述

我们将构建一个完整的博客系统,包含以下功能模块:

  • 用户模块:注册、登录、个人中心
  • 文章模块:发布、编辑、分类、标签
  • 评论模块:文章评论、回复
  • 管理后台:数据统计、内容管理

13.2 项目结构

blog-example/
├── go.mod
├── main.go
├── config/
│   └── config.go
├── model/
│   ├── base.go
│   ├── user.go
│   ├── article.go
│   ├── category.go
│   ├── tag.go
│   ├── comment.go
│   └── migrate.go
├── dao/
│   ├── user_dao.go
│   ├── article_dao.go
│   └── comment_dao.go
├── service/
│   ├── user_service.go
│   ├── article_service.go
│   └── comment_service.go
├── api/
│   ├── handler/
│   │   ├── user_handler.go
│   │   ├── article_handler.go
│   │   └── comment_handler.go
│   └── router.go
├── middleware/
│   ├── auth.go
│   └── logger.go
└── utils/
    ├── password.go
    ├── jwt.go
    └── response.go

13.3 模型定义

基础模型

package model

import (
    "time"
    "gorm.io/gorm"
)

type BaseModel struct {
    ID        uint64         `json:"id" gorm:"primaryKey;autoIncrement"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}

用户模型

type User struct {
    BaseModel
    Username  string    `json:"username" gorm:"size:50;uniqueIndex;not null;comment:用户名"`
    Password  string    `json:"-" gorm:"size:255;not null;comment:密码"`
    Email     string    `json:"email" gorm:"size:100;uniqueIndex;comment:邮箱"`
    Nickname  string    `json:"nickname" gorm:"size:50;comment:昵称"`
    Avatar    string    `json:"avatar" gorm:"size:500;comment:头像"`
    Bio       string    `json:"bio" gorm:"size:500;comment:简介"`
    Status    int8      `json:"status" gorm:"default:1;comment:状态:1-正常 2-禁用"`
    Role      int8      `json:"role" gorm:"default:1;comment:角色:1-普通用户 2-管理员"`
    Articles  []Article `json:"articles,omitempty" gorm:"foreignKey:AuthorID"`
}

func (User) TableName() string {
    return "blog_user"
}

// 钩子:密码加密
func (u *User) BeforeCreate(tx *gorm.DB) error {
    if u.Password != "" {
        u.Password = utils.HashPassword(u.Password)
    }
    return nil
}

文章模型

type Article struct {
    BaseModel
    Title       string    `json:"title" gorm:"size:200;not null;index;comment:标题"`
    Summary     string    `json:"summary" gorm:"size:500;comment:摘要"`
    Content     string    `json:"content" gorm:"type:longtext;not null;comment:内容"`
    Cover       string    `json:"cover" gorm:"size:500;comment:封面图"`
    AuthorID    uint64    `json:"author_id" gorm:"index;not null;comment:作者ID"`
    Author      User      `json:"author,omitempty" gorm:"foreignKey:AuthorID"`
    CategoryID  uint64    `json:"category_id" gorm:"index;comment:分类ID"`
    Category    Category  `json:"category,omitempty" gorm:"foreignKey:CategoryID"`
    Tags        []Tag     `json:"tags,omitempty" gorm:"many2many:article_tag;"`
    ViewCount   int64     `json:"view_count" gorm:"default:0;comment:浏览量"`
    LikeCount   int64     `json:"like_count" gorm:"default:0;comment:点赞数"`
    Status      int8      `json:"status" gorm:"default:1;comment:状态:1-草稿 2-已发布 3-下架"`
    PublishedAt *time.Time `json:"published_at" gorm:"comment:发布时间"`
    Comments    []Comment `json:"comments,omitempty" gorm:"foreignKey:ArticleID"`
}

func (Article) TableName() string {
    return "blog_article"
}

// 发布文章
func (a *Article) Publish(db *gorm.DB) error {
    now := time.Now()
    a.Status = 2
    a.PublishedAt = &now
    return db.Save(a).Error
}

分类模型

type Category struct {
    BaseModel
    Name     string    `json:"name" gorm:"size:50;not null;comment:分类名称"`
    Slug     string    `json:"slug" gorm:"size:50;uniqueIndex;comment:URL别名"`
    ParentID *uint64   `json:"parent_id" gorm:"index;comment:父分类ID"`
    Parent   *Category `json:"parent,omitempty" gorm:"foreignKey:ParentID"`
    Sort     int       `json:"sort" gorm:"default:0;comment:排序"`
    Articles []Article `json:"articles,omitempty" gorm:"foreignKey:CategoryID"`
}

func (Category) TableName() string {
    return "blog_category"
}

标签模型

type Tag struct {
    BaseModel
    Name     string    `json:"name" gorm:"size:50;not null;uniqueIndex;comment:标签名"`
    Slug     string    `json:"slug" gorm:"size:50;uniqueIndex;comment:URL别名"`
    Articles []Article `json:"articles,omitempty" gorm:"many2many:article_tag;"`
}

func (Tag) TableName() string {
    return "blog_tag"
}

评论模型

type Comment struct {
    BaseModel
    ArticleID uint64    `json:"article_id" gorm:"index;not null;comment:文章ID"`
    Article   Article   `json:"article,omitempty" gorm:"foreignKey:ArticleID"`
    UserID    uint64    `json:"user_id" gorm:"index;not null;comment:用户ID"`
    User      User      `json:"user,omitempty" gorm:"foreignKey:UserID"`
    ParentID  *uint64   `json:"parent_id" gorm:"index;comment:父评论ID(回复)"`
    Parent    *Comment  `json:"parent,omitempty" gorm:"foreignKey:ParentID"`
    Content   string    `json:"content" gorm:"size:2000;not null;comment:内容"`
    Status    int8      `json:"status" gorm:"default:1;comment:状态:1-正常 2-待审核 3-删除"`
    Replies   []Comment `json:"replies,omitempty" gorm:"foreignKey:ParentID"`
}

func (Comment) TableName() string {
    return "blog_comment"
}

13.4 DAO 层实现

基础 DAO

package dao

import "gorm.io/gorm"

type BaseDAO struct {
    db *gorm.DB
}

func NewBaseDAO(db *gorm.DB) *BaseDAO {
    return &BaseDAO{db: db}
}

func (d *BaseDAO) DB() *gorm.DB {
    return d.db
}

// Transaction 执行事务
func (d *BaseDAO) Transaction(fn func(*gorm.DB) error) error {
    return d.db.Transaction(fn)
}

文章 DAO

package dao

import (
    "context"
    "myblog/model"
    "gorm.io/gorm"
)

type ArticleDAO struct {
    *BaseDAO
}

func NewArticleDAO(db *gorm.DB) *ArticleDAO {
    return &ArticleDAO{BaseDAO: NewBaseDAO(db)}
}

// Create 创建文章
func (d *ArticleDAO) Create(ctx context.Context, article *model.Article) error {
    return d.db.WithContext(ctx).Create(article).Error
}

// GetByID 根据ID获取文章(包含作者、分类、标签)
func (d *ArticleDAO) GetByID(ctx context.Context, id uint64) (*model.Article, error) {
    var article model.Article
    err := d.db.WithContext(ctx).
        Preload("Author", func(db *gorm.DB) *gorm.DB {
            return db.Select("id", "username", "nickname", "avatar")
        }).
        Preload("Category").
        Preload("Tags").
        First(&article, id).Error
    return &article, err
}

// GetList 获取文章列表
func (d *ArticleDAO) GetList(ctx context.Context, req ArticleListRequest) ([]model.Article, int64, error) {
    db := d.db.WithContext(ctx).Model(&model.Article{})
    
    // 构建查询条件
    if req.Status > 0 {
        db = db.Where("status = ?", req.Status)
    }
    if req.CategoryID > 0 {
        db = db.Where("category_id = ?", req.CategoryID)
    }
    if req.Keyword != "" {
        db = db.Where("title LIKE ? OR summary LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
    }
    
    // 统计总数
    var total int64
    if err := db.Count(&total).Error; err != nil {
        return nil, 0, err
    }
    
    // 查询数据
    var articles []model.Article
    err := db.Preload("Author", func(db *gorm.DB) *gorm.DB {
        return db.Select("id", "username", "nickname")
    }).
    Preload("Category").
    Order(req.SortBy + " " + req.Order).
    Offset((req.Page - 1) * req.PageSize).
    Limit(req.PageSize).
    Find(&articles).Error
    
    return articles, total, err
}

// UpdateViewCount 更新浏览量(使用乐观锁)
func (d *ArticleDAO) UpdateViewCount(ctx context.Context, id uint64) error {
    return d.db.WithContext(ctx).Model(&model.Article{}).
        Where("id = ?", id).
        UpdateColumn("view_count", gorm.Expr("view_count + ?", 1)).Error
}

// Update 更新文章
func (d *ArticleDAO) Update(ctx context.Context, article *model.Article) error {
    return d.db.WithContext(ctx).Model(article).Omit("CreatedAt").Updates(article).Error
}

// Delete 删除文章(软删除)
func (d *ArticleDAO) Delete(ctx context.Context, id uint64) error {
    return d.db.WithContext(ctx).Delete(&model.Article{}, id).Error
}

type ArticleListRequest struct {
    Page       int
    PageSize   int
    Status     int8
    CategoryID uint64
    Keyword    string
    SortBy     string
    Order      string
}

13.5 Service 层

文章服务

package service

import (
    "context"
    "errors"
    "myblog/dao"
    "myblog/model"
)

type ArticleService struct {
    articleDAO *dao.ArticleDAO
    tagDAO     *dao.TagDAO
}

func NewArticleService(articleDAO *dao.ArticleDAO, tagDAO *dao.TagDAO) *ArticleService {
    return &ArticleService{
        articleDAO: articleDAO,
        tagDAO:     tagDAO,
    }
}

// CreateArticle 创建文章
func (s *ArticleService) CreateArticle(ctx context.Context, req CreateArticleRequest) (*model.Article, error) {
    // 处理标签
    tags, err := s.tagDAO.GetOrCreateByNames(ctx, req.Tags)
    if err != nil {
        return nil, err
    }
    
    article := &model.Article{
        Title:      req.Title,
        Summary:    req.Summary,
        Content:    req.Content,
        Cover:      req.Cover,
        AuthorID:   req.AuthorID,
        CategoryID: req.CategoryID,
        Tags:       tags,
        Status:     req.Status,
    }
    
    if err := s.articleDAO.Create(ctx, article); err != nil {
        return nil, err
    }
    
    return article, nil
}

// GetArticleDetail 获取文章详情
func (s *ArticleService) GetArticleDetail(ctx context.Context, id uint64) (*model.Article, error) {
    article, err := s.articleDAO.GetByID(ctx, id)
    if err != nil {
        return nil, err
    }
    
    // 异步更新浏览量
    go s.articleDAO.UpdateViewCount(context.Background(), id)
    
    return article, nil
}

// PublishArticle 发布文章
func (s *ArticleService) PublishArticle(ctx context.Context, articleID, userID uint64) error {
    article, err := s.articleDAO.GetByID(ctx, articleID)
    if err != nil {
        return err
    }
    
    if article.AuthorID != userID {
        return errors.New("无权操作")
    }
    
    return article.Publish(s.articleDAO.DB())
}

13.6 API 层

文章 Handler

package handler

import (
    "net/http"
    "strconv"
    "github.com/gin-gonic/gin"
    "myblog/service"
    "myblog/utils"
)

type ArticleHandler struct {
    articleService *service.ArticleService
}

func NewArticleHandler(service *service.ArticleService) *ArticleHandler {
    return &ArticleHandler{articleService: service}
}

// Create 创建文章
func (h *ArticleHandler) Create(c *gin.Context) {
    var req service.CreateArticleRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, utils.ErrorResponse(err.Error()))
        return
    }
    
    // 从上下文获取当前用户ID
    userID, _ := c.Get("userID")
    req.AuthorID = userID.(uint64)
    
    article, err := h.articleService.CreateArticle(c.Request.Context(), req)
    if err != nil {
        c.JSON(http.StatusInternalServerError, utils.ErrorResponse(err.Error()))
        return
    }
    
    c.JSON(http.StatusOK, utils.SuccessResponse(article))
}

// GetDetail 获取详情
func (h *ArticleHandler) GetDetail(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 64)
    if err != nil {
        c.JSON(http.StatusBadRequest, utils.ErrorResponse("无效的文章ID"))
        return
    }
    
    article, err := h.articleService.GetArticleDetail(c.Request.Context(), id)
    if err != nil {
        c.JSON(http.StatusNotFound, utils.ErrorResponse("文章不存在"))
        return
    }
    
    c.JSON(http.StatusOK, utils.SuccessResponse(article))
}

// GetList 获取列表
func (h *ArticleHandler) GetList(c *gin.Context) {
    var req dao.ArticleListRequest
    if err := c.ShouldBindQuery(&req); err != nil {
        c.JSON(http.StatusBadRequest, utils.ErrorResponse(err.Error()))
        return
    }
    
    // 设置默认值
    if req.Page <= 0 {
        req.Page = 1
    }
    if req.PageSize <= 0 {
        req.PageSize = 10
    }
    if req.SortBy == "" {
        req.SortBy = "created_at"
    }
    if req.Order == "" {
        req.Order = "desc"
    }
    
    articles, total, err := h.articleService.GetArticleList(c.Request.Context(), req)
    if err != nil {
        c.JSON(http.StatusInternalServerError, utils.ErrorResponse(err.Error()))
        return
    }
    
    c.JSON(http.StatusOK, utils.PageResponse(articles, total, req.Page, req.PageSize))
}

13.7 中间件

JWT 认证

package middleware

import (
    "net/http"
    "strings"
    "github.com/gin-gonic/gin"
    "myblog/utils"
)

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        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": "认证格式错误"})
            c.Abort()
            return
        }
        
        claims, err := utils.ParseToken(parts[1])
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token"})
            c.Abort()
            return
        }
        
        c.Set("userID", claims.UserID)
        c.Set("username", claims.Username)
        c.Next()
    }
}

13.8 数据库初始化

package model

import (
    "gorm.io/gorm"
)

func Migrate(db *gorm.DB) error {
    return db.AutoMigrate(
        &User{},
        &Category{},
        &Tag{},
        &Article{},
        &Comment{},
    )
}

func Seed(db *gorm.DB) error {
    // 创建默认分类
    categories := []Category{
        {Name: "技术", Slug: "tech"},
        {Name: "生活", Slug: "life"},
        {Name: "随笔", Slug: "essay"},
    }
    for _, cat := range categories {
        db.FirstOrCreate(&cat, Category{Slug: cat.Slug})
    }
    
    // 创建管理员用户
    admin := User{
        Username: "admin",
        Password: "admin123",
        Email:    "admin@example.com",
        Nickname: "管理员",
        Role:     2,
    }
    db.FirstOrCreate(&admin, User{Username: "admin"})
    
    return nil
}

13.9 完整启动代码

package main

import (
    "log"
    "myblog/api"
    "myblog/dao"
    "myblog/model"
    "myblog/service"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

func main() {
    // 连接数据库
    dsn := "user:password@tcp(127.0.0.1:3306)/blog?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info),
    })
    if err != nil {
        log.Fatal("数据库连接失败:", err)
    }
    
    // 自动迁移
    if err := model.Migrate(db); err != nil {
        log.Fatal("数据库迁移失败:", err)
    }
    
    // 种子数据
    if err := model.Seed(db); err != nil {
        log.Fatal("种子数据失败:", err)
    }
    
    // 初始化 DAO
    articleDAO := dao.NewArticleDAO(db)
    tagDAO := dao.NewTagDAO(db)
    
    // 初始化 Service
    articleService := service.NewArticleService(articleDAO, tagDAO)
    
    // 启动服务器
    router := api.NewRouter(articleService)
    log.Println("服务器启动在 :8080")
    router.Run(":8080")
}

13.10 小结

本章通过构建一个完整的博客系统,展示了 GORM 在实际项目中的应用。包括:

  • 完整的模型设计(含关联关系)
  • DAO 层的封装和事务处理
  • Service 层的业务逻辑
  • API 层的接口实现
  • 数据库迁移和种子数据

本文代码地址:https://github.com/LittleMoreInteresting/gorm_study

欢迎关注公众号,一起学习进步!

如有疑问关注公众号给我留言
wx

关注公众号

©2017-2023 鲁ICP备17023316号-1 Powered by Hugo