全栈开发实战(一)——简易博客社区后端搭建教程(附源码)

全栈开发实战(一)——简易博客社区后端搭建

项目展示视频
项目Github地址

(一)项目准备

在项目开始前,首先确保你已安装好Go语言并配置好Go语言编辑器,同时安装好MySQL或其他数据库,其次,为了方便调试,可安装相应的接口测试工具和数据库可视化工具

本项目所使用的工具如下:

工具说明
GoLand编写并运行程序
Postman接口测试
Navicat Premiun数据库可视化

1. 创建项目

打开Goland,创建一个新的项目blog_server

在这里插入图片描述

为了更方便的import包,建议修改镜像路径如下:

GOPROXY=https://goproxy.io,direct

2. 模块安装

安装gin——一个golang的微框架

go get -u github.com/gin-gonic/gin

安装gorm——Golang语言中一款性能极好的ORM库

go get -u github.com/jinzhu/gorm

安装mysql驱动——用于操作数据库

go get github.com/go-sql-driver/mysql

安装jwt包——用于生成和验证token

go get github.com/dgrijalva/jwt-go

安装uuid包——用于生成id

go get -u -v github.com/satori/go.uuid

3. 创建数据库

从终端进入mysql,创建数据库blog

在这里插入图片描述

4. 创建静态文件目录

在项目文件夹下新建static文件夹,在该文件夹下新建images文件夹并放入一张png格式图片,将图片命名为default_avatar.png作为用户的初始头像(也可以是其他格式,请自行修改)

(二)登录注册接口

1. 用户模型

在项目文件夹下新建model文件夹,该文件夹存放项目的数据结构模型

我们先来构建用户结构体User,User继承gorm.Model,可自动添加id等基本字段。这里需要注意,为了实现收藏与关注功能,每个用户还有一个Collects和Following字段,用于保存收藏的文章编号和关注的用户编号,该字段的数据类型为数组

而UserInfo为部分的用户信息,便于将数据库的查询结果绑定到结构体上

新建user.go写入:

/* model/user.go */
type User struct {
	gorm.Model
	UserName    string `gorm:"varchar(20);not null"`
	PhoneNumber string `gorm:"varchar(20);not null;unique"`
	Password    string `gorm:"size:255;not null"`
	Avatar      string `gorm:"size:255;not null"`
	Collects    Array  `gorm:"type:longtext"`
	Following   Array  `gorm:"type:longtext"`
	Fans        int    `gorm:"AUTO_INCREMENT"`
}

type UserInfo struct {
	ID       uint   `json:"id"`
	Avatar   string `json:"avatar"`
	UserName string `json:"userName"`
}

由于数据库本身无数组这一数据结构,我们需要自定义,并在数据存取时进行格式转换,即将数据存到数据库时,对数据进行处理,获得数据库支持的类型,而从数据库读取数据后,对其进行处理,获得Go类型的变量

新建array.go写入:

/* model/array.go */
type Array []string

// Scan 从数据库读取数据后,对其进行处理,获得Go类型的变量
func (m *Array) Scan(val interface{}) error {
	s := val.([]uint8)
	ss := strings.Split(string(s), "|")
	*m = ss
	return nil
}

// Value 将数据存到数据库时,对数据进行处理,获得数据库支持的类型
func (m Array) Value() (driver.Value, error) {
	str := strings.Join(m, "|")
	return str, nil
}

2. 连接数据库

在项目文件夹下新建common文件夹,该文件夹存放项目的一些通用功能

新建文件database.go,编写数据库初始化函数InitDB()与数据库数据获取函数GetDB()

/* common/database.go */
var DB *gorm.DB

// InitDB() 数据库初始化
func InitDB() *gorm.DB {
	driverName := "mysql"
	user := "root"
	password := "你的Mysql root用户的密码"
	host := "localhost"
	port := "3306"
	database := "blog"
	charset := "utf8"
	args := fmt.Sprintf("%s:%s@(%s:%s)/%s?charset=%s&parseTime=true",
		user,
		password,
		host,
		port,
		database,
		charset)
	// 连接数据库
	db, err := gorm.Open(driverName, args)
	if err != nil {
		panic("failed to open database: " + err.Error())
	}
	// 迁移数据表
	db.AutoMigrate(&model.User{})
	DB = db
	return db
}

// 数据库信息获取
func GetDB() *gorm.DB {
	return DB
}

3. 注册功能

在项目文件夹下新建cotroller文件夹,该文件夹存放主要的操作函数

新建UserController.go编写与用户有关的函数

我们先来编写注册函数Register(),用户注册函数的流程包括获取参数、数据验证、密码加密、创建用户、返回结果,在创建用户时,我们为用户写入一个默认的头像

/* controller/UserController.go */
// Register 注册
func Register(c *gin.Context) {
	db := common.GetDB()
	// 获取参数
	var requestUser model.User
	c.Bind(&requestUser)
	userName := requestUser.UserName
	phoneNumber := requestUser.PhoneNumber
	password := requestUser.Password
	// 数据验证
	var user model.User
	db.Where("phone_number = ?", phoneNumber).First(&user)
	if user.ID != 0 {
		c.JSON(http.StatusOK, gin.H{
			"code": 422,
			"msg":  "用户已存在",
		})
		return
	}
	// 密码加密
	hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	// 创建用户
	newUser := model.User{
		UserName:    userName,
		PhoneNumber: phoneNumber,
		Password:    string(hashedPassword),
		Avatar:      "/images/default_avatar.png",
		Collects:    model.Array{},
		Following:   model.Array{},
		Fans:        0,
	}
	db.Create(&newUser)
	// 返回结果
	c.JSON(http.StatusOK, gin.H{
		"code": 200,
		"msg":  "注册成功",
	})
}

