第6个项目 - 完整博客系统
嘿,朋友们!我是长安。
今天我们要做的最后一个实战项目是完整博客系统。这是一个企业级的Web应用,整合了前面所有项目学到的知识,包括用户认证、CRUD操作、文件上传、权限管理等。
说实话,这个项目是真正的实战综合项目!它涵盖了企业开发中最常见的功能模块。我当年找工作就是靠这类项目经验拿到的offer。完成这个项目后,你就具备了独立开发企业级Web应用的能力!
🎯 项目目标
实现一个功能完整的博客系统,支持以下功能:
| 模块 | 功能 | 技术点 |
|---|---|---|
| 用户系统 | 注册、登录、个人信息 | JWT认证、密码加密 |
| 文章管理 | 发布、编辑、删除文章 | CRUD、Markdown |
| 评论系统 | 发表评论、回复评论 | 树形结构 |
| 分类标签 | 文章分类和标签 | 多对多关系 |
| 文件上传 | 上传图片、头像 | 文件处理 |
| 搜索功能 | 搜索文章内容 | 全文搜索 |
📁 项目结构
blog-system/
├── main.go # 程序入口
├── config/
│ └── config.go # 配置管理
├── models/
│ ├── user.go # 用户模型
│ ├── article.go # 文章模型
│ ├── comment.go # 评论模型
│ └── tag.go # 标签模型
├── services/
│ ├── user.go # 用户服务
│ ├── article.go # 文章服务
│ └── auth.go # 认证服务
├── handlers/
│ ├── user.go # 用户处理器
│ ├── article.go # 文章处理器
│ └── comment.go # 评论处理器
├── middleware/
│ ├── auth.go # 认证中间件
│ └── cors.go # CORS中间件
├── utils/
│ ├── jwt.go # JWT工具
│ ├── hash.go # 密码哈希
│ └── upload.go # 文件上传
└── go.mod
🚀 第一步:项目初始化
# 创建项目目录
mkdir blog-system
cd blog-system
# 初始化Go模块
go mod init github.com/yourusername/blog-system
# 安装依赖
go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
go get -u github.com/golang-jwt/jwt/v5
go get -u golang.org/x/crypto/bcrypt
# 创建目录结构
mkdir config models services handlers middleware utils uploads
mkdir uploads/avatars uploads/images
📦 第二步:配置和工具类
文件: config/config.go
package config
import "os"
type Config struct {
Server ServerConfig
Database DatabaseConfig
JWT JWTConfig
Upload UploadConfig
}
type ServerConfig struct {
Port string
}
type DatabaseConfig struct {
Host string
Port string
User string
Password string
Database string
}
type JWTConfig struct {
Secret string
ExpireHour int
}
type UploadConfig struct {
MaxSize int64 // 最大文件大小(字节)
Path string // 上传路径
}
func LoadConfig() *Config {
return &Config{
Server: ServerConfig{
Port: getEnv("PORT", "8080"),
},
Database: DatabaseConfig{
Host: getEnv("DB_HOST", "localhost"),
Port: getEnv("DB_PORT", "3306"),
User: getEnv("DB_USER", "root"),
Password: getEnv("DB_PASSWORD", ""),
Database: getEnv("DB_NAME", "blog_system"),
},
JWT: JWTConfig{
Secret: getEnv("JWT_SECRET", "your-secret-key"),
ExpireHour: 24 * 7, // 7天
},
Upload: UploadConfig{
MaxSize: 5 * 1024 * 1024, // 5MB
Path: "./uploads",
},
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
文件: utils/jwt.go
package utils
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
var jwtSecret []byte
// InitJWT 初始化JWT密钥
func InitJWT(secret string) {
jwtSecret = []byte(secret)
}
// Claims JWT载荷
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
// GenerateToken 生成JWT Token
func GenerateToken(userID uint, username string, expireHour int) (string, error) {
claims := Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(expireHour))),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
// ParseToken 解析JWT Token
func ParseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
文件: utils/hash.go
package utils
import "golang.org/x/crypto/bcrypt"
// HashPassword 哈希密码
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
// CheckPassword 验证密码
func CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
文件: utils/upload.go
package utils
import (
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"time"
)
// SaveUploadFile 保存上传文件
func SaveUploadFile(file *multipart.FileHeader, uploadPath string, allowedExts []string) (string, error) {
// 检查文件扩展名
ext := strings.ToLower(filepath.Ext(file.Filename))
allowed := false
for _, allowedExt := range allowedExts {
if ext == allowedExt {
allowed = true
break
}
}
if !allowed {
return "", fmt.Errorf("不支持的文件类型: %s", ext)
}
// 生成唯一文件名
filename := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename)
filepath := filepath.Join(uploadPath, filename)
// 确保目录存在
if err := os.MkdirAll(uploadPath, 0755); err != nil {
return "", err
}
// 打开上传文件
src, err := file.Open()
if err != nil {
return "", err
}
defer src.Close()
// 创建目标文件
dst, err := os.Create(filepath)
if err != nil {
return "", err
}
defer dst.Close()
// 复制文件内容
if _, err := io.Copy(dst, src); err != nil {
return "", err
}
return filename, nil
}
📊 第三步:数据模型
文件: models/user.go
package models
import (
"time"
"gorm.io/gorm"
)
// User 用户模型
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"uniqueIndex;size:50;not null" json:"username"`
Email string `gorm:"uniqueIndex;size:100;not null" json:"email"`
Password string `gorm:"size:255;not null" json:"-"`
Nickname string `gorm:"size:50" json:"nickname"`
Avatar string `gorm:"size:255" json:"avatar"`
Bio string `gorm:"type:text" json:"bio"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 关联
Articles []Article `json:"articles,omitempty"`
Comments []Comment `json:"comments,omitempty"`
}
// RegisterRequest 注册请求
type RegisterRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// UpdateProfileRequest 更新资料请求
type UpdateProfileRequest struct {
Nickname string `json:"nickname" binding:"max=50"`
Bio string `json:"bio" binding:"max=500"`
}
文件: models/article.go
package models
import (
"time"
"gorm.io/gorm"
)
// Article 文章模型
type Article struct {
ID uint `gorm:"primaryKey" json:"id"`
Title string `gorm:"size:200;not null" json:"title"`
Content string `gorm:"type:longtext;not null" json:"content"`
Summary string `gorm:"type:text" json:"summary"`
CoverImage string `gorm:"size:255" json:"cover_image"`
ViewCount int `gorm:"default:0" json:"view_count"`
LikeCount int `gorm:"default:0" json:"like_count"`
Published bool `gorm:"default:false" json:"published"`
UserID uint `gorm:"not null;index" json:"user_id"`
CategoryID uint `gorm:"index" json:"category_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 关联
User User `json:"user,omitempty"`
Category Category `json:"category,omitempty"`
Tags []Tag `gorm:"many2many:article_tags;" json:"tags,omitempty"`
Comments []Comment `json:"comments,omitempty"`
}
// CreateArticleRequest 创建文章请求
type CreateArticleRequest struct {
Title string `json:"title" binding:"required,min=1,max=200"`
Content string `json:"content" binding:"required"`
Summary string `json:"summary" binding:"max=500"`
CategoryID uint `json:"category_id"`
TagIDs []uint `json:"tag_ids"`
Published bool `json:"published"`
}
// UpdateArticleRequest 更新文章请求
type UpdateArticleRequest struct {
Title string `json:"title" binding:"omitempty,min=1,max=200"`
Content string `json:"content"`
Summary string `json:"summary" binding:"max=500"`
CategoryID uint `json:"category_id"`
TagIDs []uint `json:"tag_ids"`
Published *bool `json:"published"`
}
// Category 分类模型
type Category struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:50;not null;uniqueIndex" json:"name"`
Slug string `gorm:"size:50;not null;uniqueIndex" json:"slug"`
CreatedAt time.Time `json:"created_at"`
Articles []Article `json:"articles,omitempty"`
}
// Tag 标签模型
type Tag struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:50;not null;uniqueIndex" json:"name"`
Slug string `gorm:"size:50;not null;uniqueIndex" json:"slug"`
CreatedAt time.Time `json:"created_at"`
Articles []Article `gorm:"many2many:article_tags;" json:"articles,omitempty"`
}
文件: models/comment.go
package models
import (
"time"
"gorm.io/gorm"
)
// Comment 评论模型
type Comment struct {
ID uint `gorm:"primaryKey" json:"id"`
Content string `gorm:"type:text;not null" json:"content"`
UserID uint `gorm:"not null;index" json:"user_id"`
ArticleID uint `gorm:"not null;index" json:"article_id"`
ParentID *uint `gorm:"index" json:"parent_id"` // 父评论ID(用于回复)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// 关联
User User `json:"user,omitempty"`
Article Article `json:"article,omitempty"`
Parent *Comment `json:"parent,omitempty"`
Replies []Comment `gorm:"foreignKey:ParentID" json:"replies,omitempty"`
}
// CreateCommentRequest 创建评论请求
type CreateCommentRequest struct {
Content string `json:"content" binding:"required,min=1,max=1000"`
ArticleID uint `json:"article_id" binding:"required"`
ParentID *uint `json:"parent_id"`
}
🎮 第四步:服务层
文件: services/user.go
package services
import (
"errors"
"github.com/yourusername/blog-system/models"
"github.com/yourusername/blog-system/utils"
"gorm.io/gorm"
)
type UserService struct {
db *gorm.DB
}
func NewUserService(db *gorm.DB) *UserService {
return &UserService{db: db}
}
// Register 用户注册
func (s *UserService) Register(req models.RegisterRequest) (*models.User, error) {
// 检查用户名是否存在
var count int64
s.db.Model(&models.User{}).Where("username = ?", req.Username).Count(&count)
if count > 0 {
return nil, errors.New("用户名已存在")
}
// 检查邮箱是否存在
s.db.Model(&models.User{}).Where("email = ?", req.Email).Count(&count)
if count > 0 {
return nil, errors.New("邮箱已被注册")
}
// 哈希密码
hashedPassword, err := utils.HashPassword(req.Password)
if err != nil {
return nil, err
}
// 创建用户
user := &models.User{
Username: req.Username,
Email: req.Email,
Password: hashedPassword,
Nickname: req.Username, // 默认昵称为用户名
}
if err := s.db.Create(user).Error; err != nil {
return nil, err
}
return user, nil
}
// Login 用户登录
func (s *UserService) Login(req models.LoginRequest) (*models.User, string, error) {
var user models.User
err := s.db.Where("username = ?", req.Username).First(&user).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, "", errors.New("用户名或密码错误")
}
return nil, "", err
}
// 验证密码
if !utils.CheckPassword(req.Password, user.Password) {
return nil, "", errors.New("用户名或密码错误")
}
// 生成Token
token, err := utils.GenerateToken(user.ID, user.Username, 24*7)
if err != nil {
return nil, "", err
}
return &user, token, nil
}
// GetByID 根据ID获取用户
func (s *UserService) GetByID(id uint) (*models.User, error) {
var user models.User
err := s.db.First(&user, id).Error
return &user, err
}
// UpdateProfile 更新用户资料
func (s *UserService) UpdateProfile(userID uint, req models.UpdateProfileRequest) error {
return s.db.Model(&models.User{}).Where("id = ?", userID).Updates(map[string]interface{}{
"nickname": req.Nickname,
"bio": req.Bio,
}).Error
}
// UpdateAvatar 更新头像
func (s *UserService) UpdateAvatar(userID uint, avatar string) error {
return s.db.Model(&models.User{}).Where("id = ?", userID).Update("avatar", avatar).Error
}
文件: services/article.go
package services
import (
"github.com/yourusername/blog-system/models"
"gorm.io/gorm"
)
type ArticleService struct {
db *gorm.DB
}
func NewArticleService(db *gorm.DB) *ArticleService {
return &ArticleService{db: db}
}
// Create 创建文章
func (s *ArticleService) Create(userID uint, req models.CreateArticleRequest) (*models.Article, error) {
article := &models.Article{
Title: req.Title,
Content: req.Content,
Summary: req.Summary,
CategoryID: req.CategoryID,
UserID: userID,
Published: req.Published,
}
// 开启事务
tx := s.db.Begin()
// 创建文章
if err := tx.Create(article).Error; err != nil {
tx.Rollback()
return nil, err
}
// 关联标签
if len(req.TagIDs) > 0 {
var tags []models.Tag
tx.Find(&tags, req.TagIDs)
if err := tx.Model(article).Association("Tags").Replace(tags); err != nil {
tx.Rollback()
return nil, err
}
}
tx.Commit()
// 重新加载关联数据
s.db.Preload("User").Preload("Category").Preload("Tags").First(article, article.ID)
return article, nil
}
// GetByID 获取文章详情
func (s *ArticleService) GetByID(id uint) (*models.Article, error) {
var article models.Article
err := s.db.Preload("User").Preload("Category").Preload("Tags").
First(&article, id).Error
if err == nil {
// 增加浏览量
s.db.Model(&article).UpdateColumn("view_count", gorm.Expr("view_count + ?", 1))
}
return &article, err
}
// GetList 获取文章列表
func (s *ArticleService) GetList(page, pageSize int, categoryID, userID uint, keyword string) ([]models.Article, int64, error) {
var articles []models.Article
var total int64
query := s.db.Model(&models.Article{}).Where("published = ?", true)
// 筛选条件
if categoryID > 0 {
query = query.Where("category_id = ?", categoryID)
}
if userID > 0 {
query = query.Where("user_id = ?", userID)
}
if keyword != "" {
query = query.Where("title LIKE ? OR content LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
}
// 统计总数
query.Count(&total)
// 分页查询
offset := (page - 1) * pageSize
err := query.Preload("User").Preload("Category").Preload("Tags").
Offset(offset).Limit(pageSize).
Order("created_at desc").
Find(&articles).Error
return articles, total, err
}
// Update 更新文章
func (s *ArticleService) Update(articleID, userID uint, req models.UpdateArticleRequest) (*models.Article, error) {
var article models.Article
if err := s.db.First(&article, articleID).Error; err != nil {
return nil, err
}
// 检查权限
if article.UserID != userID {
return nil, gorm.ErrPermissionDenied
}
// 更新字段
updates := make(map[string]interface{})
if req.Title != "" {
updates["title"] = req.Title
}
if req.Content != "" {
updates["content"] = req.Content
}
if req.Summary != "" {
updates["summary"] = req.Summary
}
if req.CategoryID > 0 {
updates["category_id"] = req.CategoryID
}
if req.Published != nil {
updates["published"] = *req.Published
}
tx := s.db.Begin()
if err := tx.Model(&article).Updates(updates).Error; err != nil {
tx.Rollback()
return nil, err
}
// 更新标签
if len(req.TagIDs) > 0 {
var tags []models.Tag
tx.Find(&tags, req.TagIDs)
if err := tx.Model(&article).Association("Tags").Replace(tags); err != nil {
tx.Rollback()
return nil, err
}
}
tx.Commit()
// 重新加载
s.db.Preload("User").Preload("Category").Preload("Tags").First(&article, article.ID)
return &article, nil
}
// Delete 删除文章
func (s *ArticleService) Delete(articleID, userID uint) error {
var article models.Article
if err := s.db.First(&article, articleID).Error; err != nil {
return err
}
// 检查权限
if article.UserID != userID {
return gorm.ErrPermissionDenied
}
return s.db.Delete(&article).Error
}
🚦 第五步:中间件
文件: middleware/auth.go
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/yourusername/blog-system/utils"
)
// AuthMiddleware JWT认证中间件
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取Authorization header
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "请先登录",
})
c.Abort()
return
}
// 提取Token
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Token格式错误",
})
c.Abort()
return
}
// 解析Token
claims, err := utils.ParseToken(parts[1])
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Token无效或已过期",
})
c.Abort()
return
}
// 将用户信息存入上下文
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Next()
}
}
文件: middleware/cors.go
package middleware
import (
"github.com/gin-gonic/gin"
)
// CORSMiddleware CORS中间件
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
🌐 第六步:HTTP处理器(仅展示核心部分)
文件: handlers/user.go
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yourusername/blog-system/models"
"github.com/yourusername/blog-system/services"
"github.com/yourusername/blog-system/utils"
)
type UserHandler struct {
userService *services.UserService
uploadPath string
}
func NewUserHandler(userService *services.UserService, uploadPath string) *UserHandler {
return &UserHandler{
userService: userService,
uploadPath: uploadPath,
}
}
// Register 用户注册
func (h *UserHandler) Register(c *gin.Context) {
var req models.RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := h.userService.Register(req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "注册成功",
"user": user,
})
}
// Login 用户登录
func (h *UserHandler) Login(c *gin.Context) {
var req models.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, token, err := h.userService.Login(req)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "登录成功",
"token": token,
"user": user,
})
}
// GetProfile 获取个人资料
func (h *UserHandler) GetProfile(c *gin.Context) {
userID := c.GetUint("user_id")
user, err := h.userService.GetByID(userID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
c.JSON(http.StatusOK, user)
}
// UploadAvatar 上传头像
func (h *UserHandler) UploadAvatar(c *gin.Context) {
userID := c.GetUint("user_id")
file, err := c.FormFile("avatar")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择文件"})
return
}
// 保存文件
filename, err := utils.SaveUploadFile(file, h.uploadPath+"/avatars", []string{".jpg", ".jpeg", ".png", ".gif"})
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 更新用户头像
avatarURL := "/uploads/avatars/" + filename
if err := h.userService.UpdateAvatar(userID, avatarURL); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "上传成功",
"avatar": avatarURL,
})
}
🏃 第七步:主程序
文件: main.go
package main
import (
"fmt"
"log"
"github.com/gin-gonic/gin"
"github.com/yourusername/blog-system/config"
"github.com/yourusername/blog-system/handlers"
"github.com/yourusername/blog-system/middleware"
"github.com/yourusername/blog-system/models"
"github.com/yourusername/blog-system/services"
"github.com/yourusername/blog-system/utils"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
// 加载配置
cfg := config.LoadConfig()
// 初始化JWT
utils.InitJWT(cfg.JWT.Secret)
// 连接数据库
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.Database.User, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Database)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("数据库连接失败:", err)
}
// 自动迁移
db.AutoMigrate(&models.User{}, &models.Article{}, &models.Comment{}, &models.Category{}, &models.Tag{})
// 初始化服务
userService := services.NewUserService(db)
articleService := services.NewArticleService(db)
// 初始化处理器
userHandler := handlers.NewUserHandler(userService, cfg.Upload.Path)
articleHandler := handlers.NewArticleHandler(articleService, cfg.Upload.Path)
// 创建Gin引擎
router := gin.Default()
// 中间件
router.Use(middleware.CORSMiddleware())
// 静态文件
router.Static("/uploads", cfg.Upload.Path)
// 公开路由
public := router.Group("/api")
{
public.POST("/register", userHandler.Register)
public.POST("/login", userHandler.Login)
public.GET("/articles", articleHandler.GetList)
public.GET("/articles/:id", articleHandler.GetDetail)
}
// 需要认证的路由
auth := router.Group("/api")
auth.Use(middleware.AuthMiddleware())
{
// 用户相关
auth.GET("/profile", userHandler.GetProfile)
auth.PUT("/profile", userHandler.UpdateProfile)
auth.POST("/upload/avatar", userHandler.UploadAvatar)
// 文章相关
auth.POST("/articles", articleHandler.Create)
auth.PUT("/articles/:id", articleHandler.Update)
auth.DELETE("/articles/:id", articleHandler.Delete)
}
// 启动服务器
log.Printf("🚀 博客系统启动在 http://localhost:%s", cfg.Server.Port)
router.Run(":" + cfg.Server.Port)
}
🧪 测试API
# 1. 注册用户
curl -X POST http://localhost:8080/api/register \
-H "Content-Type: application/json" \
-d '{"username":"test","email":"test@example.com","password":"123456"}'
# 2. 登录
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"123456"}'
# 3. 创建文章(需要Token)
curl -X POST http://localhost:8080/api/articles \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"title":"我的第一篇文章","content":"这是文章内容","published":true}'
# 4. 获取文章列表
curl http://localhost:8080/api/articles
# 5. 上传头像
curl -X POST http://localhost:8080/api/upload/avatar \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "avatar=@avatar.jpg"
💡 扩展思考
- 添加富文本编辑器 - 使用Markdown或富文本编辑
- 邮件验证 - 注册时发送验证邮件
- 社交登录 - 支持GitHub、Google登录
- 文章点赞收藏 - 添加点赞和收藏功能
- 全文搜索 - 使用Elasticsearch
- 评论审核 - 管理员审核评论
- RSS订阅 - 生成RSS feed
- 数据统计 - 文章阅读统计、用户活跃度
🎯 小结
恭喜你完成了所有6个实战项目!通过这个博客系统,你学会了:
✅ 完整的用户认证系统(JWT)
✅ RESTful API设计和实现
✅ 数据库关系设计和CRUD操作
✅ 文件上传和处理
✅ 中间件的使用
✅ 企业级Web应用架构
现在你已经具备了独立开发Go Web应用的能力!
