博客系统 #

一、系统概述 #

1.1 功能模块 #

text
┌─────────────────────────────────────────────────────────┐
│                    博客系统功能                          │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  文章管理                                                │
│  ├── 文章发布                                           │
│  ├── 文章编辑                                           │
│  ├── 文章删除                                           │
│  └── 文章列表                                           │
│                                                         │
│  分类标签                                                │
│  ├── 分类管理                                           │
│  └── 标签管理                                           │
│                                                         │
│  评论系统                                                │
│  ├── 发表评论                                           │
│  └── 评论管理                                           │
│                                                         │
└─────────────────────────────────────────────────────────┘

1.2 数据模型 #

text
User ──┬── Post ──┬── Comment
       │          │
       │          └── Tag (多对多)
       │
       └── Category

二、数据模型 #

2.1 用户模型 #

go
type User struct {
    ID        uint           `gorm:"primaryKey" json:"id"`
    Name      string         `gorm:"size:100;not null" json:"name"`
    Email     string         `gorm:"size:100;uniqueIndex" json:"email"`
    Password  string         `gorm:"size:255" json:"-"`
    Bio       string         `gorm:"size:500" json:"bio"`
    Avatar    string         `gorm:"size:255" json:"avatar"`
    Role      string         `gorm:"size:20;default:'user'" json:"role"`
    Status    string         `gorm:"size:20;default:'active'" json:"status"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
    
    Posts     []Post         `gorm:"foreignKey:AuthorID" json:"posts,omitempty"`
    Comments  []Comment      `gorm:"foreignKey:UserID" json:"comments,omitempty"`
}

2.2 文章模型 #

go
type Post struct {
    ID          uint           `gorm:"primaryKey" json:"id"`
    Title       string         `gorm:"size:200;not null" json:"title"`
    Slug        string         `gorm:"size:200;uniqueIndex;not null" json:"slug"`
    Summary     string         `gorm:"size:500" json:"summary"`
    Content     string         `gorm:"type:text" json:"content"`
    Cover       string         `gorm:"size:255" json:"cover"`
    Status      string         `gorm:"size:20;default:'draft'" json:"status"`
    ViewCount   int            `gorm:"default:0" json:"view_count"`
    LikeCount   int            `gorm:"default:0" json:"like_count"`
    AuthorID    uint           `json:"author_id"`
    CategoryID  uint           `json:"category_id"`
    CreatedAt   time.Time      `json:"created_at"`
    UpdatedAt   time.Time      `json:"updated_at"`
    DeletedAt   gorm.DeletedAt `gorm:"index" json:"-"`
    
    Author      User           `gorm:"foreignKey:AuthorID" json:"author"`
    Category    Category       `gorm:"foreignKey:CategoryID" json:"category"`
    Tags        []Tag          `gorm:"many2many:post_tags" json:"tags"`
    Comments    []Comment      `gorm:"foreignKey:PostID" json:"comments,omitempty"`
}

const (
    PostStatusDraft     = "draft"
    PostStatusPublished = "published"
    PostStatusArchived  = "archived"
)

2.3 分类模型 #

go
type Category struct {
    ID        uint           `gorm:"primaryKey" json:"id"`
    Name      string         `gorm:"size:100;not null" json:"name"`
    Slug      string         `gorm:"size:100;uniqueIndex;not null" json:"slug"`
    ParentID  *uint          `json:"parent_id"`
    SortOrder int            `gorm:"default:0" json:"sort_order"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
    
    Posts     []Post         `gorm:"foreignKey:CategoryID" json:"posts,omitempty"`
    Children  []Category     `gorm:"foreignKey:ParentID" json:"children,omitempty"`
}

2.4 标签模型 #

go
type Tag struct {
    ID        uint           `gorm:"primaryKey" json:"id"`
    Name      string         `gorm:"size:50;not null" json:"name"`
    Slug      string         `gorm:"size:50;uniqueIndex;not null" json:"slug"`
    Color     string         `gorm:"size:20" json:"color"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
    
    Posts     []Post         `gorm:"many2many:post_tags" json:"posts,omitempty"`
}

2.5 评论模型 #

go
type Comment struct {
    ID        uint           `gorm:"primaryKey" json:"id"`
    Content   string         `gorm:"type:text;not null" json:"content"`
    Status    string         `gorm:"size:20;default:'pending'" json:"status"`
    PostID    uint           `json:"post_id"`
    UserID    uint           `json:"user_id"`
    ParentID  *uint          `json:"parent_id"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
    
    Post      Post           `gorm:"foreignKey:PostID" json:"post"`
    User      User           `gorm:"foreignKey:UserID" json:"user"`
    Replies   []Comment      `gorm:"foreignKey:ParentID" json:"replies,omitempty"`
}