4. 生成token

由于用户登录成功后我们需要为他发放一个token,我们先来编写token生成函数ReleaseToken()

在common文件夹下新建文件jwt.go,编写生成token的函数:

/* common/jwt.go */
// jwt加密密钥
var jwtKey = []byte("a_secret_key")

type Claims struct {
	UserId uint
	jwt.StandardClaims
}

// ReleaseToken 生成token
func ReleaseToken(user model.User) (string, error) {
	// token的有效期
	expirationTime := time.Now().Add(7 * 24 * time.Hour)
	claims := &Claims{
		// 自定义字段
		UserId: user.ID,
		// 标准字段
		StandardClaims: jwt.StandardClaims{
			// 过期时间
			ExpiresAt: expirationTime.Unix(),
			// 发放时间
			IssuedAt: time.Now().Unix(),
		},
	}
	// 使用jwt密钥生成token
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString(jwtKey)
	if err != nil {
		return "", err
	}
	// 返回token
	return tokenString, nil
}

5. 登录功能

我们接下来编写登录函数,用户登录函数的流程包括获取参数、数据验证、判断密码是否正确、发放token、返回结果,发放token时调用刚刚编写的ReleaseToke()

/* controller/UserController.go */
// Login 登录
func Login(c *gin.Context) {
	db := common.GetDB()
	// 获取参数
	var requestUser model.User
	c.Bind(&requestUser)
	phoneNumber := requestUser.PhoneNumber
	password := requestUser.Password
	// 数据验证
	var user model.User
	db.Where("phone_number =?", phoneNumber).First(&user)
	if user.ID == 0 {
		c.JSON(http.StatusOK, gin.H{
			"code": 422,
			"msg":  "用户不存在",
		})
		return
	}
	// 判断密码是否正确
	if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
		c.JSON(http.StatusOK, gin.H{
			"code": 422,
			"msg":  "密码错误",
		})
		return
	}
	// 发放token
	token, err := common.ReleaseToken(user)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"code": 500,
			"msg":  "系统异常",
		})
		return
	}
	// 返回结果
	c.JSON(http.StatusOK, gin.H{
		"code": 200,
		"data": gin.H{"token": token},
		"msg":  "登录成功",
	})
}

6. 解析token

前端接收到返回的token后会将其保存,当请求需要token验证的接口时再发送给后端,此时,后端就需要对token进行解析,识别出用户的身份

我们回到文件jwt.go,编写解析token的函数

/* common/jwt.go */
// ParseToken 解析token
func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
	claims := &Claims{}
	token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (i interface{}, err error) {
		return jwtKey, nil
	})
	return token, claims, err
}

7. 中间件验证

接下来我们编写一个中间件,获取到前端请求中的token,调用ParseToken()对其进行解析,若token不合规范,该请求将会被抛弃,当token符合规范时才可以进行下一步操作

在项目文件夹下新建文件夹middleware,该文件夹存放项目所需的中间件

新建文件AuthMiddleware.go,编写中间件AuthMiddleware()

/* middl1e/AuthMiddleware.go */
func AuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 获取authorization header
		tokenString := c.Request.Header.Get("Authorization")
		// token为空
		if tokenString == "" {
			c.JSON(http.StatusOK, gin.H{
				"code": 401,
				"msg":  "权限不足",
			})
			c.Abort()
			return
		}
		// 非法token
		if tokenString == "" || len(tokenString) < 7 || !strings.HasPrefix(tokenString, "Bearer") {
			c.JSON(http.StatusUnauthorized, gin.H{
				"code": 401,
				"msg":  "权限不足",
			})
			c.Abort()
			return
		}
		// 提取token的有效部分
		tokenString = tokenString[7:]
		// 解析token
		token, claims, err := common.ParseToken(tokenString)
		// 非法token
		if err != nil || !token.Valid {
			c.JSON(http.StatusUnauthorized, gin.H{
				"code": 401,
				"msg":  "权限不足",
			})
			c.Abort()
			return
		}
		// 获取claims中的userId
		userId := claims.UserId
		DB := common.GetDB()
		var user model.User
		DB.Where("id =?", userId).First(&user)
		// 将用户信息写入上下文便于读取
		c.Set("user", user)
		c.Next()
	}
}

为了测试中间件,我们编写一个需要传token的函数,对前端发送的token进行解析并返回用户的部分信息

/* controller/UserController.go */
// GetInfo 登录后获取信息
func GetInfo(c *gin.Context) {
	// 获取上下文中的用户信息
	user, _ := c.Get("user")
	// 返回用户信息
	//response.Success(c, gin.H{"id": user.(model.User).ID, "avatar": user.(model.User).Avatar}, "登录获取信息成功")
	c.JSON(http.StatusOK, gin.H{
		"code": 200,
		"data": gin.H{"id": user.(model.User).ID, "avatar": user.(model.User).Avatar},
		"msg":  "登录获取信息成功",
	})
}

8. 编写路由

在项目文件夹下新建文件夹routes并新建文件routes.go,写入我们的路由

