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

项目展示视频
项目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.ModelUserName    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 = ssreturn 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 = dbreturn 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.Userc.Bind(&requestUser)userName := requestUser.UserNamephoneNumber := requestUser.PhoneNumberpassword := requestUser.Password// 数据验证var user model.Userdb.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 uintjwt.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密钥生成tokentoken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)tokenString, err := token.SignedString(jwtKey)if err != nil {return "", err}// 返回tokenreturn tokenString, nil
}

5. 登录功能

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

/* controller/UserController.go */
// Login 登录
func Login(c *gin.Context) {db := common.GetDB()// 获取参数var requestUser model.Userc.Bind(&requestUser)phoneNumber := requestUser.PhoneNumberpassword := requestUser.Password// 数据验证var user model.Userdb.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}// 发放tokentoken, 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 headertokenString := c.Request.Header.Get("Authorization")// token为空if tokenString == "" {c.JSON(http.StatusOK, gin.H{"code": 401,"msg":  "权限不足",})c.Abort()return}// 非法tokenif tokenString == "" || len(tokenString) < 7 || !strings.HasPrefix(tokenString, "Bearer") {c.JSON(http.StatusUnauthorized, gin.H{"code": 401,"msg":  "权限不足",})c.Abort()return}// 提取token的有效部分tokenString = tokenString[7:]// 解析tokentoken, claims, err := common.ParseToken(tokenString)// 非法tokenif err != nil || !token.Valid {c.JSON(http.StatusUnauthorized, gin.H{"code": 401,"msg":  "权限不足",})c.Abort()return}// 获取claims中的userIduserId := claims.UserIdDB := common.GetDB()var user model.UserDB.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.Filenameext := path.Ext(filename)// 用上传时间作为文件名name := "image_" + time.Now().Format("20060102150405")newFilename := name + extout, 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.Categoryif 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中的分类idcategoryId := 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.Timefunc (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.Timevar 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 = dbreturn 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中的idarticleId := c.Params.ByName("id")// 查找文章var article model.Articleif a.DB.Where("id = ?", articleId).First(&article).RecordNotFound() {response.Fail(c, nil, "文章不存在")return}// 获取登录用户user, _ := c.Get("user")userId := user.(model.User).IDif 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中的idarticleId := c.Params.ByName("id")// 查找文章var article model.Articleif a.DB.Where("id = ?", articleId).First(&article).RecordNotFound() {response.Fail(c, nil, "文章不存在")return}// 获取登录用户user, _ := c.Get("user")userId := user.(model.User).IDif 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中的idarticleId := c.Params.ByName("id")// 查找文章var article model.Articleif 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 []stringvar 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 stringif 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中的userIduserId := c.Params.ByName("id")// 判断用户身份user, _ := c.Get("user")var curUser model.Userif 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中的userIduserId := c.Params.ByName("id")// 判断用户身份user, _ := c.Get("user")//var self boolvar curUser model.Userif userId == strconv.Itoa(int(user.(model.User).ID)) {//self = truecurUser = user.(model.User)} else {//self = falsedb.Where("id = ?", userId).First(&curUser)if curUser.ID == 0 {response.Fail(c, nil, "用户不存在")return}}// 返回用户详细信息var articles, collects []model.ArticleInfovar following []model.UserInfovar collist, follist []stringcollist = 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()// 获取用户IDuser, _ := c.Get("user")// 获取参数var requestUser model.Userc.Bind(&requestUser)avatar := requestUser.Avatar// 查找用户var curUser model.Userdb.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()// 获取用户IDuser, _ := c.Get("user")// 获取参数var requestUser model.Userc.Bind(&requestUser)userName := requestUser.UserName// 查找用户var curUser model.Userdb.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()// 获取用户IDuser, _ := c.Get("user")// 获取path中的idid := c.Params.ByName("id")var curUser model.Userdb.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()// 获取用户IDuser, _ := c.Get("user")// 获取path中的idid := c.Params.ByName("id")// 查找用户var curUser model.Userdb.Where("id = ?", user.(model.User).ID).First(&curUser)var newCollects []stringnewCollects = 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()// 获取用户IDuser, _ := c.Get("user")// 获取path中的indexindex, _ := strconv.Atoi(c.Params.ByName("index"))// 查找用户var curUser model.Userdb.Where("id = ?", user.(model.User).ID).First(&curUser)var newCollects []stringnewCollects = 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()// 获取用户IDuser, _ := c.Get("user")// 获取path中的idid := c.Params.ByName("id")var curUser model.Userdb.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()// 获取用户IDuser, _ := c.Get("user")// 获取path中的idid := c.Params.ByName("id")// 查找用户var curUser model.Userdb.Where("id = ?", user.(model.User).ID).First(&curUser)//var newFollowing []stringnewFollowing := append(curUser.Following, id)// 更新关注列表if err := db.Model(&curUser).Update("following", newFollowing).Error; err != nil {response.Fail(c, nil, "更新失败")return}// 更新粉丝数var followUser model.Userdb.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()// 获取用户IDuser, _ := c.Get("user")// 获取path中的indexindex, _ := strconv.Atoi(c.Params.ByName("index"))// 查找用户var curUser model.Userdb.Where("id = ?", user.(model.User).ID).First(&curUser)//var newFollowing []stringnewFollowing := 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.Userdb.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. 接口测试

添加收藏

取消收藏

添加关注

取消关注

(九)总结

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

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

  1. CSDN博客文章阅读模式插件(附源码)

    插件地址:https://greasyfork.org/zh-CN/scripts/380667-csdn%E5%8D%9A%E5%AE%A2%E9%98%85%E8%AF%BB%E6%A8%A1%E ...

  2. Android App开发实战项目之大头贴App功能实现(附源码和演示 简单易上手)

    需要图片集和源码请点赞关注收藏后评论区留言~~~ 一.需求描述 大头贴App有两个特征,第一个是头要大,拿来一张照片后把人像区域裁剪出来,这样新图片里的人头才会比较大,第二个是在周围贴上装饰物,而且装 ...

  3. Spring Boot 专栏全栈开发实战

    2020 年 11 月 12 日,Spring 官方发布了 Spring Boot 2.4.0 GA 的公告,链接为 Spring Boot 2.4.0 available now.为了让大家能够学习 ...

  4. 【哈士奇赠书活动 - 18期】-〖Flask Web全栈开发实战〗

    文章目录 ⭐️ 赠书活动 - <Flask Web全栈开发实战> ⭐️ 编辑推荐 ⭐️ 内容提要 ⭐️ 赠书活动 → 获奖名单 ⭐️ 赠书活动 - <Flask Web全栈开发实战& ...

  5. Spring Boot+Vue全栈开发实战——花了一个礼拜读懂了这本书

    很幸运能够阅读王松老师的<Spring Boot+Vue全栈开发实战>这本书!之前也看过Spring Boot与Vue的相关知识,自己也会使用了Spring Boot+Vue进行开发项目. ...

  6. ChatGPT全栈开发实战:从需求分析到数据可视化,一站式指南助你快速构建全面应用

    <ChatGPT全栈开发实战:从需求分析到数据可视化,一站式指南助你快速构建全面应用>是一本旨在引领我们进入全栈开发世界的综合指南.通过结合强大的ChatGPT技术和全栈开发的实践,我们将 ...

  7. 【Python开发】Flask开发实战:个人博客(三)

    Flask开发实战:个人博客(三) 在[Python开发]Flask开发实战:个人博客(一) 中,我们已经完成了 数据库设计.数据准备.模板架构.表单设计.视图函数设计.电子邮件支持 等总体设计的内容 ...

  8. 读书笔记《Spring Boot+Vue全栈开发实战》(下)

    本书将带你全面了解Spring Boot基础与实践,带领读者一步步进入 Spring Boot 的世界. 前言 第九章 Spring Boot缓存 第十章 Spring Boot安全管理 第十一章 S ...

  9. ehcache springboot_阿里内部进阶学习SpringBoot+Vue全栈开发实战文档

    前言 Spring 作为一个轻量级的容器,在JavaEE开发中得到了广泛的应用,但是Spring 的配置烦琐臃肿,在和各种第三方框架进行整合时代码量都非常大,并且整合的代码大多是重复的,为了使开发者能 ...

最新文章

  1. 神策数据房东雨:精准推荐的场景和实践
  2. scikit-learn学习笔记(六)Decision Trees(决策树)
  3. FPGA构造spi时序——AD7176为例(转)
  4. angularjs1-8,cacheFactory,sce
  5. Mac文件夹图标颜色自定义工具Color Folder
  6. Java高并发 -- 并发扩展
  7. 如何重置HDX卡的固件(firmware)
  8. lamp整合三连发(1)
  9. P-NUCLEO-IHM001 板载STLINK 驱动安装
  10. 云服务商拿来主义或大限将至,Elastic 表示将变更开源许可协议并进行诉讼
  11. HTTP 和 HTTPS 有什么区别?
  12. debain系统安装nginx
  13. Java编译错误与运行时错误区别
  14. 05 爬虫应用(2)——抓取昵图性感美女图片(针对传统翻页图片版本)
  15. Kubernetes(K8s)优势究竟是什么?
  16. Spring Security Oauth2 认证流程
  17. 高校供需撮合交易平台规范管理及创新发展
  18. Linux中ls -al(ls -l)命令中的各个参数的含义
  19. 2016银行卡BIN
  20. 强化学习基础知识梳理(4)

热门文章

  1. win7 update
  2. 【opencv-c++】cv::imread函数读取图像
  3. stm32智能家居+微信小程序接收控制
  4. 1DCNN 2DCNN LeNet5,VGGNet16使用tensorflow2.X实现
  5. Java中不能做switch参数的数据类型
  6. 深入分析ReentrantLock公平锁和非公平锁的区别
  7. AQS中的公平锁和非公平锁
  8. 相机视频实时人脸追踪检测
  9. 内蒙古自治区关于加快充换电基础设施建规划 安科瑞 许敏
  10. 关于大学新青年织梦后台的操作