Go语言快速入门Go语言快速入门
首页
基础篇
进阶篇
高阶篇
实战篇
Go官方网站
编程指南
首页
基础篇
进阶篇
高阶篇
实战篇
Go官方网站
编程指南
  • 实战篇

    • 🚀 实战篇
    • 第1个项目 - 命令行文件管理器
    • 第2个项目 - RESTful API Todo服务
    • 第3个项目 - Web爬虫 新闻采集器
    • 第4个项目 - 实时聊天室 WebSocket
    • 第5个项目 - URL短链接服务
    • 第6个项目 - 完整博客系统

第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"

💡 扩展思考

  1. 添加富文本编辑器 - 使用Markdown或富文本编辑
  2. 邮件验证 - 注册时发送验证邮件
  3. 社交登录 - 支持GitHub、Google登录
  4. 文章点赞收藏 - 添加点赞和收藏功能
  5. 全文搜索 - 使用Elasticsearch
  6. 评论审核 - 管理员审核评论
  7. RSS订阅 - 生成RSS feed
  8. 数据统计 - 文章阅读统计、用户活跃度

🎯 小结

恭喜你完成了所有6个实战项目!通过这个博客系统,你学会了:

✅ 完整的用户认证系统(JWT)
✅ RESTful API设计和实现
✅ 数据库关系设计和CRUD操作
✅ 文件上传和处理
✅ 中间件的使用
✅ 企业级Web应用架构

现在你已经具备了独立开发Go Web应用的能力!


🎉 恭喜你!

完成了所有6个实战项目,你已经掌握了Go开发的核心技能!

  • 继续深入学习Go的高级特性
  • 尝试开发自己的项目
  • 加入开源社区贡献代码
  • 访问 编程指南 获取更多资源
最近更新: 2025/12/27 13:26
Contributors: 王长安
Prev
第5个项目 - URL短链接服务