/* routes/rotues.go */
func CollectRoutes(r *gin.Engine) *gin.Engine {
	// 允许跨域访问
	r.Use(middleware.CORSMiddleware())
	// 注册
	r.POST("/register", controller.Register)
	// 登录
	r.POST("/login", controller.Login)
    // 登录获取用户信息
    r.GET("/user", middleware.AuthMiddleware(),controller.GetInfo)

	return r
}

新建文件main.go文件,将package改为main并写入连接数据库、设置静态文件夹路径、创建并启动路由的相关语句

func main() {
	// 获取初始化的数据库
	db := common.InitDB()
	// 延迟关闭数据库
	defer db.Close()
	// 创建路由引擎
	r := gin.Default()
	// 配置静态文件路径
	r.StaticFS("/images", http.Dir("./static/images"))
	// 启动路由
	routes.CollectRoutes(r)
	// 启动服务
	panic(r.Run(":8080"))
}

这里有个小坑,记得在main.go中手动import mysql-driver

_ "github.com/go-sql-driver/mysql"

8. 接口测试

在控制台输入go run main.go运行项目

打开Postman进行接口测试

注册接口测试

在这里插入图片描述

数据库新增了用户信息

在这里插入图片描述

登录接口测试

在这里插入图片描述

我们将token写入登录获取信息接口的头部,测试中间件

在这里插入图片描述

加上后端地址打开图片,测试静态文件目录设置是否正确

在这里插入图片描述

(三)图片上传接口

1. 上传图像功能

在controller文件夹下新建文件FileController.go,编写上传图像函数Upload,该函数接收前端传来的图片文件,保存于后端的静态文件夹并返回图片url

/* controller/FileController.go */
// Upload 上传图像
func Upload(c *gin.Context) {
	file, header, err := c.Request.FormFile("file")
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"code": 500,
			"msg":  "格式错误",
		})
		return
	}
	filename := header.Filename
	ext := path.Ext(filename)
    // 用上传时间作为文件名
	name := "image_" + time.Now().Format("20060102150405")
	newFilename := name + ext
	out, err := os.Create("static/images/" + newFilename)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"code": 500,
			"msg":  "创建错误",
		})
		return
	}
	defer out.Close()
	_, err = io.Copy(out, file)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"code": 500,
			"msg":  "复制错误",
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"code": 200,
		"data": gin.H{"filePath": "/images/" + newFilename},
		"msg":  "上传成功",
	})
}

2. 修改路由

在routes.go中添加图像上传的路由

/* model/category.go */
func CollectRoutes(r *gin.Engine) *gin.Engine {
	// 注册
	r.POST("/register", controller.Register)
	// 登录
	r.POST("/login", controller.Login)
    // 登录获取用户信息
    r.GET("/user", middleware.AuthMiddleware(),controller.GetInfo)
	// 上传图像
	r.POST("/upload", controller.Upload)

	return r
}

3. 接口测试

测试图像上传接口

在这里插入图片描述

在这里插入图片描述

(四)分类查询接口

1. 分类表

为实现对文章进行分类,我们需要一个分类表(当然也可以写对分类增删查改的接口,不过我为了省事就跳过啦)

在数据库blog中新建表categories,添加字段id及category_name并插入一些数据,其中id为0定义为全部,不对文章进行分类

在这里插入图片描述

2. 分类模型

在model文件夹下新建category.go,构建分类模型

/* model/category.go */
type Category struct {
	ID           uint   `json:"id" gorm:"type:char(36);primary_key;"`
	CategoryName string `json:"name" gorm:"type:varchar(50);not null"`
}

3. 查询分类

在cotroller文件夹下新建文件CategoryController.go,编写查询全部分类的函数SearchCategory()和按分类id查询分类名的函数SearchCategoryName()

/* controller/CategoryController.go */
// SearchCategory 查询分类
func SearchCategory(c *gin.Context) {
   db := common.GetDB()
   var categories []model.Category
   if err := db.Find(&categories).Error; err != nil {
      response.Fail(c, nil, "查找失败")
      return
   }
   response.Success(c, gin.H{"categories": categories}, "查找成功")
}

// SearchCategoryName 查询分类名
func SearchCategoryName(c *gin.Context) {
   db := common.GetDB()
   var category model.Category
   // 获取path中的分类id
   categoryId := c.Params.ByName("id")
   if err := db.Where("id = ?", categoryId).First(&category).Error; err != nil {
      response.Fail(c, nil, "分类不存在")
      return
   }
   response.Success(c, gin.H{"categoryName": category.CategoryName}, "查找成功")
}

4. 修改路由

/* routes/routes.go */
func CollectRoutes(r *gin.Engine) *gin.Engine {
	// 允许跨域访问
	r.Use(middleware.CORSMiddleware())
	// 注册
	r.POST("/register", controller.Register)
	// 登录
	r.POST("/login", controller.Login)
    // 登录获取用户信息
    r.GET("/user", middleware.AuthMiddleware(),controller.GetInfo)
	// 上传图像
	r.POST("/upload", controller.Upload)
	// 查询分类
	r.GET("/category", controller.SearchCategory)         // 查询分类
	r.GET("/category/:id", controller.SearchCategoryName) // 查询分类名
	return r
}

5. 测试接口

测试分类接口

在这里插入图片描述

在这里插入图片描述

(五)文章增删查改接口

1. 文章模型