const (
    CommentStatusPending  = "pending"
    CommentStatusApproved = "approved"
    CommentStatusSpam     = "spam"
)

三、文章服务 #

3.1 服务定义 #

go
package services

import (
    "errors"
    "strings"
    "time"
    "myapi/internal/models"
    "myapi/internal/repositories"
    "github.com/slugify"
)

type PostService struct {
    postRepo     *repositories.PostRepository
    categoryRepo *repositories.CategoryRepository
    tagRepo      *repositories.TagRepository
}

func NewPostService(postRepo *repositories.PostRepository, categoryRepo *repositories.CategoryRepository, tagRepo *repositories.TagRepository) *PostService {
    return &PostService{
        postRepo:     postRepo,
        categoryRepo: categoryRepo,
        tagRepo:      tagRepo,
    }
}

type CreatePostInput struct {
    Title      string `json:"title" validate:"required"`
    Summary    string `json:"summary"`
    Content    string `json:"content" validate:"required"`
    Cover      string `json:"cover"`
    Status     string `json:"status"`
    CategoryID uint   `json:"category_id"`
    Tags       []uint `json:"tags"`
}

type UpdatePostInput struct {
    Title      string `json:"title"`
    Summary    string `json:"summary"`
    Content    string `json:"content"`
    Cover      string `json:"cover"`
    Status     string `json:"status"`
    CategoryID uint   `json:"category_id"`
    Tags       []uint `json:"tags"`
}

func (s *PostService) Create(authorID uint, input *CreatePostInput) (*models.Post, error) {
    slug := slugify.Slug(input.Title)
    
    post := &models.Post{
        Title:      input.Title,
        Slug:       slug,
        Summary:    input.Summary,
        Content:    input.Content,
        Cover:      input.Cover,
        Status:     input.Status,
        AuthorID:   authorID,
        CategoryID: input.CategoryID,
    }
    
    if post.Status == "" {
        post.Status = models.PostStatusDraft
    }
    
    if err := s.postRepo.Create(post); err != nil {
        return nil, err
    }
    
    if len(input.Tags) > 0 {
        tags, _ := s.tagRepo.FindByIDs(input.Tags)
        s.postRepo.AssociateTags(post, tags)
    }
    
    return post, nil
}

func (s *PostService) Update(id uint, input *UpdatePostInput) (*models.Post, error) {
    post, err := s.postRepo.FindByID(id)
    if err != nil {
        return nil, errors.New("文章不存在")
    }
    
    if input.Title != "" {
        post.Title = input.Title
        post.Slug = slugify.Slug(input.Title)
    }
    if input.Summary != "" {
        post.Summary = input.Summary
    }
    if input.Content != "" {
        post.Content = input.Content
    }
    if input.Cover != "" {
        post.Cover = input.Cover
    }
    if input.Status != "" {
        post.Status = input.Status
    }
    if input.CategoryID != 0 {
        post.CategoryID = input.CategoryID
    }
    
    if err := s.postRepo.Update(post); err != nil {
        return nil, err
    }
    
    if input.Tags != nil {
        tags, _ := s.tagRepo.FindByIDs(input.Tags)
        s.postRepo.AssociateTags(post, tags)
    }
    
    return post, nil
}

func (s *PostService) Delete(id uint) error {
    return s.postRepo.Delete(id)
}

func (s *PostService) GetByID(id uint) (*models.Post, error) {
    post, err := s.postRepo.FindByID(id)
    if err != nil {
        return nil, err
    }
    
    s.postRepo.IncrementViewCount(post)
    
    return post, nil
}

func (s *PostService) GetBySlug(slug string) (*models.Post, error) {
    return s.postRepo.FindBySlug(slug)
}

func (s *PostService) GetList(page, size int, filters map[string]interface{}) ([]models.Post, int64, error) {
    return s.postRepo.FindAll(page, size, filters)
}

func (s *PostService) GetPublished(page, size int) ([]models.Post, int64, error) {
    filters := map[string]interface{}{
        "status": models.PostStatusPublished,
    }
    return s.postRepo.FindAll(page, size, filters)
}

func (s *PostService) GetByCategory(categoryID uint, page, size int) ([]models.Post, int64, error) {
    filters := map[string]interface{}{
        "category_id": categoryID,
        "status":      models.PostStatusPublished,
    }
    return s.postRepo.FindAll(page, size, filters)
}

func (s *PostService) GetByTag(tagID uint, page, size int) ([]models.Post, int64, error) {
    return s.postRepo.FindByTag(tagID, page, size)
}

func (s *PostService) Search(keyword string, page, size int) ([]models.Post, int64, error) {
    return s.postRepo.Search(keyword, page, size)
}

四、文章处理器 #

go
package handlers

