实战案例 - 博客系统
第十三章:实战案例 - 博客系统
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
欢迎关注公众号,一起学习进步!