在model文件夹下新建article.go文件夹,写入数据库存储的文章数据结构以及返回的文章数据结构,这里我们将使用uuid生成文章id

/* model/article.go */
type Article struct {
	ID         uuid.UUID `json:"id" gorm:"type:char(36);primary_key;"`
	UserId     uint      `json:"user_id" gorm:"not null"`
	CategoryId uint      `json:"category_id" gorm:"not null"`
	Title      string    `json:"title" gorm:"type:varchar(50);not null"`
	Content    string    `json:"content" gorm:"type:text;not null"`
	HeadImage  string    `json:"head_image"`
	CreatedAt  Time      `json:"created_at" gorm:"type:timestamp"`
	UpdatedAt  Time      `json:"updated_at" gorm:"type:timestamp"`
}

type ArticleInfo struct {
	ID         string `json:"id"`
	CategoryId uint   `json:"category_id"`
	Title      string `json:"title"`
	Content    string `json:"content"`
	HeadImage  string `json:"head_image"`
	CreatedAt  Time   `json:"created_at"`
}

// BeforeCreate 在创建文章之前将id赋值
func (a *Article) BeforeCreate(s *gorm.Scope) error {
	return s.SetColumn("ID", uuid.NewV4())
}

我们定义一个Time类型,将时间戳转化为实际时间

/* model/time.go */
const timeFormat = "2006-01-02 15:04:05"
const timezone = "Asia/Shanghai"

type Time time.Time

func (t Time) MarshalJSON() ([]byte, error) {
	b := make([]byte, 0, len(timeFormat)+2)
	b = append(b, '"')
	b = time.Time(t).AppendFormat(b, timeFormat)
	b = append(b, '"')
	return b, nil
}

func (t *Time) UnmarshalJSON(data []byte) (err error) {
	now, _ := time.ParseInLocation(`"`+timeFormat+`"`, string(data), time.Local)
	*t = Time(now)
	return
}

func (t Time) String() string {
	return time.Time(t).Format(timeFormat)
}

func (t Time) local() time.Time {
	loc, _ := time.LoadLocation(timezone)
	return time.Time(t).In(loc)
}

func (t Time) Value() (driver.Value, error) {
	var zeroTime time.Time
	var ti = time.Time(t)
	if ti.UnixNano() == zeroTime.UnixNano() {
		return nil, nil
	}
	return ti, nil
}

func (t *Time) Scan(v interface{}) error {
	value, ok := v.(time.Time)
	if ok {
		*t = Time(value)
		return nil
	}
	return fmt.Errorf("can not convert %v to timestamp", v)
}

同时,在InitDB()中加入loc确定时区

/* common/database.go */
// InitDB() 数据库初始化
func InitDB() *gorm.DB {
	driverName := "mysql"
	user := "root"
	password := "你的Mysql root用户的密码"
	host := "localhost"
	port := "3306"
	database := "blog"
	charset := "utf8"
	loc := "Asia/Shanghai"
	args := fmt.Sprintf("%s:%s@(%s:%s)/%s?charset=%s&parseTime=true&loc=%s",
		user,
		password,
		host,
		port,
		database,
		charset,
		url.QueryEscape(loc))
	// 连接数据库
	db, err := gorm.Open(driverName, args)
	if err != nil {
		panic("failed to open database: " + err.Error())
	}
	// 迁移数据表
	db.AutoMigrate(&model.User{})
	DB = db
	return db
}

为了方便,我们定义一个请求数据时的文章数据类型,保留文章增删查改操作的基本字段,以便后端接整个结构体并验证

在项目文件夹下新建文件夹vo,并新建文件article.go

/* vo/article.go */
type CreateArticleRequest struct {
	// 加上binging用于表单验证
	CategoryId uint   `json:"category_id" binging:"required"`
	Title      string `json:"title" binging:"required"`
	Content    string `json:"content" binging:"required"`
	HeadImage  string `json:"head_image"`
}

2. 返回同一的响应格式

为减少代码量,我们为项目封装一个统一的失败与成功的返回格式

在项目文件夹下新建文件夹response并新建文件response.go,编写返回函数

/* response/response.go */
func Response(c *gin.Context, httpStatus int, code int, data gin.H, msg string) {
	c.JSON(httpStatus, gin.H{"code": code, "data": data, "msg": msg})
}

// Success 成功
func Success(c *gin.Context, data gin.H, msg string) {
	Response(c, http.StatusOK, 200, data, msg)
}

// Fail 失败
func Fail(c *gin.Context, data gin.H, msg string) {
	Response(c, http.StatusOK, 400, data, msg)
}

2. 文章增删改查功能

在文件夹controller下新建文件ArticleController.go,编写文章增删改查的操作函数,其中List()函数返回关键字和分类查询的结果,count为满足关键字和分类的数据条数,便于前端进行分页

type ArticleController struct {
	DB *gorm.DB
}

type IArticleController interface {
	Create(c *gin.Context)
	Update(c *gin.Context)
	Delete(c *gin.Context)
	Show(c *gin.Context)
	List(c *gin.Context)
}

func (a ArticleController) Create(c *gin.Context) {
	var articleRequest vo.CreateArticleRequest
	// 数据验证
	if err := c.ShouldBindJSON(&articleRequest); err != nil {
		response.Fail(c, nil, "数据错误")
		return
	}
	// 获取登录用户
	user, _ := c.Get("user")
	// 创建文章
	article := model.Article{
		UserId:     user.(model.User).ID,
		CategoryId: articleRequest.CategoryId,
		Title:      articleRequest.Title,
		Content:    articleRequest.Content,
		HeadImage:  articleRequest.HeadImage,
	}
	if err := a.DB.Create(&article).Error; err != nil {
		response.Fail(c, nil, "发布失败")
		return
	}
	response.Success(c, gin.H{"id": article.ID}, "发布成功")
}