import (
    "net/http"
    "strconv"
    "myapi/internal/services"
    "myapi/pkg/response"
    "github.com/labstack/echo/v4"
)

type PostHandler struct {
    postService *services.PostService
}

func NewPostHandler(postService *services.PostService) *PostHandler {
    return &PostHandler{postService: postService}
}

func (h *PostHandler) List(c echo.Context) error {
    page, _ := strconv.Atoi(c.QueryParam("page"))
    if page < 1 {
        page = 1
    }
    
    size, _ := strconv.Atoi(c.QueryParam("size"))
    if size < 1 || size > 50 {
        size = 10
    }
    
    posts, total, err := h.postService.GetPublished(page, size)
    if err != nil {
        return response.InternalError(c, "获取文章列表失败")
    }
    
    return response.SuccessWithPage(c, posts, total, page, size)
}

func (h *PostHandler) Get(c echo.Context) error {
    id, err := strconv.ParseUint(c.Param("id"), 10, 64)
    if err != nil {
        return response.BadRequest(c, "无效的文章ID")
    }
    
    post, err := h.postService.GetByID(uint(id))
    if err != nil {
        return response.NotFound(c, "文章不存在")
    }
    
    return response.Success(c, post)
}

func (h *PostHandler) GetBySlug(c echo.Context) error {
    slug := c.Param("slug")
    
    post, err := h.postService.GetBySlug(slug)
    if err != nil {
        return response.NotFound(c, "文章不存在")
    }
    
    return response.Success(c, post)
}

func (h *PostHandler) Create(c echo.Context) error {
    authorID := c.Get("userID").(uint)
    
    input := new(services.CreatePostInput)
    if err := c.Bind(input); err != nil {
        return response.BadRequest(c, "参数格式错误")
    }
    
    if err := c.Validate(input); err != nil {
        return response.BadRequest(c, err.Error())
    }
    
    post, err := h.postService.Create(authorID, input)
    if err != nil {
        return response.InternalError(c, "创建文章失败")
    }
    
    return c.JSON(http.StatusCreated, response.Response{
        Code:    0,
        Message: "创建成功",
        Data:    post,
    })
}

func (h *PostHandler) Update(c echo.Context) error {
    id, err := strconv.ParseUint(c.Param("id"), 10, 64)
    if err != nil {
        return response.BadRequest(c, "无效的文章ID")
    }
    
    input := new(services.UpdatePostInput)
    if err := c.Bind(input); err != nil {
        return response.BadRequest(c, "参数格式错误")
    }
    
    post, err := h.postService.Update(uint(id), input)
    if err != nil {
        return response.BadRequest(c, err.Error())
    }
    
    return response.Success(c, post)
}

func (h *PostHandler) Delete(c echo.Context) error {
    id, err := strconv.ParseUint(c.Param("id"), 10, 64)
    if err != nil {
        return response.BadRequest(c, "无效的文章ID")
    }
    
    if err := h.postService.Delete(uint(id)); err != nil {
        return response.InternalError(c, "删除失败")
    }
    
    return c.NoContent(http.StatusNoContent)
}

func (h *PostHandler) Search(c echo.Context) error {
    keyword := c.QueryParam("q")
    if keyword == "" {
        return response.BadRequest(c, "请输入搜索关键词")
    }
    
    page, _ := strconv.Atoi(c.QueryParam("page"))
    if page < 1 {
        page = 1
    }
    
    size, _ := strconv.Atoi(c.QueryParam("size"))
    if size < 1 || size > 50 {
        size = 10
    }
    
    posts, total, err := h.postService.Search(keyword, page, size)
    if err != nil {
        return response.InternalError(c, "搜索失败")
    }
    
    return response.SuccessWithPage(c, posts, total, page, size)
}

五、路由配置 #

go
func setupRoutes(e *echo.Echo, postHandler *handlers.PostHandler, jwtConfig auth.JWTConfig) {
    api := e.Group("/api/v1")
    
    posts := api.Group("/posts")
    {
        posts.GET("", postHandler.List)
        posts.GET("/:id", postHandler.Get)
        posts.GET("/slug/:slug", postHandler.GetBySlug)
        posts.GET("/search", postHandler.Search)
    }
    
    protected := api.Group("")
    protected.Use(middleware.JWTAuth(jwtConfig))
    {
        protected.POST("/posts", postHandler.Create)
        protected.PUT("/posts/:id", postHandler.Update)
        protected.DELETE("/posts/:id", postHandler.Delete)
    }
}

六、总结 #

博客系统要点:

模块 功能
用户 用户管理、认证
文章 发布、编辑、删除
分类 分类管理
标签 标签管理、多对多
评论 评论发表、管理

准备好学习部署上线了吗?让我们进入下一章!

最后更新:2026-03-28