博客系统 #
一、系统概述 #
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