func (a ArticleController) Update(c *gin.Context) {
	var articleRequest vo.CreateArticleRequest
	// 数据验证
	if err := c.ShouldBindJSON(&articleRequest); err != nil {
		response.Fail(c, nil, "数据错误")
		return
	}
	// 获取path中的id
	articleId := c.Params.ByName("id")
	// 查找文章
	var article model.Article
	if a.DB.Where("id = ?", articleId).First(&article).RecordNotFound() {
		response.Fail(c, nil, "文章不存在")
		return
	}
	// 获取登录用户
	user, _ := c.Get("user")
	userId := user.(model.User).ID
	if userId != article.UserId {
		response.Fail(c, nil, "登录用户不正确")
		return
	}
	// 更新文章
	if err := a.DB.Model(&article).Update(articleRequest).Error; err != nil {
		response.Fail(c, nil, "修改失败")
		return
	}
	response.Success(c, nil, "修改成功")
}

func (a ArticleController) Delete(c *gin.Context) {
	// 获取path中的id
	articleId := c.Params.ByName("id")
	// 查找文章
	var article model.Article
	if a.DB.Where("id = ?", articleId).First(&article).RecordNotFound() {
		response.Fail(c, nil, "文章不存在")
		return
	}
	// 获取登录用户
	user, _ := c.Get("user")
	userId := user.(model.User).ID
	if userId != article.UserId {
		response.Fail(c, nil, "登录用户不正确")
		return
	}
	// 删除文章
	if err := a.DB.Delete(&article).Error; err != nil {
		response.Fail(c, nil, "删除失败")
		return
	}
	response.Success(c, nil, "删除成功")
}

func (a ArticleController) Show(c *gin.Context) {
	// 获取path中的id
	articleId := c.Params.ByName("id")
	// 查找文章
	var article model.Article
	if a.DB.Where("id = ?", articleId).First(&article).RecordNotFound() {
		response.Fail(c, nil, "文章不存在")
		return
	}
	// 展示文章详情
	response.Success(c, gin.H{"article": article}, "查找成功")
}

func (a ArticleController) List(c *gin.Context) {
	// 获取关键词、分类、分页参数
	keyword := c.DefaultQuery("keyword", "")
	categoryId := c.DefaultQuery("categoryId", "0")
	pageNum, _ := strconv.Atoi(c.DefaultQuery("pageNum", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "5"))
	var query []string
	var args []string
	// 若关键词存在
	if keyword != "" {
		query = append(query, "(title LIKE ? OR content LIKE ?)")
		args = append(args, "%"+keyword+"%")
		args = append(args, "%"+keyword+"%")
	}
	// 若分类存在
	if categoryId != "0" {
		query = append(query, "category_id = ?")
		args = append(args, categoryId)
	}
	// 拼接字符串
	var querystr string
	if len(query) > 0 {
		querystr = strings.Join(query, " AND ")
	}
	// 页面内容
	var article []model.ArticleInfo
	// 文章总数
	var count int
	// 查询文章
	switch len(args) {
	case 0:
		a.DB.Table("articles").Select("id, category_id, title, LEFT(content,80) AS content, head_image, created_at").Order("created_at desc").Offset((pageNum - 1) * pageSize).Limit(pageSize).Find(&article)
		a.DB.Model(model.Article{}).Count(&count)
	case 1:
		a.DB.Table("articles").Select("id, category_id, title, LEFT(content,80) AS content, head_image, created_at").Where(querystr, args[0]).Order("created_at desc").Offset((pageNum - 1) * pageSize).Limit(pageSize).Find(&article)
		a.DB.Model(model.Article{}).Where(querystr, args[0]).Count(&count)
	case 2:
		a.DB.Table("articles").Select("id, category_id, title, LEFT(content,80) AS content, head_image, created_at").Where(querystr, args[0], args[1]).Order("created_at desc").Offset((pageNum - 1) * pageSize).Limit(pageSize).Find(&article)
		a.DB.Model(model.Article{}).Where(querystr, args[0], args[1]).Count(&count)
	case 3:
		a.DB.Table("articles").Select("id, category_id, title, LEFT(content,80) AS content, head_image, created_at").Where(querystr, args[0], args[1], args[2]).Order("created_at desc").Offset((pageNum - 1) * pageSize).Limit(pageSize).Find(&article)
		a.DB.Model(model.Article{}).Where(querystr, args[0], args[1], args[2]).Count(&count)
	}
	// 展示文章列表
	response.Success(c, gin.H{"article": article, "count": count}, "查找成功")
}

func NewArticleController() IArticleController {
	db := common.GetDB()
	db.AutoMigrate(model.Article{})
	return ArticleController{DB: db}
}

3. 修改路由

我们使用路由组简化路由的表达

/* routes/routes.go */
func CollectRoutes(r *gin.Engine) *gin.Engine {
	// 允许跨域访问
	r.Use(middleware.CORSMiddleware())
	// 注册
	r.POST("/register", controller.Register)
	// 登录
	r.POST("/login", controller.Login)
    // 登录获取用户信息
    r.GET("/user", middleware.AuthMiddleware(),controller.GetInfo)
	// 上传图像
	r.POST("/upload", controller.Upload)
	// 查询分类
	r.GET("/category", controller.SearchCategory)         // 查询分类
	r.GET("/category/:id", controller.SearchCategoryName) // 查询分类名
    //用户文章的增删查改
	articleRoutes := r.Group("/article")
	articleController := controller.NewArticleController()
	articleRoutes.POST("", middleware.AuthMiddleware(), articleController.Create)      // 发布文章
	articleRoutes.PUT(":id", middleware.AuthMiddleware(), articleController.Update)    // 修改文章
	articleRoutes.DELETE(":id", middleware.AuthMiddleware(), articleController.Delete) // 删除文章
	articleRoutes.GET(":id", articleController.Show)                                   // 查看文章
	articleRoutes.POST("list", articleController.List) 
	return r
}

4. 接口测试

文章发布接口

在这里插入图片描述

在这里插入图片描述

文章修改接口

在这里插入图片描述

在这里插入图片描述

文章修改接口

在这里插入图片描述

在这里插入图片描述

文章关键字和分类分页查询

在这里插入图片描述

在这里插入图片描述

文章查看接口

在这里插入图片描述

(六)用户信息管理接口

1.获取简要信息

在显示文章时需要同时显示作者头像,因此我们写一个函数返回文章作者的头像、文章作者的ID以及当前登录用户的ID,以便判断文章的作者是否是登录用户

/* controller/UserController.go */
// GetBriefInfo 获取简要信息
func GetBriefInfo(c *gin.Context) {
	db := common.GetDB()
	// 获取path中的userId
	userId := c.Params.ByName("id")
	// 判断用户身份
	user, _ := c.Get("user")
	var curUser model.User
	if userId == strconv.Itoa(int(user.(model.User).ID)) {
		curUser = user.(model.User)
	} else {
		db.Where("id =?", userId).First(&curUser)
		if curUser.ID == 0 {
			response.Fail(c, nil, "用户不存在")
			return
		}
	}
	// 返回用户简要信息
	response.Success(c, gin.H{"id": curUser.ID, "name": curUser.UserName, "avatar": curUser.Avatar, "loginId": user.(model.User).ID}, "查找成功")
}

2. 获取详细信息

在用户信息的详情页,需要展示用户的头像、用户名、文章列表、收藏夹、关注列表等信息,我们写一个函数返回上述信息

由于收藏夹、关注列表是自定义类型,我们需要将其转化为字符串数组才能使用IN查询

/* controller/UserController.go */
// GetDetailedInfo 获取详细信息
func GetDetailedInfo(c *gin.Context) {
	db := common.GetDB()
	// 获取path中的userId
	userId := c.Params.ByName("id")
	// 判断用户身份
	user, _ := c.Get("user")
	//var self bool
	var curUser model.User
	if userId == strconv.Itoa(int(user.(model.User).ID)) {
		//self = true
		curUser = user.(model.User)
	} else {
		//self = false
		db.Where("id = ?", userId).First(&curUser)
		if curUser.ID == 0 {
			response.Fail(c, nil, "用户不存在")
			return
		}
	}
	// 返回用户详细信息
	var articles, collects []model.ArticleInfo
	var following []model.UserInfo
	var collist, follist []string
	collist = ToStringArray(curUser.Collects)
	follist = ToStringArray(curUser.Following)
	db.Table("articles").Select("id, category_id, title, LEFT(content,80) AS content, head_image, created_at").Where("user_id = ?", userId).Order("created_at desc").Find(&articles)
	db.Table("articles").Select("id, category_id, title, LEFT(content,80) AS content, head_image, created_at").Where("id IN (?)", collist).Order("created_at desc").Find(&collects)
	db.Table("users").Select("id, avatar, user_name").Where("id IN (?)", follist).Find(&following)
	response.Success(c, gin.H{"id": curUser.ID, "name": curUser.UserName, "avatar": curUser.Avatar, "loginId": user.(model.User).ID, "articles": articles, "collects": collects, "following": following, "fans": curUser.Fans}, "查找成功")
}

// ToStringArray 将自定义类型转化为字符串数组
func ToStringArray(l []string) (a model.Array) {
	for i := 0; i < len(a); i++ {
		l = append(l, a[i])
	}
	return l
}

2. 修改信息功能

用户允许修改头像和用户名,实现方式如下:

/* controller/UserController.go */
// ModifyAvatar 修改头像
func ModifyAvatar(c *gin.Context) {
	db := common.GetDB()
	// 获取用户ID
	user, _ := c.Get("user")
	// 获取参数
	var requestUser model.User
	c.Bind(&requestUser)
	avatar := requestUser.Avatar
	// 查找用户
	var curUser model.User
	db.Where("id = ?", user.(model.User).ID).First(&curUser)
	// 更新信息
	if err := db.Model(&curUser).Update("avatar", avatar).Error; err != nil {
		response.Fail(c, nil, "更新失败")
		return
	}
	response.Success(c, nil, "更新成功")
}

// ModifyName 修改用户名
func ModifyName(c *gin.Context) {
	db := common.GetDB()
	// 获取用户ID
	user, _ := c.Get("user")
	// 获取参数
	var requestUser model.User
	c.Bind(&requestUser)
	userName := requestUser.UserName
	// 查找用户
	var curUser model.User
	db.Where("id = ?", user.(model.User).ID).First(&curUser)
	// 更新信息
	if err := db.Model(&curUser).Update("user_name", userName).Error; err != nil {
		response.Fail(c, nil, "更新失败")
		return
	}
	response.Success(c, nil, "更新成功")
}

3. 修改路由

我们将与用户信息有关的接口写成一个路由组

/* routes/routes.go */
func CollectRoutes(r *gin.Engine) *gin.Engine {
	// 允许跨域访问
	r.Use(middleware.CORSMiddleware())
	// 注册
	r.POST("/register", controller.Register)
	// 登录
	r.POST("/login", controller.Login)
	// 上传图像
	r.POST("/upload", controller.Upload)
	// 用户信息管理
	userRoutes := r.Group("/user")
	userRoutes.Use(middleware.AuthMiddleware())
	userRoutes.GET("", controller.GetInfo)                         // 验证用户
	userRoutes.GET("briefInfo/:id", controller.GetBriefInfo)       // 获取用户简要信息
	userRoutes.GET("detailedInfo/:id", controller.GetDetailedInfo) // 获取用户详细信息
	userRoutes.PUT("avatar/:id", controller.ModifyAvatar)          // 修改头像
	userRoutes.PUT("name/:id", controller.ModifyName)              // 修改用户名
	// 查询分类
	r.GET("/category", controller.SearchCategory)         // 查询分类
	r.GET("/category/:id", controller.SearchCategoryName) // 查询分类名
	//用户文章的增删查改
	articleRoutes := r.Group("/article")
	//articleRoutes.Use(middleware.AuthMiddleware())
	articleController := controller.NewArticleController()
	articleRoutes.POST("", middleware.AuthMiddleware(), articleController.Create)      // 发布文章
	articleRoutes.PUT(":id", middleware.AuthMiddleware(), articleController.Update)    // 修改文章
	articleRoutes.DELETE(":id", middleware.AuthMiddleware(), articleController.Delete) // 删除文章
	articleRoutes.GET(":id", articleController.Show)                                   // 查看文章
	articleRoutes.POST("list", articleController.List)                                 // 显示文章列表
	return r
}

4. 接口测试

获取简要信息接口

在这里插入图片描述

获取详细信息接口

在这里插入图片描述

创建一个用户进行测试

在这里插入图片描述

修改头像接口

在这里插入图片描述

修改用户名接口

在这里插入图片描述

用户信息修改成功

在这里插入图片描述

(七)收藏关注接口

1. 收藏功能

我们来编写函数查询登录用户是否有收藏当前的文章,并编写函数实现将文章ID添加到用户收藏夹和移除用户收藏夹

/* routes/routes.go */
// Collects 查询收藏
func Collects(c *gin.Context) {
	db := common.GetDB()
	// 获取用户ID
	user, _ := c.Get("user")
	// 获取path中的id
	id := c.Params.ByName("id")
	var curUser model.User
	db.Where("id = ?", user.(model.User).ID).First(&curUser)
	// 判断是否已收藏
	for i := 0; i < len(curUser.Collects); i++ {
		if curUser.Collects[i] == id {
			response.Success(c, gin.H{"collected": true, "index": i}, "查询成功")
			return
		}
	}
	response.Success(c, gin.H{"collected": false}, "查询成功")
}

// NewCollect 新增收藏
func NewCollect(c *gin.Context) {
	db := common.GetDB()
	// 获取用户ID
	user, _ := c.Get("user")
	// 获取path中的id
	id := c.Params.ByName("id")
	// 查找用户
	var curUser model.User
	db.Where("id = ?", user.(model.User).ID).First(&curUser)
	var newCollects []string
	newCollects = append(curUser.Collects, id)
	// 更新收藏夹
	if err := db.Model(&curUser).Update("collects", newCollects).Error; err != nil {
		response.Fail(c, nil, "更新失败")
		return
	}
	response.Success(c, nil, "更新成功")
}

// UnCollect 取消收藏
func UnCollect(c *gin.Context) {
	db := common.GetDB()
	// 获取用户ID
	user, _ := c.Get("user")
	// 获取path中的index
	index, _ := strconv.Atoi(c.Params.ByName("index"))
	// 查找用户
	var curUser model.User
	db.Where("id = ?", user.(model.User).ID).First(&curUser)
	var newCollects []string
	newCollects = append(curUser.Collects[:index], curUser.Collects[index+1:]...)
	// 更新收藏夹
	if err := db.Model(&curUser).Update("collects", newCollects).Error; err != nil {
		response.Fail(c, nil, "更新失败")
		return
	}
	response.Success(c, nil, "更新成功")
}

2. 关注功能

关注功能的实现与收藏功能的实现类似,但要修改文章作者的粉丝数

// Following 查询关注
func Following(c *gin.Context) {
	db := common.GetDB()
	// 获取用户ID
	user, _ := c.Get("user")
	// 获取path中的id
	id := c.Params.ByName("id")
	var curUser model.User
	db.Where("id = ?", user.(model.User).ID).First(&curUser)
	// 判断是否已关注
	for i := 0; i < len(curUser.Following); i++ {
		if curUser.Following[i] == id {
			response.Success(c, gin.H{"followed": true, "index": i}, "查询成功")
			return
		}
	}
	response.Success(c, gin.H{"followed": false}, "查询成功")
}

// NewFollow 新增关注
func NewFollow(c *gin.Context) {
	db := common.GetDB()
	// 获取用户ID
	user, _ := c.Get("user")
	// 获取path中的id
	id := c.Params.ByName("id")
	// 查找用户
	var curUser model.User
	db.Where("id = ?", user.(model.User).ID).First(&curUser)
	//var newFollowing []string
	newFollowing := append(curUser.Following, id)
	// 更新关注列表
	if err := db.Model(&curUser).Update("following", newFollowing).Error; err != nil {
		response.Fail(c, nil, "更新失败")
		return
	}
	// 更新粉丝数
	var followUser model.User
	db.Where("id = ?", id).First(&followUser)
	if err := db.Model(&followUser).Update("fans", followUser.Fans+1).Error; err != nil {
		response.Fail(c, nil, "更新失败")
		return
	}
	response.Success(c, nil, "更新成功")
}

// UnFollow 取消关注
func UnFollow(c *gin.Context) {
	db := common.GetDB()
	// 获取用户ID
	user, _ := c.Get("user")
	// 获取path中的index
	index, _ := strconv.Atoi(c.Params.ByName("index"))
	// 查找用户
	var curUser model.User
	db.Where("id = ?", user.(model.User).ID).First(&curUser)
	//var newFollowing []string
	newFollowing := append(curUser.Following[:index], curUser.Following[index+1:]...)
	followId := curUser.Following[index]
	// 更新关注列表
	if err := db.Model(&curUser).Update("following", newFollowing).Error; err != nil {
		response.Fail(c, nil, "更新失败")
		return
	}
	// 更新粉丝数
	var followUser model.User
	db.Where("id = ?", followId).First(&followUser)
	if err := db.Model(&followUser).Update("fans", followUser.Fans-1).Error; err != nil {
		response.Fail(c, nil, "更新失败")
		return
	}
	response.Success(c, nil, "更新成功")
}

3. 跨域请求中间件

写到这里,我们后端的所有接口已经完成,但在前后端交互时,我们还需解决跨域问题

接下来我们来编写一个跨域请求的中间件处理跨域请求

在middleware文件夹下新建文件CORSMiddleware.go

/* middleware/CORSMiddleware.go/ */
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", "*")
		c.Writer.Header().Set("Access-Control-Allow-Headers", "*")
		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
		c.Writer.Header().Set("Access-Control-Max-Age", "86400")
		if c.Request.Method == http.MethodOptions {
			c.AbortWithStatus(200)
		} else {
			c.Next()
		}
	}
}

4. 修改路由

我们将处理跨域请求的中间件放在开头,同时也使用路由组完成收藏和关注的接口

/* routes/routes.go */
func CollectRoutes(r *gin.Engine) *gin.Engine {
	// 允许跨域访问
	r.Use(middleware.CORSMiddleware())
	// 注册
	r.POST("/register", controller.Register)
	// 登录
	r.POST("/login", controller.Login)
	// 上传图像
	r.POST("/upload", controller.Upload)
	r.POST("/upload/rich_editor_upload", controller.RichEditorUpload)
	// 用户信息管理
	userRoutes := r.Group("/user")
	userRoutes.Use(middleware.AuthMiddleware())
	userRoutes.GET("", controller.GetInfo)                         // 验证用户
	userRoutes.GET("briefInfo/:id", controller.GetBriefInfo)       // 获取用户简要信息
	userRoutes.GET("detailedInfo/:id", controller.GetDetailedInfo) // 获取用户详细信息
	userRoutes.PUT("avatar/:id", controller.ModifyAvatar)          // 修改头像
	userRoutes.PUT("name/:id", controller.ModifyName)              // 修改用户名
	// 我的收藏
	colRoutes := r.Group("/collects")
	colRoutes.Use(middleware.AuthMiddleware())
	colRoutes.GET(":id", controller.Collects)        // 查询收藏
	colRoutes.PUT("new/:id", controller.NewCollect)  // 收藏
	colRoutes.DELETE(":index", controller.UnCollect) // 取消收藏
	// 我的关注
	folRoutes := r.Group("/following")
	folRoutes.Use(middleware.AuthMiddleware())
	folRoutes.GET(":id", controller.Following)      // 查询关注
	folRoutes.PUT("new/:id", controller.NewFollow)  // 关注
	folRoutes.DELETE(":index", controller.UnFollow) // 取消关注
	// 查询分类
	r.GET("/category", controller.SearchCategory)         // 查询分类
	r.GET("/category/:id", controller.SearchCategoryName) // 查询分类名
	//用户文章的增删查改
	articleRoutes := r.Group("/article")
	//articleRoutes.Use(middleware.AuthMiddleware())
	articleController := controller.NewArticleController()
	articleRoutes.POST("", middleware.AuthMiddleware(), articleController.Create)      // 发布文章
	articleRoutes.PUT(":id", middleware.AuthMiddleware(), articleController.Update)    // 修改文章
	articleRoutes.DELETE(":id", middleware.AuthMiddleware(), articleController.Delete) // 删除文章
	articleRoutes.GET(":id", articleController.Show)                                   // 查看文章
	articleRoutes.POST("list", articleController.List)                                 // 显示文章列表
	return r
}

5. 接口测试

添加收藏

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

取消收藏

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

添加关注

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

取消关注

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

(九)总结

恭喜你已完成了后端的搭建,接下来将介绍前端的搭建过程~

  • 15
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值